Guide · AWS KMS · Key Management

AWS KMS for MCP Servers — Key Creation, Types, Rotation, and Pricing

AWS Key Management Service (KMS) lets MCP server operators create, rotate, and control access to cryptographic keys without managing key material directly — at $1/month per customer managed key plus $0.03 per 10,000 API calls (first 20,000 free), it is far cheaper than managing your own HSM infrastructure, but the pricing model has a critical asymmetry: the key charge is flat regardless of usage, while the API call charge scales with how often your code calls Encrypt, Decrypt, and GenerateDataKey. For MCP server operators this means encrypting secrets at startup (amortized over a long process lifetime) is nearly free, but naively calling KMS on every database record read will generate significant charges. The solution is envelope encryption with data key caching — described in the envelope encryption guide.

TL;DR

Create a symmetric CMK (SYMMETRIC_DEFAULT), enable automatic annual rotation, and attach a key policy that includes the root principal statement. For encrypting data at scale use GenerateDataKey not Encrypt — see the envelope encryption guide. For key access control see the key policies and grants guide. For monitoring unusual Decrypt calls see the CloudTrail monitoring guide. For cross-region architectures see the multi-region keys guide.

Create a customer managed key

AWS KMS has three key ownership tiers: AWS owned keys (managed by AWS services, no charge, no visibility), AWS managed keys (one per service per account per region, visible in CloudTrail, no charge), and customer managed keys (CMKs) — keys you create, control, and pay $1/month for. For MCP server deployments use CMKs whenever you need cross-service access control, a custom key policy, manual rotation control, or cross-account sharing.

# Create a symmetric CMK for MCP server secrets (most common case)
aws kms create-key \
  --description "MCP server application secrets — production" \
  --key-usage ENCRYPT_DECRYPT \
  --key-spec SYMMETRIC_DEFAULT \
  --tags TagKey=Service,TagValue=mcp-server TagKey=Environment,TagValue=production \
  --policy '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "EnableIAMPolicies",
        "Effect": "Allow",
        "Principal": { "AWS": "arn:aws:iam::123456789012:root" },
        "Action": "kms:*",
        "Resource": "*"
      }
    ]
  }'
# Output includes:
#   KeyMetadata.KeyId: "abc12345-1234-1234-1234-abcdef123456"
#   KeyMetadata.Arn: "arn:aws:kms:us-east-1:123456789012:key/abc12345-1234-1234-1234-abcdef123456"
#   KeyMetadata.KeyState: "Enabled"
#   KeyMetadata.KeySpec: "SYMMETRIC_DEFAULT"
#   KeyMetadata.KeyUsage: "ENCRYPT_DECRYPT"

# Create an alias (human-readable name; ARN is canonical, alias is convenience)
aws kms create-alias \
  --alias-name alias/mcp-server-secrets-prod \
  --target-key-id abc12345-1234-1234-1234-abcdef123456

# Verify key state and metadata
aws kms describe-key \
  --key-id alias/mcp-server-secrets-prod \
  --query 'KeyMetadata.{State:KeyState,Spec:KeySpec,Rotation:KeyRotationEnabled,Created:CreationDate}'

The root principal statement (arn:aws:iam::ACCOUNT:root) in the key policy is not optional — omitting it creates a key that can only be managed through the key policy itself. If all key administrators are later removed from the policy, recovery requires an AWS support ticket. Always include the root principal statement to preserve IAM-policy-based access as a fallback.

# Enable automatic annual key rotation (strongly recommended for all CMKs)
aws kms enable-key-rotation \
  --key-id alias/mcp-server-secrets-prod

# Verify rotation is enabled
aws kms get-key-rotation-status \
  --key-id alias/mcp-server-secrets-prod
# Output: { "KeyRotationEnabled": true }

# List all CMKs in current region with their aliases
aws kms list-keys --query 'Keys[].KeyId' --output text | \
  xargs -I{} aws kms list-aliases --key-id {} \
  --query 'Aliases[].AliasName' --output text

Key types and when to use each

The --key-spec parameter at creation time is permanent — you cannot change it after the key is created. Choose based on what cryptographic operation your MCP server needs:

