Tarek Cheikh
Founder & AWS Security Expert
AWS Key Management Service (KMS) is the cryptographic backbone of your entire AWS environment. Every encrypted S3 object, every encrypted EBS volume, every encrypted RDS database, every secret in Secrets Manager -- they all depend on KMS keys. If your KMS keys are misconfigured, an attacker can decrypt your most sensitive data, or worse, render it permanently inaccessible by scheduling key deletion.
In January 2025, the Codefinger ransomware campaign demonstrated a devastating new attack vector: threat actors used compromised AWS credentials to encrypt S3 objects with SSE-C (Server-Side Encryption with Customer-Provided Keys), then demanded ransom for the AES-256 keys required to decrypt the data. Because AWS does not store SSE-C key material, recovery without the attacker's key was impossible. Victims were threatened with permanent data deletion unless ransom was paid within seven days. This was the first known instance of attackers weaponizing AWS's native encryption infrastructure against its own customers -- and it underscored that encryption key management is not just a compliance checkbox, it is a critical defense against data loss, extortion, and regulatory penalties.
This guide covers 12 battle-tested KMS best practices, each with real AWS CLI commands, audit procedures, and the latest 2024-2026 updates from AWS.
KMS key policies are the primary access control mechanism for KMS keys. Unlike most AWS resources, KMS keys require explicit key policy permissions -- IAM policies alone are insufficient unless the key policy grants access to the account root principal. Overly permissive key policies are the most common KMS misconfiguration and are flagged by multiple Security Hub controls.
kms:ScheduleKeyDeletion and kms:DisableKey for all principals except a dedicated security break-glass role.kms:Encrypt from kms:Decrypt -- an application that writes encrypted data may not need to decrypt it."Principal": "*" in key policies without tight conditions. A wildcard principal makes the key accessible to any AWS account.{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowKeyAdministration",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:role/KMSAdminRole"},
"Action": [
"kms:Create*",
"kms:Describe*",
"kms:Enable*",
"kms:List*",
"kms:Put*",
"kms:Update*",
"kms:Revoke*",
"kms:Get*",
"kms:TagResource",
"kms:UntagResource"
],
"Resource": "*"
},
{
"Sid": "AllowKeyUsage",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:role/AppEncryptionRole"},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
"Resource": "*"
},
{
"Sid": "DenyDeletionExceptSecurityTeam",
"Effect": "Deny",
"Principal": "*",
"Action": [
"kms:ScheduleKeyDeletion",
"kms:DisableKey"
],
"Resource": "*",
"Condition": {
"ArnNotEquals": {
"aws:PrincipalArn": "arn:aws:iam::123456789012:role/SecurityBreakGlass"
}
}
}
]
}
# Audit: Get the key policy and review principal assignments
aws kms get-key-policy --key-id KEY_ID --policy-name default --output text
# Check Security Hub for KMS.1 and KMS.2 findings
aws securityhub get-findings --filters '{"GeneratorId":[{"Value":"aws-foundational-security-best-practices/v/1.0.0/KMS.1","Comparison":"PREFIX"}]}' --query "Findings[].{Title:Title,Severity:Severity.Label,Resource:Resources[0].Id}"
Security Hub: Controls [KMS.1] and [KMS.2] flag IAM policies (managed and inline) that allow decryption actions on all KMS keys using "Resource": "*". Scope decrypt permissions to specific key ARNs whenever possible.
The principal who administers a KMS key should never be the same principal who uses it for cryptographic operations. This separation prevents a single compromised role from both managing and exploiting encryption keys -- a compromised application role should not be able to delete the key that protects its own data.
# Create separate IAM roles for administration and usage
# KMS Admin Role - management plane only
aws iam create-role --role-name KMSAdminRole --assume-role-policy-document file://kms-admin-trust.json
aws iam put-role-policy --role-name KMSAdminRole --policy-name KMSAdminPolicy --policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"kms:CreateKey", "kms:DescribeKey", "kms:EnableKeyRotation",
"kms:GetKeyPolicy", "kms:GetKeyRotationStatus", "kms:ListKeys",
"kms:PutKeyPolicy", "kms:TagResource", "kms:UpdateKeyDescription"
],
"Resource": "*"
}]
}'
# KMS User Role - data plane only
aws iam create-role --role-name KMSUserRole --assume-role-policy-document file://kms-user-trust.json
aws iam put-role-policy --role-name KMSUserRole --policy-name KMSUserPolicy --policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"kms:Decrypt", "kms:Encrypt", "kms:GenerateDataKey",
"kms:GenerateDataKeyWithoutPlaintext", "kms:ReEncrypt*", "kms:DescribeKey"
],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/*"
}]
}'
# Audit: List all grants on a KMS key to verify separation
aws kms list-grants --key-id KEY_ID
Best practice: Use IAM roles rather than IAM users as key policy principals. Require MFA to assume the KMS admin role. Document the separation of duties matrix and review it quarterly.
Key rotation limits the blast radius of a key compromise. If key material is exposed, only data encrypted with the compromised material is at risk. AWS KMS automatic rotation creates new cryptographic material while retaining old material for decryption of previously encrypted data -- zero downtime, zero re-encryption required.
Since April 2024, AWS KMS supports custom rotation periods between 90 and 2,560 days (approximately 7 years). The default remains 365 days. You can also trigger on-demand rotation at any time and view the complete rotation history of any key.
# Enable automatic rotation with a 180-day period
aws kms enable-key-rotation --key-id arn:aws:kms:us-east-1:123456789012:key/KEY_ID --rotation-period-in-days 180
# Verify rotation status
aws kms get-key-rotation-status --key-id KEY_ID
# Trigger on-demand rotation (April 2024+)
aws kms rotate-key-on-demand --key-id KEY_ID
# View rotation history
aws kms list-key-rotations --key-id KEY_ID
# Audit: Find all customer-managed keys without rotation enabled
for key in $(aws kms list-keys --query "Keys[].KeyId" --output text); do
manager=$(aws kms describe-key --key-id "$key" --query "KeyMetadata.KeyManager" --output text 2>/dev/null)
if [ "$manager" = "CUSTOMER" ]; then
status=$(aws kms get-key-rotation-status --key-id "$key" --query "KeyRotationEnabled" --output text 2>/dev/null)
if [ "$status" = "False" ]; then
echo "ROTATION DISABLED: $key"
fi
fi
done
CIS Benchmark: Control 3.8 (ensure rotation for customer-managed KMS keys is enabled). Security Hub: Control [KMS.4] flags customer-managed keys without automatic rotation enabled.
Limitation: Automatic rotation is supported only for symmetric encryption keys with KMS-generated key material. Asymmetric keys, HMAC keys, and keys with imported key material must be rotated manually by creating a new key and updating aliases.
Pricing: The first and second rotations add $1/month (prorated hourly) to key cost. All rotations after the second are free, capping the per-key cost at $3/month regardless of rotation frequency.
Deleting a KMS key is irreversible. Once the waiting period expires and the key is deleted, all data encrypted with that key becomes permanently unrecoverable. This is the encryption equivalent of dropping a production database with no backup.
ScheduleKeyDeletion and DisableKey API calls via CloudTrail.kms:ScheduleKeyDeletion across your organization, except for a dedicated security OU.# Create a CloudWatch metric filter for key deletion events
aws logs put-metric-filter --log-group-name CloudTrail/DefaultLogGroup --filter-name KMSKeyDeletionAttempts --filter-pattern '{ ($.eventName = DisableKey) || ($.eventName = ScheduleKeyDeletion) }' --metric-transformations metricName=KMSKeyDeletionCount,metricNamespace=CloudTrailMetrics,metricValue=1
# Create alarm for deletion attempts
aws cloudwatch put-metric-alarm --alarm-name KMSKeyDeletionAlarm --metric-name KMSKeyDeletionCount --namespace CloudTrailMetrics --statistic Sum --period 300 --threshold 1 --comparison-operator GreaterThanOrEqualToThreshold --evaluation-periods 1 --alarm-actions arn:aws:sns:us-east-1:123456789012:SecurityAlerts
# Cancel a pending key deletion
aws kms cancel-key-deletion --key-id KEY_ID
aws kms enable-key --key-id KEY_ID
CIS Benchmark: Control 3.7 (ensure a log metric filter and alarm exist for disabling or scheduled deletion of customer-managed KMS keys). Security Hub: Control [KMS.3] verifies that KMS keys are not scheduled for unintentional deletion.
Encryption context is a set of key-value pairs included in cryptographic operations. It serves as Additional Authenticated Data (AAD) -- it is not encrypted, but it is cryptographically bound to the ciphertext. You must provide the same encryption context to decrypt that was used to encrypt. This provides both an integrity check and a powerful audit trail.
# Encrypt with encryption context
aws kms encrypt --key-id KEY_ID --plaintext fileb://secret.txt --encryption-context "Department=Finance,Project=Payroll" --output text --query CiphertextBlob > encrypted.b64
# Decrypt -- must provide the SAME encryption context
aws kms decrypt --ciphertext-blob fileb://encrypted.b64 --encryption-context "Department=Finance,Project=Payroll"
# Decryption FAILS if context does not match -- provides tamper detection
Require encryption context in key policies using the kms:EncryptionContext condition key:
{
"Sid": "RequireEncryptionContext",
"Effect": "Deny",
"Principal": "*",
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey*"
],
"Resource": "*",
"Condition": {
"Null": {
"kms:EncryptionContextKeys": "true"
}
}
}
Audit benefit: Encryption context values are logged in CloudTrail in plaintext. You can search CloudTrail for all decryption operations for a specific department, customer, or project without accessing the encrypted data itself. This is invaluable for forensic analysis and compliance audits.
Envelope encryption is the standard pattern for encrypting data larger than 4 KB (the KMS API limit). KMS generates a data key, you use the plaintext data key to encrypt your data locally, then store the encrypted data key alongside the ciphertext. Only the encrypted data key needs to be sent to KMS for decryption.
# Generate a data key
aws kms generate-data-key --key-id KEY_ID --key-spec AES_256 --encryption-context "TableName=Users"
# Returns:
# - Plaintext (base64): Use to encrypt data locally, then discard from memory
# - CiphertextBlob (base64): Store alongside the encrypted data
# To decrypt later:
# 1. Send CiphertextBlob to kms:Decrypt to get the plaintext data key
# 2. Use the plaintext data key to decrypt the data locally
# For the AWS Encryption SDK (recommended for production):
pip install aws-encryption-sdk
# Encrypt using the SDK CLI
aws-encryption-cli --encrypt --input secret-file.txt --wrapping-keys key=arn:aws:kms:us-east-1:123456789012:key/KEY-ID --encryption-context Environment=production --output secret-file.txt.encrypted --metadata-output metadata.json --commitment-policy require-encrypt-require-decrypt
Data key caching: The AWS Encryption SDK supports caching data keys for a configurable maximum number of messages, bytes, or time. Set conservative limits: maximum age of 5 minutes, maximum messages of 100, and maximum bytes of 10 GB. This balances performance with security by limiting the exposure window of any single data key.
The kms:ViaService condition key restricts a KMS key so it can only be used when the request originates from a specific AWS service. This prevents a compromised principal from using the key directly via the KMS API, even if they have kms:Decrypt permission.
{
"Sid": "RestrictKeyToS3Only",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:role/AppRole"},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:ViaService": "s3.us-east-1.amazonaws.com"
}
}
}
# Verify kms:ViaService conditions in existing key policies
aws kms get-key-policy --key-id KEY_ID --policy-name default --output text | python3 -c "import sys,json; p=json.load(sys.stdin); [print(s.get('Sid',''), s.get('Condition',{})) for s in p['Statement']]"
# Test: direct KMS API call should be DENIED when kms:ViaService is enforced
aws kms decrypt --ciphertext-blob fileb://encrypted.b64 --key-id KEY_ID
# Expected: AccessDeniedException
Codefinger defense: The January 2025 Codefinger ransomware attack used compromised credentials to call S3 PutObject with SSE-C keys directly. While kms:ViaService does not protect against SSE-C (which bypasses KMS entirely), restricting KMS key usage to specific services limits an attacker's ability to use your KMS keys for unauthorized encryption or decryption via direct API calls. Combine this with S3 bucket policies that deny SSE-C uploads to neutralize the Codefinger attack vector entirely.
Best practice: Create separate KMS keys for each service -- your S3 key, RDS key, Secrets Manager key, and EBS key should all be distinct with service-specific policies.
Cross-account KMS access is required in many architectures -- centralized encryption, shared data lakes, cross-account backups. There are two mechanisms: key policies and grants. Each has distinct security implications.
{
"Sid": "AllowCrossAccountUsage",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::987654321098:root"},
"Action": [
"kms:Decrypt",
"kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:PrincipalOrgID": "o-EXAMPLEORG",
"kms:ViaService": "s3.us-east-1.amazonaws.com"
}
}
}
# Create a grant for cross-account access (scoped and temporary)
aws kms create-grant --key-id KEY_ID --grantee-principal arn:aws:iam::987654321098:role/DataAnalystRole --operations Decrypt DescribeKey --constraints '{"EncryptionContextSubset":{"Department":"Analytics"}}'
# List all grants on a key to audit cross-account access
aws kms list-grants --key-id KEY_ID
# Retire a grant when no longer needed
aws kms retire-grant --grant-id GRANT_ID --key-id KEY_ID
Best practice: Always restrict cross-account access to principals within your AWS Organization using aws:PrincipalOrgID. The external account must also grant its principals permission to use the KMS key via an IAM policy -- both key policy and IAM policy authorization are required. Audit grants quarterly; unlike key policies, grants have no native expiration.
Multi-Region keys share the same key material and key ID across AWS Regions, allowing you to encrypt in one Region and decrypt in another without cross-Region API calls. They are essential for disaster recovery and globally distributed applications, but they expand the attack surface because a key compromise or policy misconfiguration affects all replica Regions simultaneously.
kms:ReplicateKey to authorized roles and approved Regions only.# Create a multi-Region primary key
aws kms create-key --multi-region --description "Multi-Region key for global application" --tags TagKey=Environment,TagValue=Production
# Create a replica in another Region
aws kms replicate-key --key-id mrk-EXAMPLE --replica-region eu-west-1 --description "Replica for EU disaster recovery"
# SCP to restrict replication to approved Regions
# Deny kms:ReplicateKey where kms:ReplicaRegion is NOT in the approved list
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "RestrictMRKReplication",
"Effect": "Deny",
"Action": "kms:ReplicateKey",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"kms:ReplicaRegion": ["us-east-1", "eu-west-1"]
}
}
}]
}
Best practice: Prefer single-Region keys unless you have a concrete requirement for cross-Region decryption. Multi-Region keys add operational complexity and expand the blast radius of a key compromise to all replica Regions. Ensure key policies are consistent across all replicas using AWS Config multi-Region aggregator.
Every KMS API call is logged in CloudTrail. This is your primary mechanism for detecting unauthorized key usage, key policy changes, and potential data exfiltration. CloudTrail logging for KMS management events is enabled by default, but you must verify that data events are also captured for sensitive keys.
Decrypt -- Unexpected decrypt calls may indicate data exfiltrationDisableKey, ScheduleKeyDeletion -- Potential destructive actionsPutKeyPolicy -- Key policy modifications may grant unauthorized accessCreateGrant -- New grants may provide backdoor accessGenerateDataKey -- High volume may indicate bulk encryption for ransomware# Search CloudTrail for key policy changes in the last 24 hours
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=PutKeyPolicy --start-time $(date -u -v-1d +"%Y-%m-%dT%H:%M:%SZ") --query "Events[].{Time:EventTime,User:Username,Key:Resources[0].ResourceName}"
# Search for key deletion attempts in the last 7 days
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=ScheduleKeyDeletion --start-time $(date -u -v-7d +"%Y-%m-%dT%H:%M:%SZ")
# CloudWatch Logs Insights query for anomalous decrypt volume:
# filter eventSource = "kms.amazonaws.com" and eventName = "Decrypt"
# | stats count(*) as decryptCount by bin(1h), userIdentity.arn
# | sort decryptCount desc
# Create metric filter for KMS access denied events (reconnaissance detection)
aws logs put-metric-filter --log-group-name CloudTrail/DefaultLogGroup --filter-name KMSAccessDenied --filter-pattern '{ ($.eventSource = kms.amazonaws.com) && ($.errorCode = AccessDenied*) }' --metric-transformations metricName=KMSAccessDeniedCount,metricNamespace=CloudTrailMetrics,metricValue=1
Incident context: During the January 2025 Codefinger ransomware campaign, defenders who had CloudTrail logging of S3 and KMS operations were able to reconstruct the attack timeline precisely, identifying the exact objects encrypted by attacker-controlled SSE-C keys. Organizations without CloudTrail logging had no visibility into which objects were affected.
CIS Benchmark: Control 3.7 (log metric filter and alarm for KMS key deletion/disabling). Control 3.8 (CloudTrail logs encrypted with KMS CMKs).
A KMS key with "Principal": "*" and no conditions is effectively public -- any AWS principal, including those in other accounts, can use it. This is analogous to a public S3 bucket, but for your encryption keys. It is one of the most severe KMS misconfigurations possible.
# Audit all key policies for wildcard principals
for key in $(aws kms list-keys --query "Keys[].KeyId" --output text); do
policy=$(aws kms get-key-policy --key-id "$key" --policy-name default --output text 2>/dev/null)
if echo "$policy" | grep -q '"Principal"[[:space:]]*:[[:space:]]*"*"'; then
echo "WARNING - Public principal on key: $key"
fi
if echo "$policy" | grep -q '"Principal"[[:space:]]*:[[:space:]]*{"AWS"[[:space:]]*:[[:space:]]*"*"}'; then
echo "WARNING - Public principal on key: $key"
fi
done
# Use IAM Access Analyzer to find externally shared KMS keys
aws accessanalyzer list-findings --analyzer-arn arn:aws:access-analyzer:us-east-1:123456789012:analyzer/my-analyzer --filter '{"resourceType":{"eq":["AWS::KMS::Key"]}}'
"Principal": "*" with specific account ARNs or role ARNs.aws:PrincipalOrgID, kms:CallerAccount, or kms:ViaService.Security Hub: Control [KMS.5] (where available) flags KMS keys with overly permissive policies. Access Analyzer findings for KMS keys are delivered within minutes of a policy change. Use Security Hub for centralized visibility.
Point-in-time audits are not enough. KMS configurations drift as teams create new keys, modify policies, and add grants. Continuous compliance monitoring catches misconfigurations within minutes, not months.
Enable the AWS Foundational Security Best Practices standard in Security Hub for automated KMS compliance checks:
# Enable the managed Config rule for KMS key rotation
aws configservice put-config-rule --config-rule '{
"ConfigRuleName": "cmk-backing-key-rotation-enabled",
"Source": {
"Owner": "AWS",
"SourceIdentifier": "CMK_BACKING_KEY_ROTATION_ENABLED"
}
}'
# Enable the managed Config rule for KMS key deletion
aws configservice put-config-rule --config-rule '{
"ConfigRuleName": "kms-cmk-not-scheduled-for-deletion",
"Source": {
"Owner": "AWS",
"SourceIdentifier": "KMS_CMK_NOT_SCHEDULED_FOR_DELETION"
}
}'
# Enable CIS Benchmark v3.0 in Security Hub
aws securityhub batch-enable-standards --standards-subscription-requests '[{
"StandardsArn": "arn:aws:securityhub:::standards/cis-aws-foundations-benchmark/v/3.0.0"
}]'
# Check compliance status
aws configservice describe-compliance-by-config-rule --config-rule-names cmk-backing-key-rotation-enabled kms-cmk-not-scheduled-for-deletion --query "ComplianceByConfigRules[*].{Rule:ConfigRuleName,Compliance:Compliance.ComplianceType}"
Use EventBridge rules to trigger Lambda functions that automatically remediate non-compliant KMS configurations:
# EventBridge rule to auto-enable rotation on newly created keys
aws events put-rule --name AutoEnableKMSRotation --event-pattern '{
"source": ["aws.kms"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventName": ["CreateKey"]
}
}'
Best practice: Integrate Security Hub findings into your ticketing system (Jira, ServiceNow) via EventBridge. Set SLA targets: Critical findings (public key policies) resolved within 4 hours, High findings (missing rotation) resolved within 24 hours.
| Misconfiguration | Risk | Detection |
|---|---|---|
| Wildcard principal in key policy | Any AWS account can use the key | IAM Access Analyzer, [KMS.5] |
| Key rotation disabled | Extended exposure if key material is compromised | AWS Config: CMK_BACKING_KEY_ROTATION_ENABLED, [KMS.4] |
Decryption on all keys (Resource: *) |
Lateral movement across all encrypted resources | [KMS.1], [KMS.2] |
| No encryption context required | No additional authentication, weak audit trail | Key policy review, CloudTrail analysis |
| SSE-C allowed on S3 buckets | Codefinger-style ransomware encryption attack | S3 bucket policy audit, S3 Security Card |
| Key deletion without CloudWatch alarm | Irreversible data loss goes undetected | CIS Control 3.7, CloudTrail metric filters |
| # | Practice | Priority |
|---|---|---|
| 1 | Enforce least-privilege key policies | Critical |
| 2 | Enforce separation of duties | Critical |
| 3 | Enable automatic key rotation | High |
| 4 | Protect against accidental key deletion | Critical |
| 5 | Use encryption context | High |
| 6 | Implement envelope encryption | High |
| 7 | Restrict keys with kms:ViaService | High |
| 8 | Secure cross-account access | High |
| 9 | Secure multi-Region keys | Medium |
| 10 | Monitor key usage with CloudTrail | Critical |
| 11 | Prevent overly permissive key policies | Critical |
| 12 | Implement continuous compliance | High |
This article is just the start. Get the full picture with our free whitepaper - 8 chapters covering IAM, S3, VPC, monitoring, agentic AI security, compliance, and a prioritized action plan with 50+ CLI commands.
Comprehensive guide to securing Amazon S3. Covers Block Public Access, encryption (SSE-KMS, SSE-C deprecation), Object Lock, MFA Delete, VPC endpoints, presigned URLs, and GuardDuty S3 Protection.
Comprehensive guide to securing Amazon RDS databases. Covers encryption at rest and in transit, private subnet deployment, IAM database authentication, RDS Proxy, audit logging, Secrets Manager rotation, and snapshot security.
Comprehensive guide to securing AWS Identity and Access Management. Covers MFA enforcement, least privilege, IAM Identity Center, SCPs, Access Analyzer, and credential management.