MCP Server Secrets Injection with HashiCorp Vault
How to inject short-lived, dynamic secrets into MCP servers at runtime using Vault and similar secret managers — without hardcoding credentials or leaking them through environment variables.
MCP (Model Context Protocol) servers are middleware that give AI agents access to tools — databases, APIs, file systems. They also concentrate risk: a single compromised MCP process can hand an attacker every credential it has ever touched. Static secrets in environment variables are the most common way teams bootstrap MCP servers, and they are also the biggest footgun.
This post covers runtime secrets injection: how to give an MCP server exactly the credentials it needs for a single request, have those credentials expire automatically, and never write a long-lived secret to disk or an environment variable.
Why environment variables are not enough
Environment variables feel safe because they aren't in source code. They aren't. They leak in:
ps auxoutput on shared hosts/proc/<pid>/environon Linux- Docker inspect output
- Log lines that print the full environment on startup
- Crash dumps and core files
More importantly, environment variables are static. Once a process starts, its environment doesn't change. A credential rotated in your secret manager is not rotated in a running MCP server unless you restart it. Teams often don't restart long-running servers frequently. The window between rotation and restart is an exposure window.
Dynamic secrets solve this by making credentials short-lived by design.
HashiCorp Vault dynamic secrets primer
Vault's dynamic secrets engine generates credentials on demand and automatically revokes them after a configurable TTL (time-to-live). The workflow for a database looks like this:
- Your orchestrator authenticates to Vault using a machine identity (AWS IAM, Kubernetes service account, AppRole, etc.).
- Vault issues a lease: a temporary username/password pair with a 15-minute TTL.
- Your MCP server uses those credentials for the duration of the request.
- Vault revokes them when the lease expires — or you revoke explicitly after the request finishes.
From Vault's perspective, every MCP request is a distinct identity with its own credentials. A leaked credential from one request cannot be reused after the lease expires.
Injection patterns
Pattern 1: Sidecar agent (recommended for containerised deployments)
Run a lightweight Vault agent sidecar alongside your MCP server container. The agent:
- Authenticates to Vault using the pod's service account token (Kubernetes auth method)
- Writes rendered secrets to a shared in-memory volume (tmpfs)
- Rotates the secrets before they expire
Your MCP server reads secrets from the tmpfs path, not from environment variables:
import { readFileSync } from "fs";
function getDbCredentials() {
// Vault agent renders this file and keeps it fresh
const raw = readFileSync("/vault/secrets/db.json", "utf8");
return JSON.parse(raw) as { username: string; password: string };
}
The key property: if you restart the MCP server, it picks up a fresh credential from the file. No environment variable mutation required.
Vault agent config snippet:
vault {
address = "https://vault.internal:8200"
}
auto_auth {
method "kubernetes" {
mount_path = "auth/kubernetes"
config = {
role = "mcp-server"
}
}
}
template {
destination = "/vault/secrets/db.json"
contents = <<EOT
{
"username": "{{ with secret "database/creds/mcp-readonly" }}{{ .Data.username }}{{ end }}",
"password": "{{ with secret "database/creds/mcp-readonly" }}{{ .Data.password }}{{ end }}"
}
EOT
}
Pattern 2: Per-request credential fetch
For MCP servers that handle requests with distinct security boundaries — for example, multi-tenant servers where each request belongs to a different customer — issue one Vault lease per request and revoke it when the request completes.
import Vault from "node-vault";
const vault = Vault({ endpoint: process.env.VAULT_ADDR });
async function withDbCredentials<T>(
fn: (creds: { username: string; password: string }) => Promise<T>
): Promise<T> {
const result = await vault.read("database/creds/mcp-readonly");
const { username, password, lease_id } = result.data;
try {
return await fn({ username, password });
} finally {
// Revoke immediately rather than waiting for TTL
await vault.revokeSelf({ lease_id });
}
}
This pattern gives you audit logs per request — Vault records every lease issuance with the requesting identity. If a credential is misused, you have a timestamp, the issuing identity, and the lease ID to pivot on.
Pattern 3: Response wrapping for untrusted pipes
When an MCP server is spawned by an orchestrator that itself may be compromised, you don't want the orchestrator to see the raw credential. Vault's response wrapping packages a credential in a one-time-use token: only the entity that unwraps it sees the plaintext value.
# Orchestrator requests a wrapped token (TTL=60s, one-time use)
vault read -wrap-ttl=60s database/creds/mcp-readonly
# Returns: wrapping_token=s.XXXXXXXXXXXXXXX
# Pass only the wrapping token to the MCP server
# MCP server unwraps it:
vault unwrap s.XXXXXXXXXXXXXXX
The orchestrator never sees the username or password — only a token that will expire in 60 seconds and can be used exactly once. If an attacker intercepts the wrapping token before the MCP server unwraps it, Vault detects the double-unwrap and alerts.
Non-Vault alternatives
AWS Secrets Manager + Lambda extension: If your MCP server runs in Lambda, the AWS Parameters and Secrets Lambda Extension caches secrets in a local HTTP endpoint (http://localhost:2773/secretsmanager/get?secretId=...). You pay per API call, but the extension batches and caches calls within the Lambda execution environment.
Doppler: Managed secret injection that works as a CLI wrapper (doppler run -- node server.js) or as a Kubernetes operator. Easier to operate than Vault, fewer dynamic-secrets options.
Infisical: Open-source, self-hostable, with a TypeScript SDK that supports dynamic secrets for Postgres, MySQL, and Redis.
Audit logging and revocation
The operational value of Vault-style secrets injection isn't just rotation — it's the audit trail. Every credential issuance is logged with:
- The requesting identity (the AppRole, Kubernetes service account, or IAM role)
- The lease ID
- The issuance timestamp and TTL
- The requesting IP
When an incident occurs, you can query Vault's audit log for all leases issued to a compromised MCP server and revoke them in bulk:
# Revoke all leases issued to a specific prefix
vault lease revoke -prefix auth/kubernetes/login/mcp-server/
This is impossible with static environment variables. You can rotate the secret in your secret manager, but you cannot retroactively revoke leases that were never issued.
Checklist before deploying
Before wiring Vault to your MCP server, run through these:
- Least-privilege role: the Vault policy attached to your MCP server's AppRole or Kubernetes service account should grant
readon exactly the paths it needs, nothing else. - Short TTLs: start with 15 minutes for database credentials. Vault will warn you if a lease renewal fails.
- Audit log enabled: Vault ships with a file audit backend and a syslog backend. Enable at least one.
- Seal/unseal strategy: auto-unseal with AWS KMS or Azure Key Vault is strongly recommended for production. Manual unseal means your MCP servers can't start after a Vault restart until someone is at a terminal.
- Namespace isolation (Vault Enterprise): if multiple teams share a Vault cluster, use namespaces to scope policies and audit logs per team.
Dynamic secrets injection is table-stakes for any MCP server that touches sensitive data. The added complexity — a Vault agent sidecar, a policy, a Kubernetes service account binding — pays for itself the first time a credential leaks and you can prove it expired 15 minutes after issuance.
BitAtlas encrypts file contents client-side before they leave your browser, which means our servers never hold plaintext keys. For teams building on top of BitAtlas or integrating it into an MCP workflow, the credential surface is much smaller than with a traditional cloud storage backend. But the secrets injection patterns above apply to any MCP server that calls external services — database, API, or otherwise.