Cloud Security
Securing AWS infrastructure: IAM least privilege, preventive controls (SCPs, resource policies), detective controls (GuardDuty, CloudTrail, Security Hub), and network security (WAF, Security Groups, N...
Securing AWS infrastructure: IAM least privilege, preventive controls (SCPs, resource policies), detective controls (GuardDuty, CloudTrail, Security Hub), and network security (WAF, Security Groups, NACLs).
IAM Best Practices
Never use root account — lock it, enable MFA, no access keys
Use IAM roles for everything that runs (EC2, Lambda, ECS tasks, GitHub Actions OIDC)
Least privilege — start with deny all, add only what's needed
Review unused permissions with IAM Access Analyzer
// Least-privilege Lambda execution role example
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadOrdersTable",
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:Query"],
"Resource": "arn:aws:dynamodb:eu-west-1:123456789:table/Orders"
},
{
"Sid": "ReadSecretsManagerSecret",
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": "arn:aws:secretsmanager:eu-west-1:123456789:secret:myapp/db-*"
},
{
"Sid": "BasicLambdaLogging",
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": "arn:aws:logs:*:*:*"
}
]
}Service Control Policies (SCPs)
SCPs are guardrails on AWS Organizations. They restrict what member accounts can do even if their IAM policies allow it. They do not grant permissions.
// Deny leaving the organisation
{
"Sid": "DenyLeaveOrg",
"Effect": "Deny",
"Action": "organizations:LeaveOrganization",
"Resource": "*"
}
// Deny creating resources outside approved regions
{
"Sid": "DenyNonEURegions",
"Effect": "Deny",
"NotAction": [
"iam:*",
"organizations:*",
"support:*",
"cloudfront:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["eu-west-1", "eu-west-2", "eu-central-1"]
}
}
}
// Require encryption on S3 buckets
{
"Sid": "DenyUnencryptedS3Puts",
"Effect": "Deny",
"Action": "s3:PutObject",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": ["aws:kms", "AES256"]
}
}
}GuardDuty
Threat detection service. Analyses CloudTrail, VPC Flow Logs, DNS logs, S3 data events, EKS audit logs. No agents. ML-based anomaly detection.
# Enable GuardDuty
aws guardduty create-detector --enable --finding-publishing-frequency SIX_HOURS
# 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}
}
}'Key finding types: UnauthorizedAccess:IAMUser/ConsoleLogin, CryptoCurrency:EC2/BitcoinTool, Recon:IAMUser/UserPermissions, Trojan:EC2/BlackholeTraffic.
AWS WAF
Web Application Firewall. Protects CloudFront, ALB, API Gateway, AppSync from OWASP Top 10, bots, and rate abuse.
# Create WAF WebACL with managed rule groups
aws wafv2 create-web-acl \
--name myapp-waf \
--scope REGIONAL \
--default-action Allow={} \
--rules '[
{
"Name": "AWSManagedRulesCommonRuleSet",
"Priority": 1,
"OverrideAction": {"None": {}},
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesCommonRuleSet"
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "CommonRuleSet"
}
},
{
"Name": "RateLimitRule",
"Priority": 2,
"Action": {"Block": {}},
"Statement": {
"RateBasedStatement": {
"Limit": 2000,
"AggregateKeyType": "IP"
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "RateLimit"
}
}
]' \
--visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=myapp-waf \
--region eu-west-1Security Hub
Aggregates findings from GuardDuty, Inspector, Macie, IAM Access Analyzer, Firewall Manager, and third-party tools. Scores compliance against CIS AWS Foundations Benchmark, PCI DSS, AWS Foundational Security Best Practices.
aws securityhub enable-security-hub --enable-default-standards
# Get critical findings
aws securityhub get-findings \
--filters '{"SeverityLabel": [{"Value": "CRITICAL", "Comparison": "EQUALS"}], "RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}]}' \
--query 'Findings[*].[Title, ProductName, AwsAccountId]' \
--output tableSecrets — Never in Code or Environment Variables
# Store in Secrets Manager, not .env
aws secretsmanager create-secret \
--name myapp/prod/database \
--secret-string '{"host":"db.prod","password":"secure123","username":"app"}'
# Rotate automatically
aws secretsmanager rotate-secret \
--secret-id myapp/prod/database \
--rotation-lambda-arn arn:aws:lambda:eu-west-1:123456789:function:SecretsRotationUse External Secrets Operator to sync Secrets Manager → Kubernetes Secrets. Never mount raw credentials as environment variables in ECS or Lambda.
CloudTrail — Audit Log
# Enable CloudTrail for all regions
aws cloudtrail create-trail \
--name myapp-audit \
--s3-bucket-name myapp-cloudtrail-logs \
--is-multi-region-trail \
--enable-log-file-validation \
--include-global-service-events
# Query CloudTrail with Athena for incident investigation
# SELECT * FROM cloudtrail_logs WHERE eventname = 'DeleteBucket' AND eventtime > '2026-05-01'Common Failure Cases
IAM role with wildcards granting unintended S3 write access to production buckets
Why: A developer role created with s3:* on arn:aws:s3:::* to unblock a task is never tightened; it remains in place and is later assumed by CI/CD, granting write access to every bucket in the account including production.
Detect: IAM Access Analyzer generates a finding for the role; CloudTrail shows the role performing s3:DeleteObject or s3:PutObject on buckets outside its intended scope.
Fix: Replace wildcards with resource-scoped actions (s3:GetObject on arn:aws:s3:::my-bucket/*); set a permission boundary on developer roles to cap the maximum effective permissions.
SCP deny blocks break break-glass emergency access
Why: A region-restriction or service-restriction SCP applies to all principals including the emergency IAM role; during an incident, the SRE cannot access the affected resources.
Detect: Emergency role assumptions fail with ExplicitDeny from organizations in CloudTrail; the SCP blocks the exact actions needed during incident response.
Fix: Use SCP condition keys (aws:PrincipalTag/BreakGlass: true) to exempt the emergency role from restrictive SCPs; test the emergency access path in a non-production account quarterly.
GuardDuty enabled but findings never actioned because no alert routing is configured
Why: GuardDuty generates findings but they remain in the console unread; no EventBridge rule routes HIGH/CRITICAL findings to SNS or a ticketing system.
Detect: GuardDuty console shows dozens of findings days old with no acknowledgement; no GuardDuty-related SNS topics or EventBridge rules exist.
Fix: Create an EventBridge rule matching {"source": ["aws.guardduty"], "detail.severity": [{"numeric": [">=", 7]}]} and route to an SNS topic that pages on-call; triage findings within 24 hours of creation.
Secrets Manager secret rotated but application still caches the old value Why: The application reads the secret at startup and caches it in memory; after rotation the cached value is stale and DB connections fail. Detect: Rotation succeeds in Secrets Manager but the application begins returning 500s immediately after the rotation window; CloudWatch logs show authentication failures against the database. Fix: Use the Secrets Manager SDK's built-in cache client (AWS Secrets Manager Caching Library) which handles rotation automatically, or implement a fallback that re-fetches on auth failure before raising an exception.
Connections
cloud-hub · cloud/secrets-management · cloud/cloud-networking · cloud/aws-cdk · security/guardrails · cs-fundamentals/auth-patterns
Open Questions
- What monitoring and alerting matter most when this is deployed in production?
- At what scale or workload does this approach hit its practical limits?
Related reading