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 spec | Algorithm | Operations | Use case for MCP servers |
|---|---|---|---|
| SYMMETRIC_DEFAULT | AES-256-GCM | Encrypt, Decrypt, GenerateDataKey, ReEncrypt | 99% of cases: encrypting secrets, config files, database connection strings, API keys |
| RSA_2048 / RSA_3072 / RSA_4096 | RSA-OAEP or RSASSA-PKCS1-v1_5 | Encrypt/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_P521 | ECDSA | Sign, Verify | Smaller signatures than RSA; webhook payload signing; TLS client certificate operations |
| HMAC_256 / HMAC_384 / HMAC_512 | HMAC-SHA2 | GenerateMac, VerifyMac | API request signing without public-key overhead; session token MAC verification |
| SM2 (China regions only) | SM2 | Encrypt, Sign, Verify | China 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
| Charge | Rate | Free tier | MCP server impact |
|---|---|---|---|
| Customer managed key storage | $1.00/month per key | None | One key per environment (dev/staging/prod) = $3/month base; asymmetric keys also $1/month |
| API requests (symmetric) | $0.03 per 10,000 requests | 20,000 requests/month | Startup 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 requests | 20,000 requests/month (shared) | Cheapest option for MAC-only use cases |
| CloudHSM key store | $1.60/hour per HSM (separate from key storage) | None | Only 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
| Failure | Symptom | Fix |
|---|---|---|
| KMSInvalidStateException on Decrypt | All 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 Decrypt | Application role cannot call Decrypt even though the role ARN appears in key policy | Key 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 principal | File AWS support ticket with your account ID and key ARN; AWS can restore access but process takes days |
| InvalidKeyUsageException when calling Sign | Calling Sign on a SYMMETRIC_DEFAULT key fails | SYMMETRIC_DEFAULT keys cannot sign; create a new key with key-usage SIGN_VERIFY and key-spec RSA_2048 or ECC_NIST_P256 |
| InvalidCiphertextException on Decrypt | Decrypt call fails with InvalidCiphertextException | Three 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 high | Monthly KMS bill in hundreds of dollars from API calls | Application calling GenerateDataKey or Decrypt per database record; implement data key caching — see envelope encryption guide |
| Rotation fails on asymmetric key | enable-key-rotation returns UnsupportedOperationException | Automatic rotation only works on SYMMETRIC_DEFAULT CMKs; asymmetric keys require manual rotation (create new key, update alias, migrate callers) |