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
- Zero trust — every request is authenticated and authorized, no implicit trust
- Least privilege — users get the minimum tool set required for their role
- Defense in depth — authentication, authorization, audit, rate limiting, kill switches
- Credential isolation — downstream MCP credentials never leave the server; clients never see them
- Encryption in transit — TLS for all client-server and server-MCP communication
- Encryption at rest — secrets encrypted in the backing store (AES-256-GCM)
- 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:
- Server generates a master key on first boot (32 bytes, stored at
$DATA_DIR/credential_master_key, permissions 0600) - Each user gets a unique
vault_salt(32 bytes, stored in the users table) - 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_credentialstable with IV
- User key =
- 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):
| Scenario | Protected? |
|---|---|
| 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
setprivwith all capabilities cleared and--no-new-privsalways set (with isolation on, the default), so they cannot switch uid back or regain privileges, even if the container runs without the composeno-new-privilegesoption. The image also strips setuid and setgid bits from its files. Keepsecurity_opt: no-new-privilegesfor 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
0700home 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 apidslimit of 4096. When an instance stops, any process left under its uid is killed. - Clean PID 1.
tiniis 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>/cmdlineof 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
./cachehost 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_SETGIDandCAP_KILLto 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: strictfor 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.timingSafeEqualto 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: strictcookies + 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.