Logo
Cloud SecurityAdvanced22 min read

AWS Security Hardening: Zero to Production in 45 Minutes

A hands-on technical blueprint for locking down a fresh AWS account — with real CLI commands, Terraform, and architecture diagrams.

AWSIAMGuardDutyCloudTrailTerraformSecurity Hub
Last updated: 2026-02-17

The Problem#

A default AWS account is an open invitation. No MFA on root. Default VPC with wide-open security groups. No logging. No alerting. No guardrails.

The average time from account creation to first unauthorized access attempt: 4 hours.

This guide fixes that. We'll take a brand-new AWS account and lock it down to production-grade security in 45 minutes. Every step includes real CLI commands and Terraform configs you can copy-paste.


Architecture Overview#

Here's what we're building:


Phase 1: Root Account Lockdown (5 min)#

The root account is the keys to the kingdom. Lock it down first.

Enable MFA on Root

bash
# You MUST do this in the console for root — no CLI alternative
# Console → IAM → Security Credentials → Assign MFA Device
# Use a hardware key (YubiKey) or authenticator app
# NEVER use SMS-based MFA

Delete Root Access Keys

bash
# List any existing root access keys
aws iam list-access-keys --user-name root

# If any exist, DELETE THEM IMMEDIATELY
aws iam delete-access-key --user-name root --access-key-id AKIAXXXXXXXXXXXXXXXX

Create a Break-Glass Procedure

bash
# Store root credentials in a physical safe or HSM
# Document the break-glass process:
cat << 'EOF' > break-glass-procedure.md
# Root Account Break-Glass Procedure
1. Retrieve sealed envelope from office safe
2. Require TWO authorized personnel present
3. Log access in incident tracking system
4. Complete task and immediately rotate password
5. Re-seal new credentials in envelope
6. File incident report within 24 hours
EOF

Elastyx Check: Our CSPM continuously monitors for root account access key existence, MFA status, and any root console logins. Alert fires within 60 seconds.


Phase 2: Identity Architecture (10 min)#

IAM is where 80% of cloud breaches start. We're building a zero-trust identity layer.

IAM Identity Center (SSO)

bash
# Enable IAM Identity Center (formerly AWS SSO)
aws sso-admin create-instance --name "production-sso"

# Create permission sets (role templates)
aws sso-admin create-permission-set \
  --instance-arn arn:aws:sso:::instance/ssoins-XXXXX \
  --name "SecurityAuditor" \
  --description "Read-only security audit access" \
  --session-duration "PT4H" \
  --relay-state ""

# Attach AWS managed policy for security audit
aws sso-admin attach-managed-policy-to-permission-set \
  --instance-arn arn:aws:sso:::instance/ssoins-XXXXX \
  --permission-set-arn arn:aws:sso:::permissionSet/ssoins-XXXXX/ps-XXXXX \
  --managed-policy-arn arn:aws:iam::aws:policy/SecurityAudit

Enforce Least Privilege with SCPs

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyRootAccount",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:root"
        }
      }
    },
    {
      "Sid": "DenyLeaveOrganization",
      "Effect": "Deny",
      "Action": "organizations:LeaveOrganization",
      "Resource": "*"
    },
    {
      "Sid": "EnforceIMDSv2",
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "StringNotEquals": {
          "ec2:MetadataHttpTokens": "required"
        }
      }
    },
    {
      "Sid": "DenyPublicS3",
      "Effect": "Deny",
      "Action": [
        "s3:PutBucketPublicAccessBlock"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "s3:PublicAccessBlockConfiguration/BlockPublicAcls": "true"
        }
      }
    }
  ]
}

IAM Access Analyzer

bash
# Enable IAM Access Analyzer to find overly permissive policies
aws accessanalyzer create-analyzer \
  --analyzer-name "production-analyzer" \
  --type ACCOUNT

# List findings (external access grants)
aws accessanalyzer list-findings \
  --analyzer-arn arn:aws:access-analyzer:us-east-1:ACCOUNT:analyzer/production-analyzer \
  --filter '{"status": {"eq": ["ACTIVE"]}}'

Phase 3: Network Segmentation (10 min)#

Delete the default VPC. Build a proper 3-tier network.

Terraform: Production VPC

