AWS Well-Architected WAFR security reliability remediation IAM KMS

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.

Jerzy Kopaczewski ·
Your Well-Architected Review is done. The report contains a list of High Risk Items (HRI) - but what now? This runbook covers the 10 most commonly found WAFR issues and shows how to fix them step by step. Each fix includes a CLI command or Terraform configuration you can deploy immediately.

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.