CIS Amazon Web Services Foundations Benchmark
Secure configuration guidelines for AWS cloud platform
v4.0.1 March 2025Overview
▶This CIS Benchmark provides prescriptive guidance for configuring security options for a subset of Amazon Web Services with an emphasis on foundational, testable, and architecture-agnostic settings.
| Section | Area | Focus |
|---|---|---|
| 1 | Identity and Access Management | Root account, IAM users/roles, MFA, access keys, credential reports |
| 2 | Storage | S3 bucket policies, encryption, public access, EBS volumes |
| 3 | Logging | CloudTrail, S3 access logging, CloudWatch Logs |
| 4 | Monitoring | CloudWatch metric filters and alarms for security events |
| 5 | Networking | VPC, security groups, NACLs, flow logs |
Profile Definitions
▶| Profile | Description | Intended Use |
|---|---|---|
| L1 | Level 1 | Practical security baseline for all AWS accounts. May cause minimal service impact. |
| L2 | Level 2 | Defense-in-depth for security-sensitive environments. May affect cost or complexity. |
1 — Identity and Access Management
▶AWS IAM configuration covering root account security, user policies, MFA, access keys, and credential hygiene.
1.1 Root Account
▶The root user has unrestricted access to all AWS resources. Enable hardware or virtual MFA for the root account to add a second layer of authentication.
Failure to mFA is Enabled for the Root User Account may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
aws iam get-account-summary --query 'SummaryMap.AccountMFAEnabled' # Should return 1
Sign in as root > IAM Dashboard > Security credentials > Multi-factor authentication (MFA) > Assign MFA device. Use a hardware/FIDO2 key for maximum security.
| Control | Description | IG |
|---|---|---|
| 6.5 | Require MFA for Administrative Access | IG1 |
The root account should not have access keys. Access keys provide programmatic access and, when associated with the root account, grant unrestricted access to all resources. Use IAM users or roles instead.
The absence of No Root User Access Keys leaves the AWS cloud platform without an important security control. Verifying its presence ensures the system meets the minimum security baseline required by the CIS benchmark.
aws iam get-account-summary --query 'SummaryMap.AccountAccessKeysPresent' # Should return 0
Delete all root access keys: IAM Console > Root security credentials > Access keys > Delete.
Use a hardware MFA device (FIDO2 security key or hardware TOTP token) for the root account rather than a virtual MFA application. Hardware tokens are resistant to phishing and SIM-swap attacks.
Failure to hardware MFA is Enabled for the Root Account may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
aws iam list-virtual-mfa-devices --query 'VirtualMFADevices[?SerialNumber==`arn:aws:iam::<account-id>:mfa/root-account-mfa-device`]' # Should return empty (no virtual MFA for root = hardware is used)
Replace virtual MFA with a FIDO2 hardware security key for the root account.
1.2 IAM Users & Policies
▶Set the IAM password policy minimum length to 14 characters or more. Longer passwords provide significantly more entropy and resistance to brute-force attacks.
Failure to iAM Password Policy Requires Minimum Length of 14 or Greater may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
aws iam get-account-password-policy --query 'PasswordPolicy.MinimumPasswordLength' # Should be 14 or greater
aws iam update-account-password-policy --minimum-password-length 14
Assign IAM policies to groups or roles, not directly to users. This simplifies access management and ensures consistent permission assignment.
Failure to iAM Policies Are Not Attached Directly to Users may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
aws iam list-users --query 'Users[*].UserName' --output text | while read user; do policies=$(aws iam list-attached-user-policies --user-name "$user" --query 'AttachedPolicies') inline=$(aws iam list-user-policies --user-name "$user" --query 'PolicyNames') echo "$user: attached=$policies inline=$inline" done # Users should have no directly attached policies
Create IAM groups, attach policies to groups, and add users to appropriate groups. Remove direct user policy attachments.
All IAM users with console (password) access must have MFA enabled. This protects against credential compromise from phishing, keylogging, or password reuse.
Failure to mFA is Enabled for All IAM Users with Console Access may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
aws iam generate-credential-report > /dev/null 2>&1
aws iam get-credential-report --query 'Content' --output text | base64 -d | \
awk -F, '$4=="true" && $8=="false" {print $1, "MFA_NOT_ENABLED"}'
# Should return no resultsEach user should enable MFA via IAM Console > Users > Security credentials > Assign MFA device.
1.3 Access Keys & Credentials
▶Access keys should be rotated at least every 90 days to reduce the window of exposure if keys are compromised. Consider using IAM roles with temporary credentials instead of long-lived access keys.
Failure to access Keys Are Rotated Every 90 Days or Less may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
aws iam generate-credential-report > /dev/null 2>&1
aws iam get-credential-report --query 'Content' --output text | base64 -d | \
awk -F, 'NR>1 && $9=="true" {split($10,d,"T"); print $1, d[1]}' | \
while read user date; do
age=$(( ($(date +%s) - $(date -d "$date" +%s)) / 86400 ))
[[ $age -gt 90 ]] && echo "$user: key1 age=${age}d EXCEEDS 90 DAYS"
done# Create new key, update applications, then deactivate old key aws iam create-access-key --user-name <user> # After updating apps: aws iam update-access-key --user-name <user> --access-key-id <old-key> --status Inactive aws iam delete-access-key --user-name <user> --access-key-id <old-key>
Disable IAM user credentials (password and access keys) that have not been used within 45 days. Unused credentials are an attack vector for credential compromise.
Failure to unused Credentials Are Disabled After 45 Days may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
aws iam get-credential-report --query 'Content' --output text | base64 -d | \
awk -F, 'NR>1 {print $1, "pwd_last_used:" $5, "key1_last_used:" $11, "key2_last_used:" $16}'# Disable console access aws iam delete-login-profile --user-name <user> # Deactivate unused access key aws iam update-access-key --user-name <user> --access-key-id <key> --status Inactive
2 — Storage
▶Configuration of AWS storage services including S3 bucket security and EBS volume encryption.
2.1 S3 Buckets
▶S3 bucket policies should not allow public access (Principal: "*") unless the bucket is explicitly intended for public content. Enable S3 Block Public Access at the account level.
Failure to s3 Bucket Policy Does Not Grant Public Access may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
# Check account-level block public access aws s3control get-public-access-block --account-id <account-id> # All four settings should be true: # BlockPublicAcls, IgnorePublicAcls, BlockPublicPolicy, RestrictPublicBuckets # Check per-bucket aws s3api get-bucket-policy-status --bucket <bucket> --query 'PolicyStatus.IsPublic' # Should be false
aws s3control put-public-access-block --account-id <account-id> \ --public-access-block-configuration \ BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
Enable default server-side encryption (SSE-S3 or SSE-KMS) for all S3 buckets. As of January 2023, S3 encrypts all new objects by default with SSE-S3, but custom KMS keys provide additional control.
Without S3 Bucket Server-Side Encryption enabled, the AWS cloud platform may lack critical protections against known attack vectors. Enabling this control mitigates risk and aligns the deployment with industry-accepted security baselines.
aws s3api get-bucket-encryption --bucket <bucket> \ --query 'ServerSideEncryptionConfiguration.Rules[0].ApplyServerSideEncryptionByDefault'
aws s3api put-bucket-encryption --bucket <bucket> \
--server-side-encryption-configuration '{
"Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "aws:kms"}}]
}'Enable versioning on S3 buckets to preserve, retrieve, and restore every version of every object. This protects against accidental deletion and enables recovery from application failures.
Without S3 Bucket Versioning enabled, the AWS cloud platform may lack critical protections against known attack vectors. Enabling this control mitigates risk and aligns the deployment with industry-accepted security baselines.
aws s3api get-bucket-versioning --bucket <bucket> --query 'Status' # Should be "Enabled"
aws s3api put-bucket-versioning --bucket <bucket> \ --versioning-configuration Status=Enabled
Enable MFA Delete on S3 buckets so that an additional MFA authentication is required to permanently delete object versions or change versioning state. This protects against accidental or malicious deletion.
Failure to mFA Delete is Enabled on S3 Buckets may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
aws s3api get-bucket-versioning --bucket <bucket> --query 'MFADelete' # Should be "Enabled"
MFA Delete can only be enabled by the root account. Use the root user with MFA to enable it:
aws s3api put-bucket-versioning --bucket <bucket> --versioning-configuration Status=Enabled,MFADelete=Enabled --mfa 'arn:aws:iam::<account>:mfa/root-account-mfa-device <mfa-code>'
2.2 EBS Volumes
▶Enable EBS encryption by default in each region to ensure all new EBS volumes and snapshots are encrypted automatically without requiring per-volume configuration.
Failure to eBS Volume Encryption is Enabled by Default may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
aws ec2 get-ebs-encryption-by-default --query 'EbsEncryptionByDefault' # Should be true (check each region)
aws ec2 enable-ebs-encryption-by-default # Optionally set a custom KMS key: aws ec2 modify-ebs-default-kms-key-id --kms-key-id <key-arn>
3 — Logging
▶AWS logging configuration covering CloudTrail, S3 access logs, and CloudWatch integration.
3.1 CloudTrail
▶CloudTrail should be configured as a multi-region trail to capture API calls across all AWS regions. This ensures visibility into activity in regions not normally used, which could indicate unauthorized access.
Failure to cloudTrail is Enabled in All Regions may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
aws cloudtrail describe-trails --query 'trailList[*].{Name:Name, IsMultiRegion:IsMultiRegionTrail, IsOrg:IsOrganizationTrail}'
# At least one trail should have IsMultiRegionTrail=true
aws cloudtrail get-trail-status --name <trail> --query 'IsLogging'
# Should be trueaws cloudtrail update-trail --name <trail> --is-multi-region-trail
Enable log file validation to ensure CloudTrail logs have not been tampered with after delivery. CloudTrail creates a hash (digest file) for every log file, enabling tamper detection.
Without CloudTrail Log File Validation enabled, the AWS cloud platform may lack critical protections against known attack vectors. Enabling this control mitigates risk and aligns the deployment with industry-accepted security baselines.
aws cloudtrail describe-trails --query 'trailList[*].{Name:Name, LogFileValidation:LogFileValidationEnabled}'
# Should be true for all trailsaws cloudtrail update-trail --name <trail> --enable-log-file-validation
Configure CloudTrail to encrypt log files using a customer-managed KMS key (CMK). This provides an additional layer of protection and allows key access auditing through CloudTrail and KMS logs.
Without encryption, CloudTrail Logs may transmit or store sensitive information in cleartext, exposing it to interception, eavesdropping, or tampering. Encryption is a critical control for data confidentiality and integrity.
aws cloudtrail describe-trails --query 'trailList[*].{Name:Name, KmsKeyId:KmsKeyId}'
# KmsKeyId should be setaws cloudtrail update-trail --name <trail> --kms-key-id <kms-key-arn>
3.2 CloudWatch
▶Configure CloudTrail to deliver log events to a CloudWatch Logs group. This enables real-time analysis, metric filters, and alarms based on API activity.
Failure to cloudTrail is Integrated with CloudWatch Logs may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
aws cloudtrail describe-trails --query 'trailList[*].{Name:Name, CWLogGroup:CloudWatchLogsLogGroupArn}'
# CloudWatchLogsLogGroupArn should be set
aws cloudtrail get-trail-status --name <trail> --query 'LatestCloudWatchLogsDeliveryTime'
# Should be recent (within last 24 hours)aws cloudtrail update-trail --name <trail> \ --cloud-watch-logs-log-group-arn <log-group-arn> \ --cloud-watch-logs-role-arn <role-arn>
4 — Monitoring
▶CloudWatch metric filters and alarms for detecting unauthorized or security-relevant API activity.
4.1 Metric Filters & Alarms
▶Create a metric filter and alarm to detect unauthorized API calls (AccessDenied, UnauthorizedAccess). This can indicate misconfigured applications, credential probing, or attempted lateral movement.
Failure to a Log Metric Filter and Alarm Exist for Unauthorized API Calls may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
aws logs describe-metric-filters --log-group-name <cloudtrail-log-group> \
--query 'metricFilters[?filterPattern!=`null`].{Name:filterName, Pattern:filterPattern}'
# Look for filter pattern containing:
# { ($.errorCode = "*UnauthorizedAccess*") || ($.errorCode = "AccessDenied*") }Create a metric filter on the CloudTrail log group with the appropriate filter pattern, then create a CloudWatch alarm that triggers an SNS notification.
Create a metric filter and alarm to detect AWS Console sign-in events without MFA. This detects potential credential compromise or MFA policy bypass.
Failure to a Log Metric Filter and Alarm Exist for Console Sign-In Without MFA may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
# Look for metric filter with pattern:
# { ($.eventName = "ConsoleLogin") && ($.additionalEventData.MFAUsed != "Yes") }
aws logs describe-metric-filters --log-group-name <cloudtrail-log-group>Create a metric filter, publish a custom metric, and create a CloudWatch alarm linked to an SNS topic for notification.
Create a metric filter and alarm for root account usage. The root account should rarely be used and any activity should be investigated promptly.
Failure to a Log Metric Filter and Alarm Exist for Root Account Usage may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
# Look for metric filter with pattern:
# { $.userIdentity.type = "Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != "AwsServiceEvent" }
aws logs describe-metric-filters --log-group-name <cloudtrail-log-group>Create the metric filter, custom metric, CloudWatch alarm, and SNS notification for root account usage.
Monitor IAM policy changes including CreatePolicy, DeletePolicy, AttachUserPolicy, DetachUserPolicy, AttachRolePolicy, DetachRolePolicy, AttachGroupPolicy, DetachGroupPolicy, and PutGroupPolicy events.
Failure to a Log Metric Filter and Alarm Exist for IAM Policy Changes may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
# Look for filter pattern covering IAM policy change events:
# { ($.eventName=CreatePolicy) || ($.eventName=DeletePolicy) ||
# ($.eventName=AttachRolePolicy) || ... }Create a comprehensive metric filter for all IAM policy change API calls and attach an alarm.
5 — Networking
▶VPC, security group, and network ACL configuration for secure AWS networking.
5.1 VPC & Security Groups
▶No security group should allow unrestricted SSH access (0.0.0.0/0 to port 22). Restrict SSH to known IP ranges or use AWS Systems Manager Session Manager for shell access.
Failure to no Security Group Allows Ingress from 0.0.0.0/0 to Port 22 may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
aws ec2 describe-security-groups --filters \
Name=ip-permission.from-port,Values=22 \
Name=ip-permission.to-port,Values=22 \
Name=ip-permission.cidr,Values='0.0.0.0/0' \
--query 'SecurityGroups[*].{ID:GroupId, Name:GroupName}'
# Should return emptyaws ec2 revoke-security-group-ingress --group-id <sg-id> \ --protocol tcp --port 22 --cidr 0.0.0.0/0
No security group should allow unrestricted RDP access (0.0.0.0/0 to port 3389). Restrict RDP to specific IP ranges or use Systems Manager for remote access.
Failure to no Security Group Allows Ingress from 0.0.0.0/0 to Port 3389 may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
aws ec2 describe-security-groups --filters \
Name=ip-permission.from-port,Values=3389 \
Name=ip-permission.to-port,Values=3389 \
Name=ip-permission.cidr,Values='0.0.0.0/0' \
--query 'SecurityGroups[*].{ID:GroupId, Name:GroupName}'
# Should return emptyaws ec2 revoke-security-group-ingress --group-id <sg-id> \ --protocol tcp --port 3389 --cidr 0.0.0.0/0
The default security group in every VPC should be configured to restrict all inbound and outbound traffic. Resources should use custom security groups with specific rules.
Failure to the Default Security Group Restricts All Traffic may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
aws ec2 describe-security-groups --filters Name=group-name,Values='default' \
--query 'SecurityGroups[*].{VPC:VpcId, Ingress:IpPermissions, Egress:IpPermissionsEgress}'
# Both Ingress and Egress should be empty listsRemove all inbound and outbound rules from the default security group in each VPC.
5.2 VPC Flow Logs
▶Enable VPC Flow Logs for all VPCs to capture IP traffic information. Flow logs are critical for network monitoring, troubleshooting, and security analysis.
Failure to vPC Flow Logging is Enabled in All VPCs may leave the AWS cloud platform vulnerable to attack or non-compliant with organizational security policies. This control helps establish a consistent, hardened configuration baseline.
# List all VPCs aws ec2 describe-vpcs --query 'Vpcs[*].VpcId' --output text | tr '\t' '\n' | while read vpc; do flowlogs=$(aws ec2 describe-flow-logs --filter "Name=resource-id,Values=$vpc" --query 'FlowLogs[0].FlowLogId' --output text) echo "$vpc: $flowlogs" done # Each VPC should have at least one flow log
aws ec2 create-flow-logs --resource-type VPC --resource-ids <vpc-id> \ --traffic-type ALL --log-destination-type cloud-watch-logs \ --log-group-name vpc-flow-logs --deliver-logs-permission-arn <role-arn>