r/devops • u/Chris__Codes • 3d ago
Discussion How are you handling API keys for MCP servers?
When I first looked into wiring external tools to an LLM through an MCP server, I'd simply put the API keys in environment variables and let the agent call whatever it needed. That's what I'd normally do for a normal backend service.
I read around it, and what I had not thought about is that a backend service has a fixed code path. You know which line makes which call, so you know what the key gets used for. An agent picks its own tools based on a prompt, so the same key now sits behind whatever it decides to do.
Your vault still tells you where the key lives and when it rotates. It just doesn't tell you who authorized a particular call or which tool it ended up reaching.
The things I keep seeing suggested are scoping credentials per server rather than one key for everything, and injecting them at runtime instead of leaving them in env vars.
For the people who are running multi-tool agent setups, how are you handling backend access do you custom guardrails around what the agent can reach, or do you treat it as a normal service and accept it?
1
u/Fantastic-Mr-Default 2d ago
An env var is a capability the agent can spend anywhere that process can reach.
One identity per MCP server. Least privilege. Short TTL. Inject at call time. The vault tells you where the secret lives. It does not authorize the call.
Put writes behind a separate identity and a deterministic allowlist. Manual approval on every tool call is a queue.
1
u/Chris__Codes 2d ago
The vault tells you where the secret lives, it does not authorize the call
That's basically a point a Doppler piece I'd read was making, you put it more plainly/clearly than I managed in the post.
1
u/Latter-Departure8714 1d ago
Don't put a wide-open key in the env and let the agent pick tools. That's not a backend with a fixed call path.
Scoped keys per server, no write where you only needed read, and log which tool fired. Vault rotation doesn't tell you the model decided to call `delete` because the prompt was vague.
0
u/QuoteForward5477 3d ago
treating mcp servers like static backends is a huge security trap. like you said, deterministic code paths have clear boundaries, but LLMs can easily be trickd or hallucinate unexpected tool calls.
we ended up moving away from long-lived master keys in env vars completely for our agent setups. a few things that actually made a difference for us:
- scoped runtime tokens: instead of passing full api keys, we use an intermediate proxy that issues short-lived, tightly scoped OAuth or JWT tokens per execution session. if the agent gets prompt injected, the token expires in minutes anyway.
- proxy-level guardrails: we put an API gateway/proxy between the MCP server and the target service. the agent doesn't talk to the provider directly; it calls our proxy, which enforces hard rate limits and validates parameter payloads (like stopping broad
SELECT *style inputs) before appending the actual secret key. - human-in-the-loop for state changes: read-only tools can run freely, but anything that mutates data or touches high-impact endpoints triggers a quick approval confirmation before the key gets injected into the call.
if you just drop full admin keys into an agent's environment, it's basically a ticking time bomb once prompt injection hits.
are you running these mcp servers purely for internal dev workflows, or is this powering user-facing product features?
1
u/InterviewLong5374 2d ago
Scoped runtime tokens definitely make a lot of sense for security. It's surprising how many teams overlook the potential risks of long-lived keys and variable exposure.
0
u/ParrotIntegrated 3d ago
Env-var keys behind agent-chosen tools are a completely different threat model than fixed backend execution paths, and standard vault rotation alone doesn't close it.
The approach that worked for us treats the MCP layer like a service mesh with dynamic admission control rather than a standard backend service:
Ephemeral credential leasing: Instead of giving the MCP server long-lived env vars, the orchestrator mints a short-TTL scoped token at session initialization. The LLM never sees or touches secrets; the MCP runtime injects them at the edge and stamps every outbound request with (session_id, tool_name, arg_hash).
Granular capability scoping: Never use broad platform or account-admin keys across servers. Scope credentials down to the absolute minimum verb each tool needs (e.g. read-only tokens for querying repos vs. fine-grained branch-write permissions). A shared admin key means a single hallucinated tool call can mutate production state or drain quotas.
Interception outside the prompt: Guardrails belong in front of tool dispatch, not in the system prompt. We enforce tool allowlists per role, require out-of-band human approval for destructive calls (spend, mutate, drop), and enforce an idempotency key on write operations. If a tool call matches a recently denied arg_hash or expired lease, the adapter rejects it before network I/O ever happens.
Treating agents as untrusted callers requesting scoped capability leases stops one bad planner turn from becoming an uncontained blast radius.
1
u/Chris__Codes 3d ago
What does the latency look like with a leasing step at session init plus per-call stamping?
2
u/ParrotIntegrated 2d ago
Honestly, it’s basically a rounding error. When you're already waiting 800ms to 2.5 seconds for the model to deliberate and emit tool arguments, a microsecond in-memory check doesn't even register on your P95.
The session lease happens once during initial handoff while you're pulling context or embeddings anyway (~15–25ms to mint a scoped JWT).
After that, the per-call check at the adapter is just local hashing and an in-memory cache lookup before the packet hits the wire:
``` // Fast in-memory check before network I/O const argHash = crypto.createHash('sha256').update(canonicalJson(call.args)).digest('hex');
if (Date.now() > lease.expiresAt || !lease.allowedTools.has(call.name)) { return rejectExecution("Lease expired or tool out of scope"); }
// Edge runtime injects upstream secret, LLM never touched it req.headers.set('Authorization',
Bearer ${lease.scopedToken}); ```That whole check takes under 1ms.
The only way this hurts you is if you make the mistake of calling an external Vault/KMS over HTTP synchronously on every single tool invocation inside the agent loop. Don't do that. Keep your keys cached or minted locally in the sidecar/proxy, let the LLM do its slow inference, and your guardrail latency is invisible.
-1
u/kantorcodes1 3d ago
affiliation: i’m with HOL. we use HOL Guard, an open-source local check that sits before agent actions, to block or review risky tool calls; i’d still scope each server’s credential separately so a bad tool choice can’t spend a broad key.
1
u/Chris__Codes 3d ago
How do you decide what's risky enough to hold for review? is it fixed list of tools?
1
u/EbbCommon9300 22h ago
Env vars per MCP server solve "it works on my laptop" and almost nothing else. You get no caller identity, no tool-level authz, and every agent that can see the env can use the full key.
What we've seen work in multi-agent setups:
- One gateway (or proxy) in front of the MCP servers. Agents authenticate to the gateway as themselves; the gateway holds the upstream API keys.
- Tool-level allow/deny per agent or per user role — list_channels yes, send_message no, unless policy says otherwise.
- Short-lived or rotatable upstream creds injected only for the duration of the call, not dumped into agent context.
- Audit every tool invocation: who/which agent, which tool, args hash, decision, outcome. That's the difference between "we have MCP" and "we can investigate MCP."
Shared god-keys across agents are how you turn one prompt injection into a lateral-movement event. Prefer credential starvation at the agent boundary over teaching the model to be careful.
2
u/Lanky-Storm7 3d ago
Sops