Guide · AWS KMS · Key Policies

AWS KMS Key Policies and Grants for MCP Servers — Cross-Account, ViaService Conditions

AWS KMS access control differs from every other AWS service in one critical way: a key policy is always evaluated, and unlike S3 bucket policies or Lambda resource policies, a KMS key policy can completely lock out IAM — if you create a key without including the root principal statement ("Principal": {"AWS": "arn:aws:iam::ACCOUNT:root"}), no IAM policy in the account can grant access to that key, and recovery requires an AWS support ticket that may take days. For MCP server deployments this means key policy design is not optional boilerplate — the minimal safe key policy has exactly two statements: one that enables IAM-policy-based access for the entire account (the root principal), and one that grants specific cryptographic operations to the application role.

TL;DR

Always include the root principal statement. Separate key administrators (who manage key metadata and policy) from key users (who encrypt/decrypt data). Use grants for cross-account and cross-service access instead of cross-account key policies. Restrict key use to specific AWS services with kms:ViaService. See the KMS overview for key creation, the envelope encryption guide for how key policies interact with GenerateDataKey, the CloudTrail monitoring guide for auditing policy violations, and the multi-region guide for key policy replication.

Key policy structure

Every KMS CMK has exactly one key policy (the "default key policy" created by the console, or a custom policy you supply). The key policy is a JSON document up to 32KB and uses the same IAM policy syntax but is evaluated as a resource-based policy attached to the key itself. A key policy statement that denies an action overrides any IAM policy that allows it — the deny wins.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnableIAMPolicies",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::123456789012:root" },
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "AllowKeyAdministrators",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::123456789012:role/mcp-platform-admin",
          "arn:aws:iam::123456789012:user/security-team-lead"
        ]
      },
      "Action": [
        "kms:Create*", "kms:Describe*", "kms:Enable*", "kms:List*",
        "kms:Put*", "kms:Update*", "kms:Revoke*", "kms:Disable*",
        "kms:Get*", "kms:Delete*", "kms:TagResource", "kms:UntagResource",
        "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion",
        "kms:RotateKeyOnDemand"
      ],
      "Resource": "*"
    },
    {
      "Sid": "AllowMCPServerCryptographicUse",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/mcp-server-task-role"
      },
      "Action": [
        "kms:Encrypt", "kms:Decrypt", "kms:ReEncrypt*",
        "kms:GenerateDataKey*", "kms:DescribeKey"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "kms:CallerAccount": "123456789012"
        }
      }
    }
  ]
}

Key administrators can manage the key but should not have cryptographic use permissions (Encrypt, Decrypt) unless they are also key users — separation of duties prevents key administrators from accessing the data protected by the key. The application role (mcp-server-task-role) gets only the cryptographic operations it needs; it cannot modify the key policy, delete the key, or disable rotation.

# Retrieve and view the current key policy
aws kms get-key-policy \
  --key-id alias/mcp-server-secrets-prod \
  --policy-name default \
  --query 'Policy' --output text | python3 -m json.tool

# Update the key policy (replaces the entire policy — there is only one policy name: "default")
aws kms put-key-policy \
  --key-id alias/mcp-server-secrets-prod \
  --policy-name default \
  --policy file://key-policy.json

# Verify the new policy took effect
aws kms get-key-policy \
  --key-id alias/mcp-server-secrets-prod \
  --policy-name default \
  --query 'Policy' --output text

Grants for delegated and cross-account access

Grants are an alternative access control mechanism for KMS that does not require modifying the key policy. A grant gives a specific IAM principal permission to perform specific cryptographic operations on a specific key, optionally with constraints (like encryption context requirements). Grants can be created by key users (not just key administrators) and can be chained up to two levels deep — a grant can include permission to create a sub-grant.

# Create a grant allowing a Lambda function role to call Decrypt with a required context key
aws kms create-grant \
  --key-id alias/mcp-server-secrets-prod \
  --grantee-principal arn:aws:iam::123456789012:role/mcp-batch-lambda \
  --retiring-principal arn:aws:iam::123456789012:role/mcp-platform-admin \
  --operations Decrypt GenerateDataKey \
  --constraints EncryptionContextSubset={"service":"mcp-batch","env":"production"} \
  --name "mcp-batch-lambda-decrypt-grant"
# Output: { "GrantId": "grant-id-string", "GrantToken": "base64-token" }

# The GrantToken is important: use it in the first few seconds after grant creation
# to avoid eventual consistency delays (KMS grants replicate within ~5 seconds)
const { Plaintext } = await kms.send(new DecryptCommand({
  CiphertextBlob: encryptedKey,
  EncryptionContext: context,
  GrantTokens: [grantToken], // pass token if calling within seconds of CreateGrant
}));

# List all grants on a key
aws kms list-grants --key-id alias/mcp-server-secrets-prod \
  --query 'Grants[].{Name:Name,Grantee:GranteePrincipal,Ops:Operations}'

# Retire a grant (used by the retiring principal or the grantee)
aws kms retire-grant \
  --grant-token <grant-token>
# Or by grant ID (only works for retiring principal, not grantee):
aws kms revoke-grant \
  --key-id alias/mcp-server-secrets-prod \
  --grant-id <grant-id>

Grants are the recommended mechanism for AWS services like EKS, RDS, and Lambda to use your CMK — when you configure a service to use a CMK (e.g., encrypting an EKS secret store), AWS creates a grant on your behalf. These service grants appear in list-grants output with GranteePrincipal pointing to an AWS service principal. Do not revoke service grants created by AWS — doing so will break the service's ability to decrypt data it manages.

