AWS Well-Architected Review - fixing the 10 most common HRI findings step by step
Fix the 10 most common high-risk items (HRI) from AWS Well-Architected Reviews. Step by step: IAM root access, encryption, backups, Multi-AZ, monitoring, logging - with CLI commands and Terraform.
After completing a WAFR, fast remediation is key to qualifying for the AWS 10% discount on related resources. Each fix below corresponds to a specific question in the Well-Architected Tool and once implemented allows you to flip the status from HRI to “resolved”.
If you haven’t done a review yet, check our WAFR preparation guide or Well-Architected Review service.
1. Root account without MFA and with active API keys
| Pillar: Security | WA Question: SEC 1 - How do you securely manage credentials? |
Problem: The AWS root account has active access keys or MFA not enabled. Root has irrevocable permissions to everything in the account.
Fix:
# Check MFA status on root account
aws iam get-account-summary --query 'SummaryMap.AccountMFAEnabled'
# If returns 0 - MFA is not enabled
# Check if root has active API keys
aws iam get-account-summary --query 'SummaryMap.AccountAccessKeysPresent'
# If returns 1 - keys exist and must be deleted
# Delete root keys (requires logging in as root in console)
# Console → IAM → Security credentials → Delete access keys
# Enable MFA (hardware key preferred, TOTP acceptable)
# Console → IAM → Security credentials → Assign MFA device
Validation:
aws iam get-account-summary \
--query '{MFA: SummaryMap.AccountMFAEnabled, AccessKeys: SummaryMap.AccountAccessKeysPresent}'
# Expected: {"MFA": 1, "AccessKeys": 0}
2. No encryption at rest (EBS, S3, RDS)
| Pillar: Security | WA Question: SEC 8 - How do you protect your data at rest? |
Problem: EBS volumes, S3 buckets, or RDS databases without encryption. Data stored in plaintext on AWS disks.
Fix (preventive - enforce on new resources):
# Enable default EBS encryption for the region
aws ec2 enable-ebs-encryption-by-default --region eu-central-1
# Verify status
aws ec2 get-ebs-encryption-by-default --region eu-central-1
# Expected: "EbsEncryptionByDefault": true
Enforce S3 encryption (Terraform):
resource "aws_s3_bucket_server_side_encryption_configuration" "enforce" {
bucket = aws_s3_bucket.main.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.s3.arn
}
bucket_key_enabled = true
}
}
Existing unencrypted EBS volumes:
# Find unencrypted volumes
aws ec2 describe-volumes \
--filters Name=encrypted,Values=false \
--query 'Volumes[].{Id:VolumeId, Size:Size, State:State, Attached:Attachments[0].InstanceId}' \
--output table
# Encryption requires: create snapshot → encrypted copy → new volume
# For attached volumes: requires planned downtime window
3. No automated backups or untested recovery process
| Pillar: Reliability | WA Question: REL 9 - How do you back up data? |
Problem: No automated backups, or backups exist but have never been tested.
Fix (AWS Backup - centralised):
resource "aws_backup_plan" "daily" {
name = "daily-backup-plan"
rule {
rule_name = "daily-retention-35"
target_vault_name = aws_backup_vault.main.name
schedule = "cron(0 3 * * ? *)" # daily at 03:00 UTC
lifecycle {
delete_after = 35 # 35-day retention
}
copy_action {
destination_vault_arn = aws_backup_vault.dr_region.arn
lifecycle {
delete_after = 7 # DR copy: 7 days
}
}
}
}
resource "aws_backup_selection" "production" {
name = "production-resources"
plan_id = aws_backup_plan.daily.id
iam_role_arn = aws_iam_role.backup.arn
selection_tag {
type = "STRINGEQUALS"
key = "environment"
value = "production"
}
}
Validation (restore test):
# Run a test restore from the latest backup
aws backup start-restore-job \
--recovery-point-arn arn:aws:ec2:eu-central-1:123456789012:snapshot/snap-xxx \
--iam-role-arn arn:aws:iam::123456789012:role/BackupRestoreRole \
--metadata '{"availabilityZone":"eu-central-1a","encrypted":"true"}' \
--resource-type EBS
# Check status
aws backup describe-restore-job --restore-job-id <job-id>
4. No KMS key rotation
| Pillar: Security | WA Question: SEC 8 - How do you protect your data at rest? |
Problem: Customer-managed KMS keys without automatic rotation enabled.
Fix:
# Find keys without rotation
for key_id in $(aws kms list-keys --query 'Keys[].KeyId' --output text); do
rotation=$(aws kms get-key-rotation-status --key-id $key_id --query 'KeyRotationEnabled' --output text 2>/dev/null)
if [ "$rotation" = "False" ]; then
alias=$(aws kms list-aliases --key-id $key_id --query 'Aliases[0].AliasName' --output text)
echo "NO ROTATION: $key_id ($alias)"
fi
done
# Enable rotation for each key
aws kms enable-key-rotation --key-id <key-id>
Terraform:
resource "aws_kms_key" "main" {
description = "Main encryption key"
enable_key_rotation = true # rotates every 365 days (automatic)
policy = data.aws_iam_policy_document.kms.json
}
Validation:
aws kms get-key-rotation-status --key-id <key-id>
# Expected: "KeyRotationEnabled": true
5. No Multi-AZ for production databases
| Pillar: Reliability | WA Question: REL 11 - How do you design your workload to withstand component failures? |
Problem: Production RDS/Aurora database without Multi-AZ. Failure of one availability zone means database unavailability (15-30 minutes to restore from snapshot).
Fix:
# Check which databases lack Multi-AZ
aws rds describe-db-instances \
--query 'DBInstances[?MultiAZ==`false`].{Id:DBInstanceIdentifier, Engine:Engine, Class:DBInstanceClass}' \
--output table
# Enable Multi-AZ (causes brief ~30s blip during failover setup)
aws rds modify-db-instance \
--db-instance-identifier production-db \
--multi-az \
--apply-immediately
Note: Enabling Multi-AZ doubles instance cost. For dev/staging environments this is typically unjustified. Apply selectively - production only.
6. Security Groups with 0.0.0.0/0 on non-web ports
| Pillar: Security | WA Question: SEC 5 - How do you protect your network resources? |
Problem: Security Groups with open internet access on SSH (22), RDP (3389), database (3306, 5432) or other non-web ports.
Fix:
# Find Security Groups with 0.0.0.0/0 rules on ports != 80/443
aws ec2 describe-security-groups \
--filters Name=ip-permission.cidr,Values=0.0.0.0/0 \
--query 'SecurityGroups[].{GroupId:GroupId, Name:GroupName, Rules:IpPermissions[?IpRanges[?CidrIp==`0.0.0.0/0`] && FromPort!=`80` && FromPort!=`443`]}' \
--output json | jq '.[] | select(.Rules | length > 0)'
# Revoke SSH from internet
aws ec2 revoke-security-group-ingress \
--group-id sg-xxx \
--protocol tcp \
--port 22 \
--cidr 0.0.0.0/0
Alternative for SSH access: AWS Systems Manager Session Manager (zero open ports):
# Access instance without SSH
aws ssm start-session --target i-xxx
7. No centralised logging (CloudTrail disabled or unmonitored)
| Pillar: Security | WA Question: SEC 4 - How do you detect and investigate security events? |
Problem: CloudTrail not active in all regions, or logs go to a bucket nobody monitors.
Fix (Terraform):
resource "aws_cloudtrail" "org_trail" {
name = "organization-trail"
s3_bucket_name = aws_s3_bucket.cloudtrail.id
is_organization_trail = true
is_multi_region_trail = true
include_global_service_events = true
enable_log_file_validation = true
cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.trail.arn}:*"
cloud_watch_logs_role_arn = aws_iam_role.cloudtrail_cloudwatch.arn
event_selector {
read_write_type = "All"
include_management_events = true
}
}
# Alert on root login
resource "aws_cloudwatch_metric_filter" "root_login" {
name = "root-console-login"
pattern = "{ $.userIdentity.type = \"Root\" && $.eventType = \"AwsConsoleSignIn\" }"
log_group_name = aws_cloudwatch_log_group.trail.name
metric_transformation {
name = "RootLoginCount"
namespace = "SecurityMetrics"
value = "1"
}
}
8. No resource tagging (no cost allocation)
| Pillar: Cost Optimisation | WA Question: COST 2 - How do you govern usage? |
Problem: >30% of resources without tags. No ability to allocate costs to teams or services.
Fix (SCP enforcing tags):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RequireTagsOnEC2",
"Effect": "Deny",
"Action": ["ec2:RunInstances"],
"Resource": ["arn:aws:ec2:*:*:instance/*", "arn:aws:ec2:*:*:volume/*"],
"Condition": {
"Null": {
"aws:RequestTag/team": "true",
"aws:RequestTag/environment": "true",
"aws:RequestTag/service": "true"
}
}
}
]
}
Bulk-tag existing resources:
# Tag all EC2 instances in a given VPC
for instance_id in $(aws ec2 describe-instances \
--filters Name=vpc-id,Values=vpc-xxx \
--query 'Reservations[].Instances[].InstanceId' --output text); do
aws ec2 create-tags --resources $instance_id \
--tags Key=team,Value=platform Key=environment,Value=production
done
9. No AWS service quota monitoring
| Pillar: Reliability | WA Question: REL 1 - How do you manage service quotas and constraints? |
Problem: No monitoring of approach to AWS limits (e.g. max VPCs, max EC2 instances, max EBS volumes). Workload grows and suddenly can’t scale.
Fix:
# Check utilisation of key limits
aws service-quotas list-service-quotas --service-code ec2 \
--query 'Quotas[?UsageMetric!=null].{Name:QuotaName, Value:Value, Usage:UsageMetric}' \
--output table
# Enable Trusted Advisor check on limits (requires Business+ support)
aws support describe-trusted-advisor-checks \
--language en \
--query 'checks[?category==`service_limits`].{Name:name, Id:id}' \
--output table
CloudWatch alarm on limits (Terraform):
resource "aws_cloudwatch_metric_alarm" "ec2_limit" {
alarm_name = "ec2-instance-limit-80pct"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "ResourceCount"
namespace = "AWS/TrustedAdvisor"
period = 86400
statistic = "Maximum"
threshold = 80 # alert at 80% of limit
alarm_actions = [aws_sns_topic.alerts.arn]
}
10. No disaster recovery plan or untested plan
| Pillar: Reliability | WA Question: REL 13 - How do you plan for disaster recovery? |
Problem: No documented DR plan, or plan exists but has never been tested.
Minimum DR requirements:
# Check cross-region replication for critical data
# S3: Cross-Region Replication
aws s3api get-bucket-replication --bucket production-data \
--query 'ReplicationConfiguration.Rules[].{Status:Status, Destination:Destination.Bucket}'
# RDS: Read Replica in second region (for Aurora: Global Database)
aws rds describe-db-instances \
--query 'DBInstances[?ReadReplicaSourceDBInstanceIdentifier!=null].{Id:DBInstanceIdentifier, Source:ReadReplicaSourceDBInstanceIdentifier, Region:AvailabilityZone}'
DR documentation (minimum):
| Element | Value | Last tested |
|---|---|---|
| RTO (recovery time objective) | 4h | 2026-03-15 |
| RPO (recovery point objective) | 1h | 2026-03-15 |
| DR region | eu-west-1 | - |
| Failover process | Documentation in Confluence | Not tested |
| Responsible | On-call SRE | - |
Key principle: A DR plan that hasn’t been tested isn’t a plan - it’s a wish. Test failover quarterly, minimum every six months.
After remediation: closing the loop in AWS WA Tool
After implementing each fix:
# 1. Log into AWS Well-Architected Tool
# 2. Open the relevant workload
# 3. Find the question linked to the fix
# 4. Change the answer from "High Risk" to "No Risk" or "Medium Risk"
# 5. Add a note describing what was fixed and when
# After closing a batch of fixes - create a milestone
aws wellarchitected create-milestone \
--workload-id <workload-id> \
--milestone-name "Remediation Q3 2026 - batch 1"
The milestone documents the “before” and “after” state, is visible to AWS, and forms the basis for qualifying for the 10% discount on remediated resources.
Need help fixing findings from your review? Check our Well-Architected Review service - we run both the review and the remediation.