Key specAlgorithmOperationsUse case for MCP servers
SYMMETRIC_DEFAULTAES-256-GCMEncrypt, Decrypt, GenerateDataKey, ReEncrypt99% of cases: encrypting secrets, config files, database connection strings, API keys
RSA_2048 / RSA_3072 / RSA_4096RSA-OAEP or RSASSA-PKCS1-v1_5Encrypt/Decrypt or Sign/Verify (not both — specified at creation)JWT signing for MCP server OAuth tokens; public-key encryption when client doesn't have AWS credentials
ECC_NIST_P256 / ECC_NIST_P384 / ECC_NIST_P521ECDSASign, VerifySmaller signatures than RSA; webhook payload signing; TLS client certificate operations
HMAC_256 / HMAC_384 / HMAC_512HMAC-SHA2GenerateMac, VerifyMacAPI request signing without public-key overhead; session token MAC verification
SM2 (China regions only)SM2Encrypt, Sign, VerifyChina compliance requirements only
# Create an RSA_2048 key for signing (e.g. JWT tokens issued by MCP server)
# Note: SIGN_VERIFY and ENCRYPT_DECRYPT are mutually exclusive at creation
aws kms create-key \
  --description "MCP server JWT signing key" \
  --key-usage SIGN_VERIFY \
  --key-spec RSA_2048 \
  --tags TagKey=Service,TagValue=mcp-auth

# Sign a message with the RSA key
aws kms sign \
  --key-id alias/mcp-jwt-signing \
  --message $(echo -n "payload" | base64) \
  --message-type RAW \
  --signing-algorithm RSASSA_PKCS1_V1_5_SHA_256

# Verify without AWS credentials using the public key
aws kms get-public-key \
  --key-id alias/mcp-jwt-signing \
  --query 'PublicKey' --output text | base64 -d > public_key.der
openssl dgst -sha256 -verify public_key.der -signature signature.bin payload.txt

Asymmetric KMS keys have a public component retrievable without authentication via GetPublicKey. This is intentional — the private key material never leaves KMS, but the public key is designed to be distributed. For MCP server JWT verification, callers can use the public key locally without making KMS API calls (and without the associated charges).

Key aliases

Aliases are mutable references to key IDs. They let you update which key a service uses without changing application configuration — critical for zero-downtime key rotation when you want to pre-create a new key before the cutover.

# List all aliases and their target key IDs
aws kms list-aliases \
  --query 'Aliases[?starts_with(AliasName, `alias/mcp`)].{Alias:AliasName,KeyId:TargetKeyId}'

# Move an alias to a different key (instant, no downtime; new Encrypt calls use new key;
# Decrypt still works on existing ciphertext with old key until alias moves back or key deleted)
aws kms update-alias \
  --alias-name alias/mcp-server-secrets-prod \
  --target-key-id new-key-id-or-arn

# Aliases cannot be reused across accounts
# Aliases cannot be used as the target of another alias
# Alias names must start with "alias/" (except reserved "alias/aws/" prefix for AWS managed keys)

# Delete an alias (does NOT delete the underlying key)
aws kms delete-alias \
  --alias-name alias/mcp-server-secrets-old

References in application code should use alias ARNs (arn:aws:kms:us-east-1:123456789012:alias/mcp-server-secrets-prod) rather than key ARNs — the alias ARN is stable even when you rotate to a new key. Exception: IAM policy conditions using kms:RequestAlias do not work in all SDK versions; in those cases use the key ARN directly in the policy and reference the alias only in application code.

Automatic key rotation

When automatic rotation is enabled on a SYMMETRIC_DEFAULT CMK, KMS generates new key material every year. The old key material is retained permanently — KMS uses it to decrypt any ciphertext that was encrypted with that material. Applications do not need to re-encrypt existing data after rotation; the next Encrypt or GenerateDataKey call automatically uses the latest key material.

# Enable rotation on an existing key
aws kms enable-key-rotation --key-id alias/mcp-server-secrets-prod

# Rotation status
aws kms get-key-rotation-status --key-id alias/mcp-server-secrets-prod
# { "KeyRotationEnabled": true }

# Force immediate rotation (on-demand, available since KMS API update 2023)
aws kms rotate-key-on-demand --key-id alias/mcp-server-secrets-prod

# List all past key rotations for audit
aws kms list-key-rotations --key-id alias/mcp-server-secrets-prod \
  --query 'Rotations[].{Date:RotationDate,Type:RotationType}'
# Types: AWS_KMS (automatic annual), IMPORTED (you provided new material), ON_DEMAND

Automatic rotation does not apply to asymmetric keys or HMAC keys — only SYMMETRIC_DEFAULT. For asymmetric keys (RSA, ECC) you must manually create a new key, update the alias, and migrate callers — existing ciphertext cannot be decrypted with the new key material since the algorithm requires the exact same private key that was used to encrypt. For MCP servers using RSA keys for JWT signing, plan key rotation as a phased cutover: publish the new public key, allow both keys to be trusted simultaneously, retire the old key after all in-flight tokens expire.