hcl
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "production-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
  database_subnets = ["10.0.201.0/24", "10.0.202.0/24", "10.0.203.0/24"]

  # NAT Gateway for private subnet egress
  enable_nat_gateway     = true
  single_nat_gateway     = false  # HA: one per AZ
  one_nat_gateway_per_az = true

  # DNS
  enable_dns_hostnames = true
  enable_dns_support   = true

  # VPC Flow Logs → CloudWatch
  enable_flow_log                      = true
  create_flow_log_cloudwatch_log_group = true
  create_flow_log_iam_role             = true
  flow_log_max_aggregation_interval    = 60

  # Database subnet group
  create_database_subnet_group       = true
  create_database_subnet_route_table = true

  # Tags
  tags = {
    Environment = "production"
    ManagedBy   = "terraform"
    Security    = "hardened"
  }
}

# Block all public access to database subnets
resource "aws_network_acl_rule" "deny_db_ingress_public" {
  network_acl_id = module.vpc.database_network_acl_id
  rule_number    = 100
  egress         = false
  protocol       = "-1"
  rule_action    = "deny"
  cidr_block     = "0.0.0.0/0"
}

Delete Default VPCs

bash
# Delete default VPC in EVERY region — attackers love these
for region in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
  echo "Checking $region..."
  vpc_id=$(aws ec2 describe-vpcs \
    --region "$region" \
    --filters "Name=isDefault,Values=true" \
    --query 'Vpcs[0].VpcId' --output text)

  if [ "$vpc_id" != "None" ] && [ -n "$vpc_id" ]; then
    echo "  Deleting default VPC: $vpc_id in $region"

    # Delete subnets
    for subnet in $(aws ec2 describe-subnets --region "$region" \
      --filters "Name=vpc-id,Values=$vpc_id" \
      --query 'Subnets[].SubnetId' --output text); do
      aws ec2 delete-subnet --region "$region" --subnet-id "$subnet"
    done

    # Delete internet gateway
    for igw in $(aws ec2 describe-internet-gateways --region "$region" \
      --filters "Name=attachment.vpc-id,Values=$vpc_id" \
      --query 'InternetGateways[].InternetGatewayId' --output text); do
      aws ec2 detach-internet-gateway --region "$region" --internet-gateway-id "$igw" --vpc-id "$vpc_id"
      aws ec2 delete-internet-gateway --region "$region" --internet-gateway-id "$igw"
    done

    # Delete the VPC
    aws ec2 delete-vpc --region "$region" --vpc-id "$vpc_id"
    echo "  ✅ Deleted"
  fi
done

Phase 4: Detection Pipeline (10 min)#

You can't defend what you can't see. We're building a full detection stack.

Enable Everything

bash
# CloudTrail — multi-region, management + data events
aws cloudtrail create-trail \
  --name "production-trail" \
  --s3-bucket-name "company-cloudtrail-logs" \
  --is-multi-region-trail \
  --enable-log-file-validation \
  --kms-key-id "arn:aws:kms:us-east-1:ACCOUNT:key/KEY-ID"

aws cloudtrail start-logging --name "production-trail"

# GuardDuty — threat detection
aws guardduty create-detector \
  --enable \
  --finding-publishing-frequency FIFTEEN_MINUTES \
  --features '[
    {"Name": "S3_DATA_EVENTS", "Status": "ENABLED"},
    {"Name": "EKS_AUDIT_LOGS", "Status": "ENABLED"},
    {"Name": "EBS_MALWARE_PROTECTION", "Status": "ENABLED"},
    {"Name": "RDS_LOGIN_EVENTS", "Status": "ENABLED"},
    {"Name": "LAMBDA_NETWORK_LOGS", "Status": "ENABLED"},
    {"Name": "RUNTIME_MONITORING", "Status": "ENABLED"}
  ]'

# Security Hub — posture scoring
aws securityhub enable-security-hub \
  --enable-default-standards

# AWS Config — configuration drift
aws configservice put-configuration-recorder \
  --configuration-recorder name=default,roleARN=arn:aws:iam::ACCOUNT:role/ConfigRole \
  --recording-group allSupported=true,includeGlobalResourceTypes=true

aws configservice start-configuration-recorder --configuration-recorder-name default

Detection Flow

Auto-Remediation: Public S3 Bucket

python
# Lambda function: auto-block public S3 buckets
import boto3
import json