Cross-account key sharing

Two approaches for cross-account access: (1) key policy + IAM policy in the consuming account, or (2) grant to the consuming account role. The key policy approach requires a change in the key owner's account for every new consumer; the grant approach lets existing key users create grants without touching the key policy.

# Approach 1: Key policy + IAM — key owner account (123456789012)
# Add the consuming role/account to the key policy
{
  "Sid": "AllowCrossAccountDecrypt",
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::999999999999:role/consuming-service-role"
  },
  "Action": ["kms:Decrypt", "kms:DescribeKey"],
  "Resource": "*"
}

# Then in the consuming account (999999999999), also need an IAM policy on the role:
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["kms:Decrypt", "kms:DescribeKey"],
    "Resource": "arn:aws:kms:us-east-1:123456789012:key/abc12345-..."
  }]
}

# Approach 2: Grant — key user in owner account creates a grant for consumer role
aws kms create-grant \
  --key-id alias/mcp-server-secrets-prod \
  --grantee-principal arn:aws:iam::999999999999:role/consuming-service-role \
  --operations Decrypt GenerateDataKey DescribeKey \
  --name "cross-account-consuming-service"
# No IAM policy needed in consuming account — grant is sufficient

# In the consuming account, the role calls KMS directly with key ARN
aws kms decrypt \
  --ciphertext-blob fileb://encrypted-data-key.bin \
  --key-id arn:aws:kms:us-east-1:123456789012:key/abc12345-... \
  --region us-east-1

When a cross-account role calls Decrypt, the call arrives from the consumer's account but is authorized by the key policy/grant in the owner's account. The CloudTrail entry for the Decrypt event lands in the key owner's account CloudTrail — not the consumer's. If you need audit visibility in both accounts, configure cross-account CloudTrail delivery or use an EventBridge rule in the key owner's account to forward KMS events.

ViaService and other key policy conditions

KMS supports a rich set of condition keys that restrict when and how a key can be used. The most useful for MCP server deployments are kms:ViaService (restrict key to specific AWS services), kms:EncryptionContextKeys (require specific context keys), and kms:RequestAlias (restrict callers to using a specific alias name).

# Restrict Decrypt to only work when called through Secrets Manager
# (not directly by the application role)
{
  "Sid": "RestrictToSecretsManager",
  "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::123456789012:role/mcp-server-task-role" },
  "Action": ["kms:Decrypt", "kms:GenerateDataKey"],
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "kms:ViaService": "secretsmanager.us-east-1.amazonaws.com"
    }
  }
}

# Require that all callers pass an "env" key in the encryption context
{
  "Sid": "RequireEnvironmentContext",
  "Effect": "Deny",
  "Principal": "*",
  "Action": ["kms:Decrypt", "kms:GenerateDataKey*", "kms:Encrypt"],
  "Resource": "*",
  "Condition": {
    "Null": {
      "kms:EncryptionContextKeys": "true"
    }
  }
}

# Restrict callers to use only the alias (not the raw key ARN) — helps prevent
# direct key ARN references that bypass alias-based rotation workflows
{
  "Sid": "RequireAlias",
  "Effect": "Deny",
  "Principal": "*",
  "Action": ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey"],
  "Resource": "*",
  "Condition": {
    "Null": {
      "kms:RequestAlias": "true"
    }
  }
}

# Prevent key from being used outside specific regions (useful for data sovereignty)
{
  "Sid": "RestrictToEURegions",
  "Effect": "Deny",
  "Principal": "*",
  "Action": "kms:*",
  "Resource": "*",
  "Condition": {
    "StringNotLike": {
      "aws:RequestedRegion": ["eu-west-1", "eu-central-1", "eu-west-2"]
    }
  }
}

The kms:ViaService condition is particularly useful for database encryption: if you use a CMK to encrypt RDS storage, you can write a key policy that only allows Decrypt when the request comes through rds.us-east-1.amazonaws.com. This prevents your application role from directly decrypting the RDS storage key even if it is compromised — it can only read data through the RDS API, not extract raw key material.

Failure modes reference

FailureSymptomFix
Key policy lockoutPutKeyPolicy fails for all principals; no one can update the policyAWS support ticket required; attach evidence of account ownership; process takes 1–5 business days; avoid by always including the root principal statement
Cross-account Decrypt fails with AccessDeniedConsumer role gets AccessDeniedException even though key policy names itBoth the key policy (in owner account) AND an IAM policy on the consumer role (in consumer account) are required for cross-account access; approach 2 (grant) bypasses this requirement
Grant not immediately effectiveApplication fails with AccessDeniedException seconds after CreateGrantKMS grants replicate with up to 5 seconds of eventual consistency; pass the GrantToken from CreateGrant response in the first few calls; avoid by creating grants in advance (not on the hot path)
Service grant revoked accidentallyEKS or RDS cannot access data after revoking a grant that turned out to be a service grantCheck the GranteePrincipal in list-grants — service grants have principal like "rds.us-east-1.amazonaws.com"; do not revoke these; restore by disabling and re-enabling the service's CMK integration
kms:ViaService blocks legitimate service callSecrets Manager or RDS fails to decrypt with AccessDeniedException mentioning kms:ViaServiceViaService value is region-specific: "secretsmanager.us-east-1.amazonaws.com" not just "secretsmanager.amazonaws.com"; verify the exact service endpoint format in the condition
Key policy change locks out applicationApplication gets AccessDeniedException after policy update, even though role is still in policyIAM policy evaluation: both key policy and IAM policy must Allow; check that the application role's attached IAM policy still allows kms:Decrypt on the key ARN (not just alias)