MCP server API key management: rotation, scoping, revocation, and storage
An API key is a promise: the holder can take actions as the key's identity, within the key's scope, until the key is revoked. MCP servers often hold several such promises simultaneously — keys for GitHub, Jira, Slack, internal APIs — and the failure to manage any one of them is a latent credential-theft waiting for a path traversal or log-leak to trigger it. This guide covers the four disciplines of healthy API key management for MCP servers: scope reduction, automated rotation, fast revocation, and appropriate storage backends.
Why API key hygiene is an MCP-specific concern
Traditional API services receive keys from callers and use them on behalf of callers. MCP servers invert this: the server holds keys for downstream systems and acts on behalf of an LLM agent. The MCP server is a credential broker. If the server is compromised — via SSRF, path traversal, prompt injection, or log leakage — every key it holds is compromised simultaneously.
SkillAudit grades API key handling under two axes: Credential Exposure (are keys protected from leaking in logs, errors, or responses?) and Security (are keys scoped and rotated to minimize the blast radius of a compromise?). A server that stores keys in environment variables, logs them on error, and has never rotated a key will typically land at grade C or D on these axes.
Step 1: scope keys to the minimum needed
Every key your MCP server holds should have the narrowest permission scope that still lets the server function. A key that can read issues does not need write access. A key that can list files does not need delete access. A key that operates on a single repository does not need organization-wide access.
Minimum-scope discipline serves two purposes. First, it limits what an attacker can do with a stolen key — a read-only token cannot exfiltrate data by creating API endpoints or uploading files. Second, it forces you to enumerate exactly what your server needs, which is useful during security reviews.
// Document your scope requirements in config validation
const ConfigSchema = z.object({
// GITHUB_TOKEN scope: read:repo — only what list_issues and get_file need
// Never request write:repo, delete_repo, or admin:org
githubToken: z.string().min(40, "GitHub token must be a classic or fine-grained PAT"),
// JIRA_API_TOKEN scope: project-scoped read only
jiraToken: z.string().min(20),
jiraProjectKey: z.string().regex(/^[A-Z]+$/, "Jira project key must be uppercase letters"),
});
// Validate that keys haven't been given broader scope than expected
// by checking the permissions endpoint at startup
async function validateTokenScope(token: string): Promise<void> {
const resp = await fetch("https://api.github.com/rate_limit", {
headers: { Authorization: `Bearer ${token}` },
});
const scopes = resp.headers.get("x-oauth-scopes") ?? "";
if (scopes.includes("write") || scopes.includes("admin")) {
throw new Error("GitHub token has write/admin scope — use a read-only token");
}
}
Step 2: automate rotation with short TTLs
A key that never rotates is a key that will eventually be compromised without you knowing. The longer a key lives, the higher the probability that it has appeared in a log file, been committed to a repository, or been transmitted over an insecure channel at some point.
The practical target for MCP server keys is a 90-day rotation window. For high-sensitivity keys (billing, admin, IAM), target 30 days. For keys exposed to agents (which may be adversarially prompted), consider dynamic short-lived credentials where possible.
# GitHub Actions workflow: rotate API keys every 90 days
name: Rotate MCP server API keys
on:
schedule:
- cron: "0 9 1 */3 *" # Every 3 months, 1st of month, 9am UTC
workflow_dispatch: # Allow manual trigger for emergency rotation
jobs:
rotate-keys:
runs-on: ubuntu-latest
permissions:
id-token: write # For OIDC token exchange with AWS
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT }}:role/key-rotator
aws-region: us-east-1
- name: Generate new GitHub fine-grained PAT
run: |
# Use GitHub API to create a new PAT with same scopes
NEW_TOKEN=$(gh api /user/installations --field expiration=90d | jq -r .token)
aws secretsmanager put-secret-value \
--secret-id "mcp-server/github-token" \
--secret-string "$NEW_TOKEN"
- name: Verify new token works
run: |
TOKEN=$(aws secretsmanager get-secret-value \
--secret-id "mcp-server/github-token" \
--query SecretString --output text)
curl -sf -H "Authorization: Bearer $TOKEN" \
https://api.github.com/rate_limit > /dev/null
- name: Revoke old token
run: |
# After successful rotation, invalidate the old token
gh api -X DELETE /user/installations/${{ vars.OLD_INSTALL_ID }}
Step 3: wire emergency revocation
Rotation on a schedule is the steady state. Revocation in an emergency — a key was leaked in a log, a repository was accidentally public, an incident is in progress — needs to happen in under 5 minutes. Prepare the runbook before you need it.
The emergency revocation workflow for an MCP server has four steps:
- Revoke at the source — invalidate the key with the issuing service (GitHub, Jira, internal IAM). This is the only action that actually stops the attacker.
- Rotate in secrets storage — generate a new key and update the secret in your manager (Secrets Manager, Vault, etc.) so your server can resume functioning.
- Redeploy the MCP server — force a restart so the new secret is loaded. If you use dynamic secrets (Vault agent sidecar, AWS Secrets Manager rotation Lambda), the server may pick up the new value automatically.
- Scan history — run trufflehog or git-secrets across your git history to confirm the key didn't appear in any commit.
# Emergency revocation script — run when a key is suspected compromised
#!/bin/bash
set -euo pipefail
SECRET_NAME="${1:?Usage: revoke.sh <secret-name>}"
echo "=== EMERGENCY REVOCATION: $SECRET_NAME ==="
echo "Step 1: Flagging current secret as compromised in Secrets Manager"
aws secretsmanager put-secret-value \
--secret-id "$SECRET_NAME" \
--secret-string "REVOKED_$(date -u +%Y%m%dT%H%M%SZ)" \
--version-stages AWSPENDING
echo "Step 2: Triggering rotation Lambda"
aws secretsmanager rotate-secret --secret-id "$SECRET_NAME"
echo "Step 3: Forcing ECS task replacement to pick up new secret"
aws ecs update-service \
--cluster mcp-prod \
--service mcp-server \
--force-new-deployment \
--query "service.deployments[0].id" --output text
echo "Done. Monitor CloudWatch for successful startup with new credentials."
Step 4: choose the right secrets storage backend
Environment variables are not secrets management. They're a convenient local development pattern that has been promoted to a production standard by mistake. The comparison below shows where each approach sits on the security vs. convenience tradeoff:
| Backend | Rotation | Audit log | SkillAudit grade |
|---|---|---|---|
| Hardcoded in source | Manual, code change required | None | CRITICAL |
| Environment variables | Manual, requires restart | None | MEDIUM risk |
| AWS Secrets Manager | Automated via Lambda rotator | CloudTrail | A-grade |
| HashiCorp Vault | Dynamic secrets (short TTL) | Vault audit log | A-grade |
| GCP Secret Manager | Automated via Cloud Functions | Cloud Audit Logs | A-grade |
The key differentiators for MCP servers are: automated rotation (so you don't depend on humans remembering to rotate), access audit logging (so you know if the secret was accessed after an incident), and OIDC/IAM-based access (so the MCP server doesn't need a static credential to access its own secrets).
SkillAudit findings for API key management
SkillAudit evaluates API key practices through static analysis of your source code and configuration. Common findings:
- CRITICAL: hardcoded API key detected (string matching known key formats)
- HIGH:
process.env.API_KEYused without startup validation — a missing key causes a runtime null-dereference rather than a clear startup error - HIGH: API key logged in error handler (
log.error({ config })where config includes key fields) - MEDIUM: no rotation documentation in README or SECURITY.md
- LOW: no token scope validation at startup
For related patterns, see MCP server secrets management and the security audit checklist.