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
# 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 MFADelete Root Access Keys
# 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 AKIAXXXXXXXXXXXXXXXXCreate a Break-Glass Procedure
# 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
EOFElastyx 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)
# 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/SecurityAuditEnforce Least Privilege with SCPs
{
"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
# 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
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
# 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
donePhase 4: Detection Pipeline (10 min)#
You can't defend what you can't see. We're building a full detection stack.
Enable Everything
# 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 defaultDetection Flow
Auto-Remediation: Public S3 Bucket
# 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
# 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
# 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.comThe Hardening Checklist#
Before you call it done, verify every item:
| # | Check | Command | Expected |
|---|---|---|---|
| 1 | Root MFA enabled | Console → IAM → Root | ✅ MFA device assigned |
| 2 | No root access keys | aws iam list-access-keys | Empty list |
| 3 | CloudTrail enabled | aws cloudtrail get-trail-status | IsLogging: true |
| 4 | GuardDuty active | aws guardduty list-detectors | Detector ID returned |
| 5 | Security Hub on | aws securityhub describe-hub | HubArn returned |
| 6 | Default VPCs deleted | aws ec2 describe-vpcs --filters Name=isDefault,Values=true | Empty in all regions |
| 7 | S3 public access blocked | aws s3control get-public-access-block --account-id ACCOUNT | All four: true |
| 8 | Config recorder running | aws configservice describe-configuration-recorder-status | recording: true |
| 9 | VPC flow logs enabled | aws ec2 describe-flow-logs | Active flow logs |
| 10 | IMDSv2 enforced | SCP deployed | ec2: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.