s3 = boto3.client('s3')

def handler(event, context):
    """Triggered by EventBridge when Security Hub finds public S3 bucket"""

    detail = event.get('detail', {})
    findings = detail.get('findings', [])

    for finding in findings:
        resources = finding.get('Resources', [])
        for resource in resources:
            if resource['Type'] == 'AwsS3Bucket':
                bucket_name = resource['Id'].split(':')[-1]

                print(f"🚨 Blocking public access on: {bucket_name}")

                s3.put_public_access_block(
                    Bucket=bucket_name,
                    PublicAccessBlockConfiguration={
                        'BlockPublicAcls': True,
                        'IgnorePublicAcls': True,
                        'BlockPublicPolicy': True,
                        'RestrictPublicBuckets': True
                    }
                )

                print(f"✅ Public access blocked on: {bucket_name}")

    return {
        'statusCode': 200,
        'body': json.dumps('Remediation complete')
    }

Phase 5: Continuous Posture Monitoring (10 min)#

Hardening is day one. Drift detection is forever.

Security Hub Custom Standards

bash
# Check your current security score
aws securityhub get-findings \
  --filters '{"ComplianceStatus": [{"Value": "FAILED", "Comparison": "EQUALS"}]}' \
  --query 'Findings[].{Title:Title,Severity:Severity.Label,Resource:Resources[0].Id}' \
  --output table

# Enable specific standards
aws securityhub batch-enable-standards --standards-subscription-requests \
  '[
    {"StandardsArn": "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/3.0.0"},
    {"StandardsArn": "arn:aws:securityhub:us-east-1::standards/aws-foundational-security-best-practices/v/1.0.0"},
    {"StandardsArn": "arn:aws:securityhub:us-east-1::standards/nist-800-53/v/5.0.0"}
  ]'

Posture Scoring Dashboard

Alerting Pipeline

bash
# EventBridge rule for critical findings → SNS → Slack/Teams
aws events put-rule \
  --name "critical-security-findings" \
  --event-pattern '{
    "source": ["aws.securityhub"],
    "detail-type": ["Security Hub Findings - Imported"],
    "detail": {
      "findings": {
        "Severity": {
          "Label": ["CRITICAL", "HIGH"]
        },
        "Workflow": {
          "Status": ["NEW"]
        }
      }
    }
  }'

# Create SNS topic for security alerts
aws sns create-topic --name "security-critical-alerts"
aws sns subscribe \
  --topic-arn arn:aws:sns:us-east-1:ACCOUNT:security-critical-alerts \
  --protocol email \
  --notification-endpoint security-team@company.com

The Hardening Checklist#

Before you call it done, verify every item:

#CheckCommandExpected
1Root MFA enabledConsole → IAM → Root✅ MFA device assigned
2No root access keysaws iam list-access-keysEmpty list
3CloudTrail enabledaws cloudtrail get-trail-statusIsLogging: true
4GuardDuty activeaws guardduty list-detectorsDetector ID returned
5Security Hub onaws securityhub describe-hubHubArn returned
6Default VPCs deletedaws ec2 describe-vpcs --filters Name=isDefault,Values=trueEmpty in all regions
7S3 public access blockedaws s3control get-public-access-block --account-id ACCOUNTAll four: true
8Config recorder runningaws configservice describe-configuration-recorder-statusrecording: true
9VPC flow logs enabledaws ec2 describe-flow-logsActive flow logs
10IMDSv2 enforcedSCP deployedec2:MetadataHttpTokens = required

What Elastyx Adds#

Everything above is manual. You built it once. But:

  • Who monitors for drift? Someone re-enables public S3 access at 2 AM.
  • Who catches the new service? A developer spins up a SageMaker notebook with default permissions.
  • Who maps to compliance? Your auditor asks for ISO 27001 evidence. Now what?

Elastyx continuously monitors 1,400+ security checks across AWS, Azure, GCP, and Kubernetes. It catches what Security Hub misses, maps findings to compliance frameworks, and auto-remediates before damage is done.

This guide got you to 80%. Elastyx gets you to 100% — and keeps you there.

Elastyx Platform

Skip the manual work. Let Elastyx do this continuously.

Everything in this guide — and 1,400+ more checks — running 24/7 across your entire cloud estate.

See Elastyx in Action