AWS Security Best Practices: IAM, Encryption, Compliance (2026)
AWS provides hundreds of security services and controls, but the defaults are not always secure. This guide covers the foundational security practices every AWS account should have in place: IAM least privilege, MFA enforcement, encryption key management, threat detection, and compliance automation — with real policy JSON and CLI commands throughout.
1. IAM Least Privilege Principle
The principle of least privilege means every IAM identity (user, role, service) should have exactly the permissions it needs — nothing more. In practice this means:
- No
*actions unless absolutely necessary - Scope resources to specific ARNs, not
* - Use resource-based policies where supported (S3, KMS, Lambda, SQS) to add a second layer of defense
- Use permission boundaries to cap the maximum permissions a role can grant
A least-privilege IAM policy for a Lambda that reads from one DynamoDB table and writes to one S3 bucket:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadDynamoDB",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:BatchGetItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/orders"
},
{
"Sid": "WriteS3Reports",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:PutObjectAcl"
],
"Resource": "arn:aws:s3:::company-reports/lambda-output/*"
},
{
"Sid": "AllowCloudWatchLogs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/order-processor:*"
}
]
}
Use IAM Access Analyzer to find over-permissive roles. It also validates policies before you create them:
# Validate an IAM policy file before attaching it
aws accessanalyzer validate-policy \
--policy-type IDENTITY_POLICY \
--policy-document file://my-policy.json
# List findings for your account (external access to resources)
aws accessanalyzer list-findings \
--analyzer-name my-analyzer \
--filter '{"status":{"eq":["ACTIVE"]}}' \
--output table
# Generate a least-privilege policy from CloudTrail activity
aws accessanalyzer generate-policy \
--principal-arn arn:aws:iam::123456789012:role/MyRole \
--cloud-trail-details '{
"trails":[{"arn":"arn:aws:cloudtrail:us-east-1:123456789012:trail/my-trail","allRegions":true}],
"accessRole":"arn:aws:iam::123456789012:role/AccessAnalyzerRole",
"startTime":"2026-05-01T00:00:00Z",
"endTime":"2026-06-01T00:00:00Z"
}'
2. MFA Enforcement and SCPs in AWS Organizations
Service Control Policies (SCPs) are guardrails applied at the AWS Organizations level. They restrict what actions any IAM entity in an account can perform — even the root user. SCPs don't grant permissions; they define the maximum boundary.
SCP to deny all actions if MFA is not present (attach to all non-root OUs):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyWithoutMFA",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
},
"StringNotLike": {
"aws:PrincipalArn": [
"arn:aws:iam::*:role/AutomationRole",
"arn:aws:iam::*:role/TerraformRole"
]
}
}
}
]
}
SCP to prevent disabling security services (attach at root OU level):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PreventDisablingSecurityServices",
"Effect": "Deny",
"Action": [
"guardduty:DeleteDetector",
"guardduty:DisassociateFromMasterAccount",
"securityhub:DisableSecurityHub",
"config:DeleteConfigurationRecorder",
"config:StopConfigurationRecorder",
"cloudtrail:DeleteTrail",
"cloudtrail:StopLogging",
"macie2:DisableMacie"
],
"Resource": "*",
"Condition": {
"StringNotLike": {
"aws:PrincipalArn": "arn:aws:iam::*:role/SecurityAdminRole"
}
}
},
{
"Sid": "PreventLeavingOrganization",
"Effect": "Deny",
"Action": [
"organizations:LeaveOrganization"
],
"Resource": "*"
}
]
}
# Create and attach an SCP
aws organizations create-policy \
--type SERVICE_CONTROL_POLICY \
--name "require-mfa" \
--description "Deny API calls without MFA" \
--content file://mfa-scp.json
# Attach to an OU
aws organizations attach-policy \
--policy-id p-abc123 \
--target-id ou-root-abc123
3. KMS Key Management
AWS KMS has two key types that teams regularly confuse:
- AWS-managed keys (aws/s3, aws/rds, etc.): Free, auto-rotated, no key policy control, cannot be deleted or disabled. Fine for basic encryption requirements.
- Customer-managed keys (CMK): $1/month per key, full key policy control, manual or auto rotation, can be disabled/deleted, cross-account sharing possible. Required for compliance (HIPAA, PCI, FedRAMP).
Create a CMK with a resource-based key policy that grants access to specific roles only:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "KeyAdministration",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/KeyAdminRole"
},
"Action": [
"kms:Create*",
"kms:Describe*",
"kms:Enable*",
"kms:List*",
"kms:Put*",
"kms:Update*",
"kms:Revoke*",
"kms:Disable*",
"kms:Get*",
"kms:Delete*",
"kms:ScheduleKeyDeletion",
"kms:CancelKeyDeletion"
],
"Resource": "*"
},
{
"Sid": "KeyUsage",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::123456789012:role/AppRole",
"arn:aws:iam::123456789012:role/LambdaRole"
]
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
"Resource": "*"
},
{
"Sid": "DenyDeleteWithoutApproval",
"Effect": "Deny",
"Principal": {"AWS": "*"},
"Action": "kms:ScheduleKeyDeletion",
"Resource": "*",
"Condition": {
"NumericLessThan": {
"kms:ScheduleKeyDeletionPendingWindowInDays": "30"
}
}
}
]
}
# Create a CMK with automatic annual rotation
aws kms create-key \
--description "App encryption key" \
--key-usage ENCRYPT_DECRYPT \
--key-spec SYMMETRIC_DEFAULT \
--policy file://key-policy.json
# Enable automatic key rotation
aws kms enable-key-rotation --key-id alias/app-key
# Verify rotation status
aws kms get-key-rotation-status --key-id alias/app-key
4. Secrets Manager vs Parameter Store
Both services store secrets, but they serve different use cases. Choosing wrong costs money or capabilities:
| Feature | Secrets Manager | Parameter Store (Standard) | Parameter Store (Advanced) |
|---|---|---|---|
| Cost | $0.40/secret/month + $0.05 per 10k API calls | Free | $0.05/parameter/month |
| Max value size | 64KB | 4KB | 8KB |
| Auto-rotation | Yes (built-in for RDS, Redshift, DocumentDB) | No | No |
| Cross-account access | Yes (resource policy) | No (same account only) | No |
| KMS encryption | Always encrypted | SecureString type only | SecureString type only |
| Versioning | Yes (AWSCURRENT, AWSPREVIOUS) | Yes (history API) | Yes |
| Best for | Database passwords, API keys requiring rotation | Config values, non-sensitive app settings | Larger config, parameter policies/TTL |
Store and retrieve a database password with Secrets Manager:
# Store a secret
aws secretsmanager create-secret \
--name prod/myapp/db-password \
--description "Production database password" \
--secret-string '{"username":"admin","password":"MyS3cur3P@ss!"}'
# Enable automatic rotation for an RDS database
aws secretsmanager rotate-secret \
--secret-id prod/myapp/db-password \
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:SecretsManagerRDSRotation \
--rotation-rules AutomaticallyAfterDays=30
# Retrieve secret value (in application code pattern)
aws secretsmanager get-secret-value \
--secret-id prod/myapp/db-password \
--query SecretString \
--output text
In application code (Python), always use the Secrets Manager SDK with caching to avoid per-call charges:
import boto3
import json
from botocore.config import Config
from aws_secretsmanager_caching import SecretCache, SecretCacheConfig
# Use the caching client — fetches once, caches for 1 hour by default
cache_config = SecretCacheConfig(max_cache_size=10, secret_refresh_interval=3600)
cache = SecretCache(config=cache_config, client=boto3.client('secretsmanager'))
def get_db_credentials():
secret_string = cache.get_secret_string('prod/myapp/db-password')
return json.loads(secret_string)
5. AWS Security Hub
Security Hub aggregates findings from GuardDuty, Inspector, Macie, Firewall Manager, and third-party tools into a single dashboard with a security score against CIS AWS Foundations Benchmark, AWS Foundational Security Best Practices, and PCI DSS.
# Enable Security Hub (enable standards you want)
aws securityhub enable-security-hub \
--enable-default-standards \
--tags Environment=prod
# Get the overall security score
aws securityhub describe-hub \
--query 'AutoEnableControls'
# List high-severity findings
aws securityhub get-findings \
--filters '{
"SeverityLabel":[{"Value":"HIGH","Comparison":"EQUALS"},
{"Value":"CRITICAL","Comparison":"EQUALS"}],
"RecordState":[{"Value":"ACTIVE","Comparison":"EQUALS"}],
"WorkflowStatus":[{"Value":"NEW","Comparison":"EQUALS"}]
}' \
--sort-criteria '[{"Field":"LastObservedAt","SortOrder":"desc"}]' \
--max-results 20 \
--query 'Findings[*].[Title,Severity.Label,ProductName,UpdatedAt]' \
--output table
6. GuardDuty Threat Detection
GuardDuty uses ML models and threat intelligence to detect anomalous behavior across CloudTrail, VPC Flow Logs, and DNS logs. It costs roughly $1–$4 per account per month for most workloads — the ROI is extremely high.
Enable GuardDuty with S3 protection and EKS protection:
# Enable GuardDuty
aws guardduty create-detector \
--enable \
--finding-publishing-frequency FIFTEEN_MINUTES \
--data-sources '{
"S3Logs": {"Enable": true},
"Kubernetes": {
"AuditLogs": {"Enable": true}
},
"MalwareProtection": {
"ScanEc2InstanceWithFindings": {"EbsVolumes": true}
}
}'
# List high-severity findings
aws guardduty list-findings \
--detector-id $(aws guardduty list-detectors --query 'DetectorIds[0]' --output text) \
--finding-criteria '{
"Criterion": {
"severity": {"Gte": 7}
}
}'
# Get finding details
aws guardduty get-findings \
--detector-id $DETECTOR_ID \
--finding-ids $FINDING_ID \
--query 'Findings[0].{Type:Type,Severity:Severity,Description:Description,Region:Region}'
Key GuardDuty finding types to respond to immediately:
- UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B — console login from unusual location
- CryptoCurrency:EC2/BitcoinTool.B — EC2 instance mining cryptocurrency
- Backdoor:EC2/C&CActivity.B — EC2 communicating with known C2 servers
- Discovery:S3/MaliciousIPCaller — S3 enumeration from known-malicious IP
- PrivilegeEscalation:IAMUser/AdministrativePermissions — privilege escalation attempt
7. AWS Config Rules for Compliance
AWS Config tracks resource configuration changes over time and evaluates them against compliance rules. Run it in all regions, even ones you don't actively use — attackers often create resources in unused regions.
# Enable Config with all resource types
aws configservice put-configuration-recorder \
--configuration-recorder '{
"name": "default",
"roleARN": "arn:aws:iam::123456789012:role/AWSConfigRole",
"recordingGroup": {
"allSupported": true,
"includeGlobalResourceTypes": true
}
}'
# Add a managed rule: require MFA on root account
aws configservice put-config-rule \
--config-rule '{
"ConfigRuleName": "root-account-mfa-enabled",
"Source": {
"Owner": "AWS",
"SourceIdentifier": "ROOT_ACCOUNT_MFA_ENABLED"
}
}'
# Add a rule: require S3 bucket versioning
aws configservice put-config-rule \
--config-rule '{
"ConfigRuleName": "s3-bucket-versioning-enabled",
"Source": {
"Owner": "AWS",
"SourceIdentifier": "S3_BUCKET_VERSIONING_ENABLED"
}
}'
# Query compliance across all rules
aws configservice describe-compliance-by-config-rule \
--compliance-types NON_COMPLIANT \
--query 'ComplianceByConfigRules[*].[ConfigRuleName,Compliance.ComplianceType]' \
--output table
Use Config Conformance Packs to deploy a bundle of rules aligned to a standard:
# Deploy the CIS AWS Foundations Benchmark conformance pack
aws configservice put-conformance-pack \
--conformance-pack-name CISBenchmark \
--template-s3-uri s3://aws-conformance-packs-us-east-1/Operational-Best-Practices-for-CIS-AWS-v1.4-Level1.yaml \
--delivery-s3-bucket my-config-conformance-packs
8. CloudTrail Log Integrity
CloudTrail records every API call made in your account. Log integrity validation uses SHA-256 hashing and RSA signing to detect tampering — even by someone with S3 write access.
# Create a trail with log file validation and S3/CloudWatch delivery
aws cloudtrail create-trail \
--name org-audit-trail \
--s3-bucket-name my-cloudtrail-logs \
--is-multi-region-trail \
--enable-log-file-validation \
--cloud-watch-logs-log-group-arn arn:aws:logs:us-east-1:123456789012:log-group:CloudTrail \
--cloud-watch-logs-role-arn arn:aws:iam::123456789012:role/CloudTrailToCloudWatchLogs \
--kms-key-id alias/cloudtrail-key
# Enable management event logging
aws cloudtrail put-event-selectors \
--trail-name org-audit-trail \
--event-selectors '[{
"ReadWriteType": "All",
"IncludeManagementEvents": true,
"DataResources": [{
"Type": "AWS::S3::Object",
"Values": ["arn:aws:s3:::sensitive-bucket/"]
}]
}]'
# Validate log file integrity
aws cloudtrail validate-logs \
--trail-arn arn:aws:cloudtrail:us-east-1:123456789012:trail/org-audit-trail \
--start-time 2026-06-01T00:00:00Z \
--end-time 2026-06-05T00:00:00Z \
--verbose
9. VPC Security: Security Groups vs NACLs
Security Groups and Network ACLs (NACLs) both control network traffic in a VPC, but they work differently:
| Feature | Security Groups | Network ACLs |
|---|---|---|
| Level | Instance/ENI level | Subnet level |
| Stateful? | Yes — return traffic allowed automatically | No — must explicitly allow inbound AND outbound |
| Default behavior | Deny all inbound, allow all outbound | Allow all inbound and outbound |
| Rule evaluation | All rules evaluated, most permissive wins | Rules evaluated by number, first match wins |
| Can reference other SGs? | Yes — by security group ID | No — CIDR ranges only |
| Best for | Normal workload isolation | Blocking specific IP ranges (blocklists), defense in depth |
Create a security group that only allows HTTPS inbound and restricts outbound to specific services:
# Create security group for app servers
aws ec2 create-security-group \
--group-name app-servers \
--description "App server security group" \
--vpc-id vpc-0123456789abcdef0
# Allow HTTPS inbound from load balancer SG only
aws ec2 authorize-security-group-ingress \
--group-id sg-app \
--protocol tcp \
--port 8080 \
--source-group sg-alb # Reference ALB security group, not 0.0.0.0/0
# Restrict outbound: only allow HTTPS to internet and internal DB port
aws ec2 authorize-security-group-egress \
--group-id sg-app \
--ip-permissions '[
{"IpProtocol":"tcp","FromPort":443,"ToPort":443,"IpRanges":[{"CidrIp":"0.0.0.0/0"}]},
{"IpProtocol":"tcp","FromPort":5432,"ToPort":5432,"UserIdGroupPairs":[{"GroupId":"sg-db"}]}
]'
# Remove the default allow-all outbound rule
aws ec2 revoke-security-group-egress \
--group-id sg-app \
--protocol -1 \
--cidr 0.0.0.0/0
10. S3 Security: Block Public Access and Encryption
Two settings that should be on in every AWS account at the account level:
# Enable S3 Block Public Access at the account level
# This overrides any bucket-level settings — a single misconfig can't expose data
aws s3control put-public-access-block \
--account-id 123456789012 \
--public-access-block-configuration '{
"BlockPublicAcls": true,
"IgnorePublicAcls": true,
"BlockPublicPolicy": true,
"RestrictPublicBuckets": true
}'
# Require SSE-KMS on all new objects (bucket policy)
aws s3api put-bucket-policy \
--bucket my-sensitive-bucket \
--policy '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-sensitive-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
},
{
"Sid": "DenyHTTP",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-sensitive-bucket",
"arn:aws:s3:::my-sensitive-bucket/*"
],
"Condition": {
"Bool": {"aws:SecureTransport": "false"}
}
}
]
}'
FAQ: AWS Security
Q: What should I do immediately after creating a new AWS account?
In priority order: (1) Enable MFA on the root user and lock away the root credentials. (2) Create an admin IAM user or SSO role for daily use — never use root for anything. (3) Enable CloudTrail in all regions with log file validation. (4) Enable GuardDuty. (5) Enable Security Hub. (6) Set up billing alerts. (7) Enable S3 Block Public Access at the account level. (8) Enable Config in all regions. These 8 steps take about 30 minutes and provide the baseline security posture AWS recommends for all accounts.
Q: What is the difference between an IAM role and an IAM user?
IAM users have permanent credentials (access key ID + secret, or password). IAM roles have temporary credentials issued by STS, valid for 15 minutes to 12 hours. Roles are assumed by principals (EC2 instances, Lambda functions, ECS tasks, or humans via SSO). Best practice: use IAM roles for everything — EC2 instance profiles, Lambda execution roles, ECS task roles. Reserve IAM users only for legacy systems that cannot use roles. For human access, use AWS SSO (Identity Center) with a corporate identity provider rather than IAM users.
Q: How do I detect if my AWS account has been compromised?
GuardDuty findings are the first signal — enable it if you haven't. Look for: unusual API calls from unfamiliar IP addresses in CloudTrail, new IAM users or roles created (especially with AdministrativeAccess), EC2 instances launched in unusual regions, large S3 data transfers, changes to CloudTrail configuration or disabling of security services. Set up CloudWatch Alarms on CloudTrail metric filters for root account usage, console logins without MFA, and IAM policy changes. Respond to CRITICAL GuardDuty findings within 1 hour.
Q: Should I use Secrets Manager or environment variables for application secrets?
Never use plain environment variables for sensitive secrets — they appear in process listings, are logged by container orchestrators, and are visible in ECS task definitions without encryption. Use Secrets Manager or Parameter Store SecureString and fetch secrets at startup, caching them in memory. If you use Secrets Manager with ECS or EKS, you can inject secrets directly into container environment variables via the secrets field in the task definition — the secret is fetched at container start time and the value is never stored in the task definition.
Q: What is the confused deputy problem in AWS IAM and how do I prevent it?
The confused deputy problem occurs when a service (the "deputy") with high permissions is tricked by a different customer's resource into performing actions on your account. For example, if a service like CloudTrail uses a role ARN to write to your S3 bucket, an attacker who knows your account number could potentially trick CloudTrail into writing to their S3 bucket. Prevention: use the aws:SourceArn and aws:SourceAccount condition keys in trust policies. This ensures that only the specific resource in your account (not someone else's) can assume the role.