Use this page when a security review or a posture tool (AWS Security Hub, Config, a CSPM product) produces hundreds of findings, often one ticket per resource. The trick is to group them by control, fix the pattern once, and roll it out account by account.
Group findings by control, not by resource
| Control | Check | Typical fix |
|---|---|---|
| VPC flow logs enabled | Every VPC has flow logs to a central bucket or log group | IaC module adds flow logs to every VPC |
| HTTPS only | Load balancer HTTP listeners redirect to HTTPS | Listener rule: redirect 80 to 443 |
| No public databases | PubliclyAccessible=false; security groups have no 0.0.0.0/0 on DB ports |
Flip flag in a window; private access path |
| No dead event targets | Scheduled and event rules point at functions that exist | Delete the rule or fix the target |
| No plaintext secrets in build configs | Build specs and CI variables use a secret manager | Move to secret manager, rotate |
| No broad IAM | No *:* policies on users or roles except break-glass |
Replace with scoped policies |
| No old access keys | Keys under 90 days or replaced by roles | Rotate or retire |
| Encryption at rest | Volumes, buckets, databases encrypted | Default encryption; migrate old resources |
One parent ticket per control, child tickets or a checklist per account. That turns a pile of per-resource tickets into a few decisions.
Read-only checks
P=<readonly-profile>; R=<region>
# VPCs without flow logs
comm -23 \
<(aws --profile $P --region $R ec2 describe-vpcs --query 'Vpcs[].VpcId' --output text | tr '\t' '\n' | sort) \
<(aws --profile $P --region $R ec2 describe-flow-logs --query 'FlowLogs[].ResourceId' --output text | tr '\t' '\n' | sort)
# HTTP listeners that do not redirect
for lb in $(aws --profile $P --region $R elbv2 describe-load-balancers --query 'LoadBalancers[].LoadBalancerArn' --output text); do
aws --profile $P --region $R elbv2 describe-listeners --load-balancer-arn "$lb" \
--query 'Listeners[?Protocol==`HTTP`].{arn:ListenerArn,action:DefaultActions[0].Type}' --output text
done | awk '$1 != "redirect"'
# Public databases
aws --profile $P --region $R rds describe-db-instances \
--query 'DBInstances[?PubliclyAccessible].DBInstanceIdentifier' --output text
# Rules whose Lambda target no longer exists
for rule in $(aws --profile $P --region $R events list-rules --query 'Rules[].Name' --output text); do
for arn in $(aws --profile $P --region $R events list-targets-by-rule --rule "$rule" \
--query 'Targets[?contains(Arn, `:function:`)].Arn' --output text); do
aws --profile $P --region $R lambda get-function --function-name "$arn" >/dev/null 2>&1 \
|| echo "dead target: rule=$rule target=$arn"
done
done
# Build projects with plaintext env vars that look secret (names only, never values)
for p in $(aws --profile $P --region $R codebuild list-projects --query 'projects[]' --output text); do
aws --profile $P --region $R codebuild batch-get-projects --names "$p" \
--query 'projects[].environment.environmentVariables[?type==`PLAINTEXT`].name' --output text \
| tr '\t' '\n' | grep -iE 'pass|secret|token|key' | sed "s/^/$p: /"
done
Fix in safe batches
- Write the fix once in a shared IaC module or a standard snippet.
- Pilot in one non-production account. Verify nothing broke.
- Roll out by account, lowest risk first, one MR per account or stack.
- Production-impacting fixes (closing public database access, enforcing HTTPS) go through change control with a named owner for each resource.
- Some fixes need an approver who owns the system. Batch the approval requests so they decide on the list once.
Verify and keep it fixed
- Re-run the check; the count goes to zero for that account.
- Add a preventive control: an AWS Config rule, an SCP, or a policy check in CI that blocks the bad pattern.
- Report counts per control per week until done.
Pitfalls
- Flipping a database to private without checking who connects from outside. Look at connections and security group sources first.
- Enforcing HTTPS on a health check or partner integration that only speaks HTTP. Find those first.
- Deleting "dead" rules that are dead because a deploy is mid-flight. Check the function's history.