Security·Secrets Management·API Keys

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

For related patterns, see MCP server secrets management and the security audit checklist.