Architecture

Security Model

MCP Ambassador is designed with defense-in-depth. Every layer independently enforces security — authentication, authorization, encrypted credential storage, audit logging, and kill switches.


Security principles

  1. Zero trust — every request is authenticated and authorized, no implicit trust
  2. Least privilege — users get the minimum tool set required for their role
  3. Defense in depth — authentication, authorization, audit, rate limiting, kill switches
  4. Credential isolation — downstream MCP credentials never leave the server; clients never see them
  5. Encryption in transit — TLS for all client-server and server-MCP communication
  6. Encryption at rest — secrets encrypted in the backing store (AES-256-GCM)
  7. Audit everything — every tool call, every auth decision, every config change

Credential vault

Per-user credential storage uses AES-256-GCM with HKDF key derivation.

How it works:

  1. Server generates a master key on first boot (32 bytes, stored at $DATA_DIR/credential_master_key, permissions 0600)
  2. Each user gets a unique vault_salt (32 bytes, stored in the users table)
  3. When a user enters credentials for an MCP:
    • User key = HKDF(master_key, salt=vault_salt, info="mcpambassador-credential-vault-v1")
    • Encrypted = AES-256-GCM(user_key, plaintext=credentials)
    • Stored in user_credentials table with IV
  4. When spawning a per-user MCP:
    • Decrypt credentials with user's derived key
    • Inject into MCP process environment
    • Zero memory after use

Threat model (Community tier):

ScenarioProtected?
Stolen database alone✅ Cannot decrypt (needs master key + user salt)
Stolen master key alone✅ Cannot decrypt (needs database + salt)
Database + master key stolen⚠️ Full compromise (accepted for community tier)

Downstream MCP isolation

Downstream stdio MCPs are third-party code, and some of them hold per-user secrets. Since v0.10.0 the server contains each one:

  • Its own uid. Every MCP instance (a shared MCP, each user's instance of a per-user MCP, and discovery runs) is started under a separate unprivileged uid from MCP_AMBASSADOR_CHILD_UID_RANGE (default 20000-20999). One MCP cannot read another's /proc/<pid>/environ, files, or home, and cannot signal or kill other processes.
  • Zero capabilities. Children are launched through setpriv with all capabilities cleared and --no-new-privs always set (with isolation on, the default), so they cannot switch uid back or regain privileges, even if the container runs without the compose no-new-privileges option. The image also strips setuid and setgid bits from its files. Keep security_opt: no-new-privileges for the server itself: it logs a warning once, at the first MCP spawn check, when that is missing.
  • Private home and cache. Each uid has a 0700 home under /tmp/mcp-homes/<uid>, which also holds its npm and uv caches. The homes are wiped when a uid is reassigned to a different instance.
  • No access to /data. The database, credential vault key, session secret and TLS keys are owned by the server's uid 1000 and are unreadable to children. The server's environment is not passed on; children get only a whitelisted environment plus what you configured for that MCP.
  • Per-instance process cap. Each child runs with its own RLIMIT_NPROC (default 256, MCP_AMBASSADOR_CHILD_NPROC; it counts threads), and the container has a pids limit of 4096. When an instance stops, any process left under its uid is killed.
  • Clean PID 1. tini is PID 1 (as uid 1000), so orphaned grandchildren are reaped. No root process remains after startup.
  • Fails closed. If isolation is configured and the required capabilities or tools are missing, or the uid map cannot be read, the MCP does not start.

Isolation can be switched off (range emptied and no legacy uid set). In that mode children run as the server uid and can read /data. Do not do this in production. The server logs a warning at startup when isolation is disabled.

Residual risks. This is process-level isolation, not a sandbox:

  • MCP instances share the container's network namespace: they can open outbound connections and reach local ports.
  • They also share the process list. /proc/<pid>/cmdline of other processes stays readable, so never pass secrets as command-line arguments; use environment variables, which other uids cannot read. Per-child network and PID namespaces are tracked as follow-up work (TD-063).
  • There is no size cap on the per-instance caches (TD-062). Homes and package caches live under the ./cache host directory, so a busy or hostile MCP can fill the host disk. Monitor free space on that mount.
  • The server process keeps CAP_SETUID, CAP_SETGID and CAP_KILL to manage children, so a compromise of the server itself is still serious.

See Docker for the required container settings.


Session security

Sessions use HMAC-SHA256 signed tokens:

  • Preshared key (amb_pk_) — admin-issued, stored by developer in config
  • Session token (amb_st_) — ephemeral, issued on registration, expires after idle timeout
  • Session secret — HMAC signing secret, persisted to survive server restarts
  • Cookie security — httpOnly, secure, sameSite: strict for web UI sessions

Login rate limiting: 5 attempts per IP per 5 minutes.


Password security

User passwords are hashed with Argon2id:

  • Argon2id is the recommended algorithm from OWASP and the Password Hashing Competition
  • Minimum password strength: score 3+ (zxcvbn), 12+ characters
  • Passwords compared using crypto.timingSafeEqual to prevent timing attacks
  • Session ID regenerated on login to prevent session fixation

Transport security

TLS is always on:

  • Development: Auto-generated self-signed certificates on first boot (TOFU model)
  • Production: Configure CA-signed certificates via environment variables
  • Client trust: Client receives server's CA fingerprint on first registration; must confirm trust
  • CORS: the admin API sends no CORS headers (origin: false)

Content Security Policy

The React SPA enforces strict CSP headers:

  • No inline scripts (all code in external bundles)
  • Source maps removed from production build
  • CSRF protection via sameSite: strict cookies + double-submit cookie pattern

Security audit history

MCP Ambassador underwent multiple security reviews during development:

  • ADR-001/003 review — Resolved findings F-001 through F-005 (timing attacks, session fixation)

  • Architecture review — Resolved findings F-006 through F-016 (credential handling, key management)

  • M26 credential vault review — Resolved race conditions, memory zeroing, env var injection

  • Phase 3 review — Resolved CSP, source maps, dependency audit (0 vulnerabilities)

    0.10.0 ships with an extensive automated test suite covering authentication, RBAC, credential vault, and full subscription lifecycle.


Responsible disclosure

Found a security issue? Please report it via GitHub Issues with the security label, or contact the maintainers directly. Do not disclose publicly until a fix is available.

Previous
Client & Server