This page covers Amazon Web Services data protection across the storage and key-management surfaces that decide whether plaintext can leak from an AWS estate. Scope is the AWS commercial regions; AWS GovCloud (US) and the China regions inherit the same controls but expose a different key-material partition (KMS keys in aws-us-gov are not usable from commercial accounts and vice versa) — re-verify partition caveats before applying the IaC below outside commercial. CIS sub-IDs and NIST / ISO mappings reference the AWS commercial benchmark unless explicitly annotated as a post-v3.0.0 feature or a best-practice recommendation that the current benchmark has not yet codified. The cross-cutting principles — classification, encryption at rest, key management, and the separation of key custody from data custody — are explained in the General Data Protection page; this page maps them to AWS primitives. Encryption in transit is canonically treated in General Network — encryption in transit and AWS-specifically in AWS Network Hardening; this page does not re-author that material. Severity assignments follow the rubric documented in methodology; equivalence callouts at the bottom of each control point to the matching control on the Azure, GCP, and OCI sibling pages, and the compliance-frameworks page describes why each control row carries the same seven framework columns.
Two anti-conflation callouts up front, because both pairs get muddled in audit reports and IaC reviews and the distinction matters for control design. First: S3 Block Public Access (BPA) has three scopes that are routinely confused.Bucket-level BPA is set per bucket via aws s3api put-public-access-block and protects that bucket only. Account-level BPA is set per account via the entirely different aws s3control put-public-access-block command (note the service: s3control, not s3api) and is a region-independent invariant that overrides any bucket-level setting in the account. Organization-level enforcement is achieved by an SCP that denies s3:PutAccountPublicAccessBlock removal and s3:PutBucketPublicAccessBlock with permissive payloads. Control aws-data-01 targets the account-level invariant pinned by an SCP — the highest leverage of the three. Second: S3 BPA is distinct from VPC Block Public Access (see aws-net-04). Both carry the "Block Public Access" name and both are region-wide invariants, but they protect entirely different resource families — S3 BPA fences bucket ACLs and bucket policies against public grants; VPC BPA fences VPCs against Internet Gateway-mediated traffic. Both must be on. Conflating them in a control catalogue (a real failure mode this site has seen in customer benchmark scorecards) means one of the two ends up disabled by accident.
Order matters. Controls 01–04 are foundational data-at-rest invariants: lock public access to S3 at the account, force default KMS-CMK encryption on every new bucket, enable EBS encryption-by-default region-by-region, and encrypt every RDS instance with a customer-managed key. Controls 05–06 turn to the keys themselves: least-privilege key policies (so that having permission to use data and permission to decrypt data are separate grants) and automatic annual rotation. Control 07 closes the loop with Amazon Macie discovering PII that slipped into otherwise-controlled buckets. KMS keys used here are audited via CloudTrail data events — cross-reference the AWS-internal pointer to AWS Logging Hardening (control aws-log-02 CloudTrail S3 data events) once that page ships in Wave 2. Optional control aws-data-08 (S3 versioning + MFA-delete) is deferred per the 06-RESEARCH open question on regulated-bucket lifecycle rules.
aws-data-01-s3-bpa-account!CRITICALPREVENTIVE
Enable Amazon S3 Block Public Access at the account level in every AWS account, with all four sub-settings (BlockPublicAcls, IgnorePublicAcls, BlockPublicPolicy, RestrictPublicBuckets) set to true, and pin the setting with an organization-wide SCP that denies any principal in any member account from removing it. Account-level BPA is a region-independent invariant managed through the s3control API — distinct from per-bucket BPA which is managed through s3api — and overrides any bucket-level grant that would otherwise expose objects (AWS S3 User Guide — Block Public Access (accessed 2026-05)). It is the single highest-leverage S3 misconfiguration mitigation AWS exposes; CIS AWS Foundations v3.0.0 codifies it as control 2.1.4. The control deliberately uses the SCP pin (not just the account toggle) because every Capital-One-class incident in recent history followed the same template: a single bucket misconfigured by a single engineer in a single account where the platform team had not foreseen this account's emergence.
Remediation — AWS CLI
# Account-level BPA — note the service is s3control (NOT s3api) and the account-id is required.
aws s3control put-public-access-block \
--account-id 111122223333 \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
# Verify (note the same service distinction).
aws s3control get-public-access-block --account-id 111122223333
# Bucket-level BPA is a DIFFERENT command (kept for awareness; account-level is the canonical control).
# aws s3api put-public-access-block --bucket my-bucket --public-access-block-configuration ...
import * as cdk from 'aws-cdk-lib';
import { aws_s3 as s3 } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class AccountS3BpaStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new s3.CfnAccountPublicAccessBlock(this, 'AccountBpa', {
accountId: this.account,
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
});
}
}
Compliance mapping
CIS AWS Foundations v3.0.0
CIS Microsoft Azure Foundations v3.0.0
CIS GCP Foundation v4.0.0
CIS OCI Foundation v2.0.0
NIST SP 800-53 rev5
ISO/IEC 27001:2022
ISO/IEC 27017:2015
2.1.4
3.x (verify)
5.x (verify)
4.x (verify)
AC-3; AC-6; SC-7
A.5.10; A.8.3
CLD.9.5.1
Log signals
CloudTrail s3:PutAccountPublicAccessBlock events where the request reduces any of the four flags (BlockPublicAcls, IgnorePublicAcls, BlockPublicPolicy, RestrictPublicBuckets) from true to false — disables the account-level guard so per-bucket public access becomes possible again.
CloudTrail s3:DeletePublicAccessBlock at bucket scope — relies on the account-level block being false to actually allow public access, so this event paired with the account-level disable is the full regression chain.
CloudTrail s3:PutBucketPolicy where the new policy has a Principal:"*" statement without a Condition on aws:SourceVpce, aws:SourceIp, or aws:PrincipalOrgID — the per-bucket equivalent that takes effect once the BPA guard is off.
Query
fields @timestamp, eventName, requestParameters.publicAccessBlockConfiguration, requestParameters.bucketName, requestParameters.policy, userIdentity.arn
| filter eventSource = "s3.amazonaws.com" and eventName in ["PutAccountPublicAccessBlock","DeletePublicAccessBlock","PutPublicAccessBlock","PutBucketPolicy"]
| filter @message like /"BlockPublicAcls":false/ or @message like /"RestrictPublicBuckets":false/ or @message like /"Principal":"\*"/
| sort @timestamp desc
| limit 100
The CloudWatch Logs Insights query catches all three regression paths in a single sweep; the raw-message regex is necessary because publicAccessBlockConfiguration is a nested JSON object and field-typed filters miss the boolean values inside it on most CloudTrail event variants.
Alert threshold
Any PutAccountPublicAccessBlock with any flag set to false — page immediately; the account-level BPA is the fleet-protection backstop and disabling any flag widens every bucket's potential exposure.
DeletePublicAccessBlock at bucket scope on any non-data-distribution bucket — high-priority ticket; the bucket should either be tagged public-distribution=true with documented justification or remain locked down.
Bucket policy with Principal:"*" and no condition — page; the bucket is now reachable by any anonymous caller and the data-classification tag determines whether the response is a containment incident or a routine misconfiguration ticket.
Initial response
Restore the account-level BPA with aws s3control put-public-access-block --account-id {acct} --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true.
For the per-bucket case, re-apply the canonical bucket policy from IaC and immediately enable aws s3api put-public-access-block at bucket scope as defence-in-depth.
Pull S3 server-access logs and CloudTrail data-event logs for the exposed bucket during the gap window, enumerate every GetObject request from outside the org's VPC-endpoint set, and open an incident via general/ir.html for each accessed object — every such object is a candidate exfiltration target and data-owners must be notified per the bucket's classification tag.
Set the default encryption on every S3 bucket to aws:kms with a customer-managed CMK — not the AWS-managed aws/s3 key. Since January 2023, AWS encrypts every new S3 object with at least SSE-S3 by default, so the question is no longer "is encryption on" but "whose key is it". The AWS-managed aws/s3 key has a key policy AWS controls; rotating it, revoking it, or applying granular grants to it is impossible from the customer side. A customer-managed CMK (referenced by ARN in the bucket's encryption configuration) puts the customer in control of the key policy, the rotation schedule (covered in aws-data-06), the deletion window, and the audit trail (each Decrypt/GenerateDataKey call is logged in CloudTrail) (AWS S3 User Guide — default bucket encryption (accessed 2026-05)). CIS AWS Foundations v3.0.0 codifies SSE on every bucket as 2.1.1; the CMK requirement is the hardened reading of that control.
Remediation — AWS CLI
# Set default encryption on a bucket to SSE-KMS with a customer-managed CMK.
aws s3api put-bucket-encryption \
--bucket my-regulated-bucket \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:eu-west-1:111122223333:key/abcd1234-..."
},
"BucketKeyEnabled": true
}]
}'
# Audit: every bucket missing aws:kms with a CMK ARN.
aws s3api list-buckets --query 'Buckets[].Name' --output text | tr '\t' '\n' | while read b; do
aws s3api get-bucket-encryption --bucket "$b" 2>/dev/null \
| grep -q '"SSEAlgorithm": "aws:kms"' || echo "MISSING-CMK $b"
done
CloudTrail s3:PutBucketEncryption events where requestParameters.serverSideEncryptionConfiguration.rules[0].applyServerSideEncryptionByDefault.sSEAlgorithm shifts from aws:kms to AES256 — drops the CMK-controlled key surface and falls back to S3-managed keys, breaking the key-policy enforcement boundary that downstream IAM relies on.
PutBucketEncryption with aws:kms retained but kmsMasterKeyID changed to an alias not in the canonical CMK allow-list — silently re-keys the bucket against a key whose policy may be more permissive.
s3:DeleteBucketEncryption events — removes the default-encryption setting entirely; new object uploads still encrypt at rest (S3 enforces this) but with S3-managed keys, the same fallback as AES256.
Query
fields @timestamp, eventName, requestParameters.bucketName, requestParameters.serverSideEncryptionConfiguration, userIdentity.arn
| filter eventSource = "s3.amazonaws.com" and eventName in ["PutBucketEncryption","DeleteBucketEncryption"]
| filter eventName = "DeleteBucketEncryption" or @message like /"SSEAlgorithm":"AES256"/ or @message like /"SSEAlgorithm":null/
| sort @timestamp desc
| limit 100
The CloudWatch Logs Insights query addresses both regression variants; for the CMK-swap case, layer in a second query that joins against the canonical CMK alias list maintained as an SSM Parameter Store value, since the alias-to-key-id resolution can drift over time.
Alert threshold
Any PutBucketEncryption shifting to AES256 in production — page immediately; the CMK key-policy layer disappears at that point and the bucket's data is effectively re-keyed to S3-managed at the next write.
CMK alias swap to a key outside the allow-list — high-priority ticket within 30 minutes; the destination key's policy may grant decrypt to broader principals and the data's effective access boundary widens.
DeleteBucketEncryption — page; same severity as the AES256 shift and indicates an operator believes encryption is not required for the bucket, which is never the org-policy default.
Initial response
Restore the encryption configuration with aws s3api put-bucket-encryption --bucket {name} --server-side-encryption-configuration file://canonical-encryption.json referencing the canonical CMK alias; confirm via get-bucket-encryption read-back.
For objects written during the gap window, identify them via S3 Inventory or by listing objects with a LastModified timestamp inside the window; re-encrypt with the canonical CMK via in-place copy (aws s3 cp s3://{bucket}/{key} s3://{bucket}/{key} --sse aws:kms --sse-kms-key-id {alias}).
Open an incident per general/ir.html if the bucket holds regulated data (PII, PHI, financial); the data-owner must be notified of the encryption-key change because key-rotation audit trails and BYOK attestations cover the canonical CMK but not the S3-managed fallback.
Enable EBS encryption-by-default in every region of every AWS account, and reference a customer-managed CMK as the default EBS key (not the AWS-managed aws/ebs key) so that the key policy, rotation schedule, and audit trail sit with the customer. Once on, every new EBS volume — root and data — is created encrypted with the configured CMK with no additional caller action; every new EBS snapshot is encrypted; every AMI built from an encrypted volume inherits encryption (Amazon EC2 — EBS encryption (accessed 2026-05)). The setting is per-region (not per-account global) — a single API call disables the invariant for that region, so the enable loop must iterate every active region and the configuration must be re-checked when AWS launches a new region. CIS AWS Foundations v3.0.0 codifies the default-encryption requirement as 2.2.1.
Remediation — AWS CLI
# Enable encryption-by-default per region.
for region in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
aws ec2 enable-ebs-encryption-by-default --region "$region"
# Point the per-region default to a customer-managed CMK (not aws/ebs).
aws ec2 modify-ebs-default-kms-key-id \
--region "$region" \
--kms-key-id alias/ebs-default-cmk
done
# Verify per region.
aws ec2 get-ebs-encryption-by-default --region eu-west-1
aws ec2 get-ebs-default-kms-key-id --region eu-west-1
AWSTemplateFormatVersion: '2010-09-09'
Description: AWS Config managed rule asserting EBS encryption-by-default is enabled in the region.
Resources:
EbsEncryptionByDefaultRule:
Type: AWS::Config::ConfigRule
Properties:
ConfigRuleName: ec2-ebs-encryption-by-default
Source:
Owner: AWS
SourceIdentifier: EC2_EBS_ENCRYPTION_BY_DEFAULT
Compliance mapping
CIS AWS Foundations v3.0.0
CIS Microsoft Azure Foundations v3.0.0
CIS GCP Foundation v4.0.0
CIS OCI Foundation v2.0.0
NIST SP 800-53 rev5
ISO/IEC 27001:2022
ISO/IEC 27017:2015
2.2.1
7.x (verify)
4.x (verify)
5.x (verify)
SC-28; SC-13
A.8.24
n/a
Log signals
CloudTrail ec2:DisableEbsEncryptionByDefault events at region scope — turns off the default-on guard so subsequent CreateVolume calls land an unencrypted volume unless the caller explicitly opts in.
CloudTrail ec2:ModifyEbsDefaultKmsKeyId changing the default key to an alias outside the org's data-encryption CMK allow-list — silently re-keys all subsequently-created volumes against a different key whose policy may not be aligned with the data classification.
CloudTrail ec2:CreateVolume events where responseElements.volume.encrypted is false — direct evidence of an unencrypted volume creation, useful as backstop signal when the regional default was flipped but the modifier event was missed.
The CloudWatch Logs Insights query covers all three signals in one pass; for the CreateVolume backstop, layer in a daily inventory job that runs describe-volumes --filters Name=encrypted,Values=false and reports any volume that survived past its expected ephemeral lifetime.
Alert threshold
Any DisableEbsEncryptionByDefault in production — page immediately and re-enable within minutes; the regional default is the fleet-wide guard and every subsequent volume creation post-disable lands unencrypted.
Default-key change to an alias outside the data-encryption CMK allow-list — high-priority ticket within 30 minutes; the new default key's policy needs validation against the data-encryption posture.
CreateVolume with encrypted=false in production — page; the volume must be terminated or migrated to an encrypted snapshot before any sensitive data lands on it.
Initial response
Re-enable EBS encryption-by-default in every affected region with aws ec2 enable-ebs-encryption-by-default --region {region}; restore the canonical default-CMK with modify-ebs-default-kms-key-id.
Inventory unencrypted volumes via aws ec2 describe-volumes --filters Name=encrypted,Values=false Name=tag:env,Values=prod; for each volume, create an encrypted snapshot (aws ec2 create-snapshot with --encrypted), then create a replacement volume from the encrypted snapshot, swap the volume on the EC2 instance during the next maintenance window, and delete the unencrypted source after the swap commits.
Open an incident via general/ir.html if any unencrypted volume holds data classified above public; treat the data as having been at risk during the volume's unencrypted lifetime and notify data-owners per the classification.
Encrypt every Amazon RDS instance and Aurora cluster at rest with a customer-managed CMK; encrypt Performance Insights data with the same CMK family; enable deletion protection. RDS encryption at rest is set at instance creation and cannot be toggled on an existing unencrypted instance — the documented migration is snapshot → copy snapshot with encryption → restore from copy, which requires a maintenance window and capacity for two copies of the database during the migration (Amazon RDS — encryption at rest (accessed 2026-05)). This single property makes "encrypt RDS at creation" a foundational invariant: missing it at provisioning time creates technical debt that is materially expensive to repay. CIS AWS Foundations v3.0.0 codifies the requirement as 2.3.1.
AWSTemplateFormatVersion: '2010-09-09'
Description: AWS Config managed rule asserting all RDS DB instances are encrypted at rest.
Resources:
RdsStorageEncryptedRule:
Type: AWS::Config::ConfigRule
Properties:
ConfigRuleName: rds-storage-encrypted
Source:
Owner: AWS
SourceIdentifier: RDS_STORAGE_ENCRYPTED
Scope:
ComplianceResourceTypes:
- AWS::RDS::DBInstance
Compliance mapping
CIS AWS Foundations v3.0.0
CIS Microsoft Azure Foundations v3.0.0
CIS GCP Foundation v4.0.0
CIS OCI Foundation v2.0.0
NIST SP 800-53 rev5
ISO/IEC 27001:2022
ISO/IEC 27017:2015
2.3.1
4.x (verify)
6.x (verify)
5.x (verify)
SC-28; SC-13
A.8.24
n/a
Log signals
CloudTrail rds:CreateDBInstance or rds:CreateDBCluster events where requestParameters.storageEncrypted is false — RDS does not allow toggling encryption after creation, so the at-launch signal is the only practical detection point and there is no in-flight remediation other than snapshot-restore.
CloudTrail rds:ModifyDBInstance where requestParameters.publiclyAccessible flips to true on a previously-private instance — adjacent posture regression that frequently accompanies the encryption-at-rest gap and matters for the data-exposure response.
CloudTrail rds:RestoreDBInstanceFromDBSnapshot where the source snapshot was unencrypted and the restore creates a new unencrypted instance — alternative path to unencrypted RDS that bypasses the CreateDBInstance filter.
The CloudWatch Logs Insights query catches both regression paths; for the snapshot-restore case, layer in a second query on rds:CopyDBSnapshot events where the copy is created without kmsKeyId (the copy is the only step that can introduce encryption to a previously-unencrypted snapshot).
Alert threshold
Any CreateDBInstance or CreateDBCluster with storageEncrypted=false in production — page immediately; the instance cannot be encrypted after creation and the only path is snapshot-copy-encrypted + restore, which means downtime to fix.
ModifyDBInstance with publiclyAccessible=true on any RDS instance holding data classified above public — page; the data is now Internet-reachable subject only to the security-group and master-credential gate.
RestoreDBInstanceFromDBSnapshot from an unencrypted snapshot — high-priority ticket; the restore creates a new unencrypted instance and the operator should have used a copy-encrypted intermediate snapshot.
Initial response
For unencrypted production instances: create an encrypted snapshot via aws rds copy-db-snapshot --kms-key-id {alias}, restore the encrypted snapshot to a new instance, swap the application's connection string in a maintenance window, then delete the unencrypted source after verifying the encrypted replacement is fully caught up.
For publicly-accessible RDS, immediately modify with aws rds modify-db-instance --no-publicly-accessible --apply-immediately; the change is non-disruptive but DNS propagation can take minutes during which the previous public endpoint may still resolve.
Open an incident via general/ir.html; pull VPC Flow Logs for the RDS ENI during the public-access window and enumerate every inbound flow from outside the corporate egress CIDRs — each is a candidate adversary connection that may have authenticated against the master credentials.
Author each customer-managed KMS key policy to grant kms:Decrypt and kms:GenerateDataKey only to specific role ARNs that have a documented need, retain the root-account grant for break-glass administration (do not remove it — without it the key becomes unmanageable if the only admin role is lost), and set a deletion window of at least 30 days. The key policy is the single authority on who can use a CMK — IAM permissions are not sufficient on their own; both layers must agree (AWS KMS Developer Guide — key policies (accessed 2026-05)). The most common antipattern (audited against the Phase 5 IAM rubric of least privilege) is the open Principal: "*" with a kms:ViaService condition, which permits decrypt by any principal that can route a call via the named service. CRITICAL because a permissive key policy nullifies every other data-at-rest control on this page: encryption with a key that anyone can decrypt is theatre.
CloudTrail kms:PutKeyPolicy events on production CMKs where the new policy contains "Principal":"*" without a Condition on kms:CallerAccount, aws:PrincipalOrgID, or aws:SourceVpce — opens the key's Decrypt / GenerateDataKey surface to any principal in the universe that has IAM permission to call the operation.
CloudTrail kms:CreateGrant events where the grantee is an external account or a service-role outside the documented per-key grantee allow-list — grants are a sideband authorization channel that bypasses the key policy and frequently used for legitimate cross-account access but every new grant warrants review.
CloudTrail kms:ScheduleKeyDeletion with pendingWindowInDays below 30 — the 30-day floor is the AWS-recommended grace window and shorter values are a deliberate compress-the-blast-radius signal that pairs with attempts to destroy forensic key material.
Query
fields @timestamp, eventName, requestParameters.keyId, requestParameters.policy, requestParameters.granteePrincipal, requestParameters.pendingWindowInDays, userIdentity.arn
| filter eventSource = "kms.amazonaws.com" and eventName in ["PutKeyPolicy","CreateGrant","ScheduleKeyDeletion","UpdateAlias"]
| filter @message like /"Principal":"\*"/ or eventName != "PutKeyPolicy"
| sort @timestamp desc
| limit 100
The CloudWatch Logs Insights query catches the policy-widening idiom directly and surfaces grant / delete signals adjacently; the "Principal":"*" substring is the highest-signal positive because the org's authored key policies never use it without a tight condition.
Alert threshold
Any PutKeyPolicy introducing Principal:"*" without an organisation-condition — page immediately; the key's effective access surface widened to the entire AWS public surface at the moment of the change.
A grant to an external account ID outside the cross-account allow-list — page; the grant is a side-channel that the key policy does not gate and any unexpected external grantee is high-confidence credential abuse.
ScheduleKeyDeletion with pendingWindowInDays < 30 on a production CMK — page and immediately call cancel-key-deletion; the short window is the canonical sabotage indicator.
Initial response
Restore the key policy from IaC with aws kms put-key-policy --key-id {alias} --policy file://canonical-policy.json; for grants, revoke with aws kms revoke-grant referencing the grant-id from the CloudTrail event.
Inventory kms:Decrypt and kms:GenerateDataKey calls against the key during the permissive window via CloudTrail; any caller outside the documented allow-list is a candidate data-exposure event and the corresponding ciphertext should be considered potentially exfiltrated.
Open an incident via general/ir.html if any external principal called kms:Decrypt during the window; the data encrypted under the key needs to be considered as potentially read by that principal, and a key-rotation event (aws-data-06-kms-rotation) should be triggered to invalidate forward use of the now-suspect key material.
Enable annual automatic rotation on every customer-managed symmetric CMK in the account, and assert the property via AWS Config managed rule cmk-backing-key-rotation-enabled. Automatic rotation re-keys the underlying backing-key material on a 365-day schedule (custom periods 90–2560 days available since 2023); the key ARN and key ID do not change, so consumers do not need to be re-pointed, but the cryptographic material protecting new ciphertext is fresh (AWS KMS Developer Guide — rotating keys (accessed 2026-05)). The typology is DETECTIVE, not PREVENTIVE — following the rubric in methodology, rotation does not prevent a key compromise (an attacker who has stolen the current backing key still has it); it bounds the usable window of a compromised key, and surfaces stale-key configurations through the Config rule. This is the same PITFALL B-14 distinction made for IAM access-key rotation (aws-iam-05) on the Phase 5 page. Severity MEDIUM because rotation alone is a weak mitigation against a determined attacker; it shines when paired with the key-policy hygiene of aws-data-05 and the audit trail of CloudTrail KMS events.
Remediation — AWS CLI
# Enable rotation on an existing CMK.
aws kms enable-key-rotation --key-id alias/data-bucket-cmk
# Verify.
aws kms get-key-rotation-status --key-id alias/data-bucket-cmk \
--query 'KeyRotationEnabled' --output text
# Detective: AWS Config managed rule across the estate.
aws configservice put-config-rule --config-rule '{
"ConfigRuleName":"cmk-backing-key-rotation-enabled",
"Source":{"Owner":"AWS","SourceIdentifier":"CMK_BACKING_KEY_ROTATION_ENABLED"}}'
AWSTemplateFormatVersion: '2010-09-09'
Description: AWS Config managed rule asserting automatic key rotation is enabled on all customer-managed CMKs.
Resources:
KmsCmkRotationRule:
Type: AWS::Config::ConfigRule
Properties:
ConfigRuleName: cmk-backing-key-rotation-enabled
Source:
Owner: AWS
SourceIdentifier: CMK_BACKING_KEY_ROTATION_ENABLED
Compliance mapping
CIS AWS Foundations v3.0.0
CIS Microsoft Azure Foundations v3.0.0
CIS GCP Foundation v4.0.0
CIS OCI Foundation v2.0.0
NIST SP 800-53 rev5
ISO/IEC 27001:2022
ISO/IEC 27017:2015
2.1.x (verify)
n/a
n/a
n/a
SC-12; SC-12(1)
A.8.24
n/a
Log signals
CloudTrail kms:DisableKeyRotation events on production CMKs — stops the AWS-managed annual automatic rotation; future key material remains at the current epoch and CVE / cryptographic-best-practice rotations stop.
CloudTrail kms:GetKeyRotationStatus responses where keyRotationEnabled is false on a key whose alias indicates production usage — passive signal during the daily completeness sweep that finds keys whose rotation has been off for some time.
For customer-managed rotation (BYOK): absence of kms:ImportKeyMaterial events for more than the org's rotation cadence (typically 365 days) on a CMK with imported key material — rotation hasn't happened and the imported key material is past its policy-defined lifetime.
The CloudWatch Logs Insights query captures the active mutation events; pair with a daily Lambda that walks list-keys and calls get-key-rotation-status on each, emitting a CloudWatch metric for the count of keys with rotation disabled. The metric drives a threshold alarm at zero so any non-zero count fires immediately.
Alert threshold
Any DisableKeyRotation on a production CMK — page immediately; the rotation cadence is a compliance control and disabling it is a deliberate posture downgrade that breaks SOC 2 / PCI / HIPAA attestations.
Imported-key-material CMK without ImportKeyMaterial in the rotation window — high-priority ticket; the key material is past its policy lifetime and the BYOK rotation playbook needs to execute.
Daily-sweep count of production CMKs with rotation disabled — informational at first occurrence, page if the count stays non-zero for two consecutive days.
Initial response
Re-enable rotation with aws kms enable-key-rotation --key-id {alias}; for BYOK keys, schedule the next import via the BYOK ceremony playbook.
Trigger a one-shot rotation by waiting up to 24 hours for the AWS-managed rotation to fire, or for BYOK by importing fresh key material with a new ExpirationModel shorter than the policy lifetime; document the rotation completion in the key-management ledger.
Open an incident via general/ir.html if the rotation was disabled for more than the org's compliance cadence; the data encrypted under the long-running key material is potentially in scope for a re-encryption cycle, particularly for regulated workloads where the cryptoperiod is bounded by policy.
Enable Amazon Macie at the AWS Organization level with a delegated administrator account, run sensitive-data discovery jobs on buckets in scope for PII/PCI/PHI classifications, and route findings to AWS Security Hub for triage and ticketing (Amazon Macie User Guide (accessed 2026-05)). Macie's value proposition is bounded: it discovers known sensitive-data patterns (US/EU PII, credit-card numbers, AWS credential material, common health-record markers) in S3 — it is not a general DLP, it does not block writes, and it does not see across services other than S3. As a detective complement to the preventive controls in aws-data-01..04, Macie closes the loop on accidental writes of PII into buckets the platform team did not designate as sensitive. Severity MEDIUM because Macie alone does not change the security posture; it changes the visibility into the posture, and visibility is what makes the preventive controls auditable.
Remediation — AWS CLI
# Org-level: delegate Macie admin to the security account.
aws macie2 enable-organization-admin-account \
--admin-account-id 222233334444
# In the delegated admin: enable Macie and schedule a sensitive-data discovery job.
aws macie2 enable-macie --status ENABLED --finding-publishing-frequency FIFTEEN_MINUTES
aws macie2 create-classification-job \
--job-type SCHEDULED \
--schedule-frequency '{"DailySchedule":{}}' \
--name "pii-discovery-regulated-buckets" \
--s3-job-definition '{"bucketDefinitions":[
{"accountId":"111122223333","buckets":["my-regulated-bucket"]}
]}'
AWSTemplateFormatVersion: '2010-09-09'
Description: Enable Amazon Macie in the account with continuous classification on a one-hour interval.
Resources:
MacieSession:
Type: AWS::Macie::Session
Properties:
FindingPublishingFrequency: ONE_HOUR
Status: ENABLED
Compliance mapping
CIS AWS Foundations v3.0.0
CIS Microsoft Azure Foundations v3.0.0
CIS GCP Foundation v4.0.0
CIS OCI Foundation v2.0.0
NIST SP 800-53 rev5
ISO/IEC 27001:2022
ISO/IEC 27017:2015
(best-practices)
n/a
n/a
n/a
RA-5; SI-4
A.5.12; A.5.13
CLD.12.4.5
Log signals
CloudTrail macie2:DisableMacie events in any member account or the delegated-administrator — stops the sensitive-data discovery pipeline tenancy-wide if executed on the admin account.
CloudTrail macie2:DeleteClassificationJob or macie2:UpdateClassificationJob with jobStatus=CANCELLED — terminates an in-progress sensitive-data sweep before it completes, leaving the target bucket population unscanned.
Macie finding-export stream volume drops to zero on a 7-day-baseline basis while the bucket population has new objects according to S3 Inventory — passive signal that scans are not producing findings even when the job configuration looks correct.
The CloudWatch Logs Insights query covers the admin-side disable paths; the finding-volume signal is best served by a CloudWatch metric on the Macie findingsPublished metric with an absence-of-signal threshold at 10% of the trailing-7-day rolling mean.
Alert threshold
Any DisableMacie on the delegated-administrator account — page immediately; the org's sensitive-data discovery posture goes dark and findings stop fanning out.
DeleteClassificationJob on a job covering a production bucket — high-priority ticket within 30 minutes; the bucket's sensitive-data inventory is now stale and the operator must either re-run the job or document why coverage is no longer required.
Findings-published rate below 10% of 7-day baseline for two consecutive 1-hour windows — informational; promote to incident if the divergence persists for 6 hours, since the typical Macie cadence produces findings throughout the day across an active bucket population.
Initial response
Re-enable Macie at the delegated-administrator account with aws macie2 enable-macie --finding-publishing-frequency FIFTEEN_MINUTES and re-associate any disassociated member accounts via create-member + invite flow.
For terminated classification jobs, re-create with aws macie2 create-classification-job using the canonical job-template from IaC; trigger a one-shot sensitive-data scan against the affected buckets so the inventory closes the gap.
Open an incident via general/ir.html if any bucket contained newly-uploaded objects during the scan-gap window; the data may have been written or read without Macie-driven controls firing, and the data-owner must be notified per the bucket's classification tag for a manual sensitive-data review.