Pricing model

ChargeRateFree tierMCP server impact
Customer managed key storage$1.00/month per keyNoneOne key per environment (dev/staging/prod) = $3/month base; asymmetric keys also $1/month
API requests (symmetric)$0.03 per 10,000 requests20,000 requests/monthStartup secrets load: ~10 calls/restart — negligible. Per-record encryption without caching: can reach millions/day
API requests (asymmetric)$0.15 per 10,000 requests (RSA), $0.15 per 10,000 (ECC Sign), $0.10 per 10,000 (ECC Verify)20,000 requests/month (shared)JWT signing: one Sign call per token issuance. Verification should use local public key (zero KMS calls)
API requests (HMAC)$0.015 per 10,000 requests20,000 requests/month (shared)Cheapest option for MAC-only use cases
CloudHSM key store$1.60/hour per HSM (separate from key storage)NoneOnly for FIPS 140-3 Level 3 compliance; not needed for typical MCP deployments
# Monitor your actual KMS API call volume to project costs
# Query CloudWatch KMS metrics (no extra cost to read metrics)
aws cloudwatch get-metric-statistics \
  --namespace AWS/KMS \
  --metric-name NumberOfRequestsForKeyMaterial \
  --dimensions Name=KeyId,Value=abc12345-1234-1234-1234-abcdef123456 \
  --start-time $(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
  --period 2592000 \
  --statistics Sum
# Sum / 10000 * 0.03 = estimated monthly cost for this key's API calls

Key deletion and scheduling

KMS enforces a mandatory 7–30 day waiting period before a key is deleted. During the waiting period the key is disabled and cannot process cryptographic operations — but the deletion can be cancelled. Once a key is deleted it is gone permanently; any ciphertext encrypted with it becomes permanently unrecoverable.

# Schedule deletion with minimum waiting period (7 days)
aws kms schedule-key-deletion \
  --key-id alias/mcp-server-secrets-old \
  --pending-window-in-days 7
# KeyState changes to "PendingDeletion" immediately
# Existing cryptographic operations fail with KMSInvalidStateException

# Cancel deletion during the waiting period
aws kms cancel-key-deletion \
  --key-id abc12345-1234-1234-1234-abcdef123456
# KeyState returns to "Disabled" — re-enable explicitly:
aws kms enable-key --key-id abc12345-1234-1234-1234-abcdef123456

# Before scheduling deletion: check for any ciphertext that uses this key
# Look for decrypt operations in CloudTrail data events over the past 90 days
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=Decrypt \
  --start-time $(date -u -d '90 days ago' +%Y-%m-%dT%H:%M:%SZ) \
  --query 'Events[?contains(Resources[].ARN, `abc12345`)].EventTime' | head -5

Failure modes reference

FailureSymptomFix
KMSInvalidStateException on DecryptAll decryption calls fail with "Key state: PendingDeletion" or "Disabled"Check key state with describe-key; cancel-key-deletion if pending, then enable-key; for disabled key just enable-key
AccessDeniedException on DecryptApplication role cannot call Decrypt even though the role ARN appears in key policyKey policy root principal statement missing — IAM policies have no effect; add root principal or explicitly add role ARN directly to key policy via PutKeyPolicy
Key locked out (no administrators can update policy)PutKeyPolicy fails for all principals; key policy has no root principalFile AWS support ticket with your account ID and key ARN; AWS can restore access but process takes days
InvalidKeyUsageException when calling SignCalling Sign on a SYMMETRIC_DEFAULT key failsSYMMETRIC_DEFAULT keys cannot sign; create a new key with key-usage SIGN_VERIFY and key-spec RSA_2048 or ECC_NIST_P256
InvalidCiphertextException on DecryptDecrypt call fails with InvalidCiphertextExceptionThree causes: wrong key used (key ARN embedded in ciphertext must match), ciphertext truncated/corrupted, or encryption context mismatch (passed different context to Decrypt than was used in Encrypt)
API call costs unexpectedly highMonthly KMS bill in hundreds of dollars from API callsApplication calling GenerateDataKey or Decrypt per database record; implement data key caching — see envelope encryption guide
Rotation fails on asymmetric keyenable-key-rotation returns UnsupportedOperationExceptionAutomatic rotation only works on SYMMETRIC_DEFAULT CMKs; asymmetric keys require manual rotation (create new key, update alias, migrate callers)