How Gig'MCP keeps API keys out of MCP servers entirely
Most approaches to MCP security try to limit what a server can do with your API keys. Gig’MCP takes a different position: the server should never have your keys in the first place. Not encrypted, not scoped, not temporarily. Never.
This post walks through how that actually works: the placeholder trick, the vault, the egress proxy, and why the whole thing holds up even against a server that’s actively trying to cheat. (Gig’MCP is pre-launch; everything below describes the implemented architecture in the open-source gateway.)
The core move: placeholder in, real key out
A sealed-tier MCP server running under Gig’MCP doesn’t get SLACK_BOT_TOKEN=xoxb-real-token in its environment. It gets a placeholder:
SLACK_BOT_TOKEN=PLACEHOLDER
The server doesn’t know and doesn’t care. It builds its requests exactly as it always would (Authorization: Bearer PLACEHOLDER) and makes its HTTPS call to slack.com.
That call can only go one place. Each sandbox lives in its own network namespace whose only route leads to the gateway’s embedded egress proxy. The proxy:
- Identifies the tenant from the connection’s source IP. Each sandbox is assigned a unique veth pair and /30 subnet at spawn time, recorded by the supervisor. The source IP resolves to exactly one (server, user, profile) triple, and it’s unforgeable, because a network namespace cannot source addresses outside its own /30.
- Checks the destination against the server’s manifest allowlist at CONNECT time, before any TLS work happens.
slack.comis on slack-mcp’s allowlist;attacker.exampleis not, and the connection dies right there. - Swaps the placeholder for the real key, fetched and decrypted from the vault for that user’s stored credential, rewriting the header per the manifest’s injection spec (
header: Authorization,format: "Bearer {token}"). - Forwards the request and writes an audit record: user, server, domain, method, timestamp. Because identity comes from the network layer, the audit log is a free byproduct rather than something servers self-report.
The real token exists inside the request only on the trusted side of the boundary, after the sandbox has already handed the traffic over.
Yes, it’s a MITM, deliberately
To rewrite a header inside an HTTPS request, the proxy has to terminate TLS. Gig’MCP’s proxy is a hand-rolled CONNECT-style MITM built on Go’s standard library (the popular Go proxy libraries turned out unmaintained when we spiked them; stdlib was simpler and gave cleaner hooks for source-IP lookup and header rewrite).
At startup the gateway generates a runtime ECDSA P-256 certificate authority. When a sandboxed server connects to an allowlisted host, the proxy mints a leaf certificate for that hostname (cached per host), presents it to the client, decrypts the stream, parses the HTTP request, rewrites the credential header, and re-encrypts toward the real destination over a normal verified TLS connection. The CA is injected into the sandbox via the standard mechanisms (SSL_CERT_FILE, NODE_EXTRA_CA_CERTS), so HTTP clients inside trust it natively.
Note the ordering: the allowlist check happens at CONNECT, before any leaf is minted. The proxy won’t even begin a TLS handshake for a destination the manifest doesn’t permit.
One important subtlety: HTTPS_PROXY is set inside the sandbox purely as a convenience for well-behaved HTTP libraries. It is not the enforcement. Enforcement is routing: the namespace’s only route is the proxy, so traffic arrives there regardless of what the process does with its environment. We proved to ourselves early that env-only proxying is bypassable by any code that simply ignores the variable; route isolation is what actually holds.
The vault: envelope encryption, keys never in the database
Where do the real keys live? In the gateway’s credential vault, encrypted with textbook envelope encryption:
- Each secret is encrypted with its own data encryption key (DEK) using XChaCha20-Poly1305 (libsodium-style primitives via Go’s
x/crypto). - Each DEK is wrapped by a master key (KEK) supplied via the
GIG_MASTER_KEYenvironment variable or a Docker_FILEsecret. The KEK is never stored in the database. - Ciphertext headers carry versioned key IDs, so master-key rotation doesn’t require a big-bang re-encryption.
Steal the SQLite or Postgres database and you hold ciphertext. Decryption requires the master key, which exists only in the gateway’s runtime environment, on hardware you control, since the whole stack is self-hosted.
What an attacker actually gets
Walk the failure modes assuming a fully malicious server:
- Dump the environment?
PLACEHOLDER. - Exfiltrate to your own domain? Not on the manifest allowlist; the proxy refuses the CONNECT. Manifests are PR-gated in the registry, where lint CI rejects bare wildcards and a denylist of known exfiltration endpoints (pastebin-alikes, request bins, raw IPs).
- Send the placeholder somewhere allowed? The string is worthless. Injection happens per-tenant, per-domain, only via the proxy’s rewrite path.
- Ignore
HTTPS_PROXYand open a raw socket? The route table sends it to the proxy anyway. There is no other gateway out of the namespace; as belt-and-suspenders, the gateway also sets the host’sFORWARDpolicy to drop. - Forge another tenant’s source IP? Can’t. The namespace can only source its own /30, and identity binding happens on the accepted connection’s address.
- Escape the sandbox and read the vault? That requires defeating user/PID/mount/network namespaces, a zero-capability uid-65534 process, and a seccomp filter that kills namespace and mount syscalls. That’s the subject of our sandboxing post.
The remaining attack worth naming honestly: a malicious server can misuse the API access you granted, through the allowlisted domain, as you. It could, say, write garbage into your Slack. Credential injection caps the blast radius at “what this server was entitled to do”; it cannot make a granted capability safe. What it removes entirely is the worst outcome: credential theft. Keys can’t be stolen from a place they never were.
The escape hatch, labeled honestly
Not every secret travels as an HTTP header the proxy can rewrite. Database connection strings, certificate-pinned clients, custom binary protocols: for these, manifests can declare Tier 2, “entrusted”. The real secret does go into the sandbox environment, with the egress allowlist still enforced by routing.
The tier is part of the manifest and surfaced at install time, because it changes the threat model: an entrusted server could exfiltrate its own secret through an allowed domain (say, into a gist, if its allowlist includes GitHub). Sealed is the default and the goal; entrusted is the clearly-labeled exception. We’d rather give you an honest two-tier model than pretend one mechanism covers everything.
Why this beats the alternatives
- Scoped/short-lived tokens reduce damage but still hand the credential to untrusted code, and most SaaS APIs don’t offer fine-grained scoping anyway.
- Secrets managers (Vault and friends) protect storage and delivery, but the consuming process still ends up holding the secret. The exfiltration problem is untouched.
- “Just audit the code” doesn’t survive contact with transitive dependencies and supply-chain attacks at ecosystem scale.
Injection at the network boundary is different in kind: the untrusted code operates on a credential it does not possess and cannot obtain, while enforcement lives in a layer (kernel routing plus a proxy outside the sandbox) it cannot reach.
If you want to go deeper, the docs cover the architecture, security model, and manifest schema; the gateway is AGPL-3.0 on GitHub. Gig’MCP is pre-launch. Watch the repo to hear when the first release ships.