Security & Threat Model

Gatehouse is designed for the homelab threat model: a single operator, one or more AI agents with varying degrees of trust, and a small set of credentials worth protecting. This page documents what the system protects against and the assumptions it relies on.

What Gatehouse protects#

  • Credential leaks from agent context windows. Proxy mode keeps the key server-side. The agent only sees the API response.
  • Secret sprawl. All credentials live in one place with a single audit log.
  • Static credential exposure. Dynamic secrets eliminate long-lived database and SSH credentials.
  • Privileged token misuse. Per-agent AppRoles with scoped policies replace shared tokens.
  • Accidental logging of credentials. The scrubber catches 15+ common credential formats in MCP and REST responses.
  • Encryption at rest. Every secret is encrypted with a per-secret DEK, which is encrypted with a KEK derived from the master key via HKDF-SHA256.

What Gatehouse does not protect against#

  • Host compromise. If an attacker gets root on the container host, they can read the master key from process memory, /proc/$PID/environ, or via docker inspect if the master key is set as an env var. For higher-trust deployments, pass the master key via a Docker secrets mount instead of an env var.
  • A malicious admin. An admin user with the admin capability can read any secret and revoke any lease. The audit log records what they did, but it does not prevent them.
  • Side channels in upstream APIs. If an agent proxies a request and the upstream API includes the credential in its response (a misconfiguration on the upstream’s side), Gatehouse cannot un-leak it.

Deployment recommendations#

Master key handling#

The GATEHOUSE_MASTER_KEY is never written to disk by Gatehouse. Losing it means losing access to every encrypted secret. Store the master key:

  • For homelabs: in a password manager separate from the one that holds Gatehouse credentials, plus a printed backup in a safe place.
  • For higher-trust deployments: use Docker secrets (mounted as a file at /run/secrets/master_key) instead of an env var, so the key does not appear in docker inspect output or /proc/$PID/environ.

Root token handling#

The root token is for bootstrapping only. Create a real admin user via the web UI, log in as that user, verify everything works, then:

  1. Stop the container.
  2. Remove the GATEHOUSE_ROOT_TOKEN env var from your docker-compose.yml or docker run command.
  3. Restart the container.

The root token still works for bootstrapping if you set it again later, but leaving it set in production bypasses every access control check.

Policy discipline#

Give each agent its own AppRole with the minimum capabilities it needs. An agent that only reads from api-keys/* should not have delete or admin. The web UI makes it easy to inspect which policies each AppRole references and what paths each policy covers.

Network exposure#

Gatehouse listens on port 3100 by default. For homelab use, keep it on a private network. If you must expose it to the internet, put it behind a reverse proxy with TLS (Caddy, nginx, Traefik) and consider IP allowlisting on the proxy.

Trusting the client IP behind a proxy#

The client IP drives every network-origin control: AppRole IP allowlists, the rotate-token IP allowlist, and the lease/proxy auto_approve_from_ip gate. Because X-Forwarded-For is just a request header, Gatehouse honors it only when the request’s immediate TCP peer is a trusted reverse proxy. Otherwise the real socket address wins, so a client that sets its own X-Forwarded-For cannot impersonate a trusted network.

Loopback (127.0.0.0/8, ::1) is always trusted, which covers a reverse proxy running on the same host. If your proxy is on a separate host or a Docker network whose gateway is not loopback, list its address in GATEHOUSE_TRUSTED_PROXIES (comma-separated CIDRs, additive to the loopback defaults). When a forwarded chain is present, Gatehouse walks it right-to-left and takes the first hop that is not itself a trusted proxy as the real client.

If you run behind such a proxy and rely on IP allowlists or auto-approve but leave GATEHOUSE_TRUSTED_PROXIES unset, these controls fail closed: the source IP becomes the proxy’s address, allowlisted logins are denied, and auto-approve falls back to the human approval flow. Direct-connection deployments need no configuration.

Proxy hardening#

The proxy (POST /v1/proxy and the MCP gatehouse_proxy tool) lets agents call upstream HTTP APIs without ever holding the credential. Three layers of scoping apply, in order:

  1. Policy. The agent’s AppRole must have the proxy capability on the secret it wants to inject. No policy match, no proxy call.
  2. allowed_domains per secret. Restricts which hostnames the secret can be forwarded to. A secret with allowed_domains = "api.openai.com" cannot be used to call evil.com even if the agent’s policy allows proxy on it.
  3. allowed_path_prefixes per secret. Restricts which URL paths a secret can hit, useful for scoping a broad credential (e.g. a fine-grained GitHub PAT) to a single repo’s API surface. Match is path-segment aligned.

Private-network access is allowed by default (GATEHOUSE_PROXY_ALLOW_PRIVATE is unset or anything other than "false"). This matches the homelab posture where your services live on 10.x or 192.168.x and blocking them by default would make the proxy useless. For deployments exposed to the public internet, set GATEHOUSE_PROXY_ALLOW_PRIVATE=false to fail-closed against 127.x, 169.254.169.254, 10.0.0.0/8, etc., and selectively re-enable per secret with metadata.allow_private=true.

The per-secret allowed_domains allowlist is the primary host-scoping control. The private-network flag is defense-in-depth for the SSRF class specifically.

SSO assurance shift#

When SSO is enabled, the IdP becomes the identity authority for the human-account login path. Specifically:

  • Local TOTP is bypassed for SSO logins, even if the user has TOTP enrolled at Gatehouse. The assumption is the IdP is enforcing its own MFA.
  • The email_verified claim on the OIDC userinfo response is the safety net against IdPs that let users self-attest emails. Default-strict; the per-deployment trust_unverified_email opt-in exists for IdPs (PocketID) that intentionally omit the claim because the email is administrator-controlled at the IdP.
  • Username and access-token login paths still require local TOTP per the user’s enrollment. SSO is the only path that defers MFA to the IdP.

If your IdP’s authentication assurance is weaker than your local TOTP setup, leave SSO disabled. The SSO docs page details the model.

Cryptographic details#

  • Symmetric encryption: XSalsa20-Poly1305 via tweetnacl. Per-secret random 32-byte DEK, encrypted with a KEK derived from the master key via HKDF-SHA256 with a domain separation label.
  • Password hashing: Argon2id via Bun.password.hash with default parameters (64 MiB memory, 3 iterations, parallelism 1).
  • JWT signing: HS256 with the GATEHOUSE_JWT_SECRET env var (generated at first startup if unset and persisted to the config directory).
  • TOTP: RFC 6238, SHA1, 6 digits, 30-second period.
  • Recovery codes: 10 one-time codes per user, each 8 characters, hashed with Argon2id before storage.