Skip to content

Security Model

mcp-unifi is designed to run on a trusted home or homelab LAN, behind your existing network boundary. This page covers the threat model, the hardening that’s baked into the image, and how to verify the supply chain.

  • Trusted boundary: the MCP server is not authenticated. Anyone who can reach http://<host>:3714/mcp can call every tool. Put it on a trusted LAN, behind a reverse proxy with auth, or behind a Tailscale ACL.
  • Local-network only: the server talks to your UniFi gateway over the local API (X-API-Key header to https://<gateway>/proxy/network/...). It does not call out to any Ubiquiti cloud endpoint, does not require a UI account, and does not require Site Manager / Cloud Console enrollment.
  • Self-signed gateway certs: most home gateways present a self-signed certificate. UNIFI_VERIFY_SSL defaults to false for that reason. Set it to true once you’ve installed a real certificate.
  • UNIFI_API_KEY and all per-controller api_key values are wrapped in Pydantic’s SecretStr. Reading the cleartext requires .get_secret_value(); repr() and structured logging never echo the raw key.
  • The startup safe_repr() log line includes only an api_key_set: true/false boolean per controller, never the key itself.
  • The structured logger has a redactor that scrubs known sensitive keys (api_key, passphrase, x_passphrase, password, secret) from any log record. WLAN passphrases are scrubbed [REDACTED] from every tool response, even in stub mode.
  • The audit log applies the same scrub to tool kwargs before writing.

The published image (ghcr.io/pete-builds/mcp-unifi) ships with:

  • Non-root: runs as UID 1000, no shell, no home directory.
  • Read-only root filesystem: enforced via Docker / Helm. /tmp is tmpfs for ephemeral writes (audit log, runtime caches).
  • no-new-privileges: prevents setuid escalation paths.
  • Capabilities dropped: all Linux capabilities dropped at the container boundary.
  • Hash-pinned deps: Python dependencies installed with pip --require-hashes from a hash-locked requirements.lock. The base image is pinned by digest.
  • Trivy scan: CI fails the build on any HIGH or CRITICAL vulnerability finding. Current status: zero findings.
  • Multi-arch: linux/amd64 and linux/arm64 from one release.

Every published image is signed via cosign keyless OIDC. The signing identity is the GitHub Actions workflow that built the image; verification confirms the image came from that workflow on this repo, not from someone with a stolen GHCR token.

Terminal window
cosign verify ghcr.io/pete-builds/mcp-unifi:latest \
--certificate-identity-regexp 'https://github.com/pete-builds/mcp-unifi' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com

A successful verification prints the signature payload (certificate subject, issuer, GitHub workflow ref). Failure means the tag does not have a valid signature from this repo.

Each release attaches a CycloneDX SBOM generated by Syft:

Terminal window
# Download from the GitHub release assets
curl -L -o sbom.cdx.json \
https://github.com/pete-builds/mcp-unifi/releases/download/v0.5.0-rc.2/sbom.cdx.json

The SBOM lists every Python package, OS package, and file digest baked into the image. Useful for vulnerability scanning, license auditing, or comparing across releases.

The release workflow uses docker/build-push-action with provenance: true, attaching a build attestation to each image. Inspect it with:

Terminal window
docker buildx imagetools inspect \
ghcr.io/pete-builds/mcp-unifi:latest \
--format '{{ json .Provenance }}'
  • No auth on the MCP endpoint. Out of scope. Use Tailscale, a reverse proxy with mTLS, or a NetworkPolicy.
  • No remote-control of the audit log. The log is local. Ship it to your SIEM with whatever you already use (Fluent Bit, syslog forwarder, etc).
  • No tenant isolation. One server instance = one tenant. Multi-tenant SaaS deployment is a post-v1.0 question.

For vulnerability reports, see SECURITY.md.