Infrastructure Security Hardening

Prerequisites

Summary

This guide covers comprehensive security hardening for cloud infrastructure, implementing defense-in-depth strategies to protect against threats. You'll learn CIS benchmark compliance, IAM best practices, encryption implementation, network security, threat detection, and automated compliance monitoring.

Security breaches cost organizations millions in damages, regulatory fines, and reputation loss. A single misconfigured S3 bucket or overly permissive IAM policy can expose sensitive data to the internet. This guide provides practical hardening techniques following industry standards to significantly reduce your attack surface.

What You'll Learn


CIS Benchmarks

What are CIS Benchmarks?

Center for Internet Security (CIS) publishes security configuration baselines for various platforms. CIS Benchmarks provide prescriptive guidance for securing systems.

CIS AWS Foundations Benchmark covers:

Each control has:

Automated CIS Compliance Scanning

Prowler - Open-source AWS security tool:

# Installation
pip3 install prowler

# Or use Docker
docker run -it --rm \
  -e AWS_ACCESS_KEY_ID \
  -e AWS_SECRET_ACCESS_KEY \
  -e AWS_SESSION_TOKEN \
  -v $(pwd)/output:/home/prowler/output \
  toniblyx/prowler:latest

# Run full CIS Benchmark scan
prowler aws --compliance cis_2.0_aws

# Output shows PASS/FAIL for each control:
# [PASS] 1.1 - Avoid root account use
# [FAIL] 1.2 - Ensure MFA enabled for root account
# [PASS] 1.3 - Ensure credentials unused for 90 days disabled
# [FAIL] 2.1.1 - Ensure S3 bucket logging enabled

# Generate HTML report
prowler aws --compliance cis_2.0_aws --output-formats html
# Creates prowler-output-*.html with all findings

AWS Security Hub - Native CIS compliance:

# Terraform to enable Security Hub with CIS standard
resource "aws_securityhub_account" "main" {}

resource "aws_securityhub_standards_subscription" "cis" {
  standards_arn = "arn:aws:securityhub:us-east-1::standards/cis-aws-foundations-benchmark/v/1.4.0"
  
  depends_on = [aws_securityhub_account.main]
}

# Security Hub automatically checks compliance
# View in Console: Security Hub → Security Standards → CIS AWS Foundations Benchmark

Key CIS Controls to Implement:

  1. 1.4 - Ensure access keys rotated every 90 days:
# Find old access keys
aws iam generate-credential-report
aws iam get-credential-report --query 'Content' --output text | base64 -d > credentials.csv

# Parse CSV for keys older than 90 days
awk -F',' '$11 != "N/A" && $11 < "'$(date -d '90 days ago' +%Y-%m-%d)'" {print $1, $11}' credentials.csv
  1. 1.12 - Ensure no root account access key exists:
# Check root account credentials
aws iam get-account-summary | grep AccountAccessKeysPresent
# Should be 0

# If root keys exist, delete immediately
aws iam delete-access-key --access-key-id AKIAIOSFODNN7EXAMPLE --user-name root
  1. 2.1.1 - Ensure S3 bucket logging enabled:
resource "aws_s3_bucket" "logs" {
  bucket = "my-logs-bucket"
}

resource "aws_s3_bucket" "data" {
  bucket = "my-data-bucket"
}

resource "aws_s3_bucket_logging" "data" {
  bucket = aws_s3_bucket.data.id
  
  target_bucket = aws_s3_bucket.logs.id
  target_prefix = "s3-access-logs/"
}
  1. 2.1.2 - Ensure S3 bucket has MFA Delete enabled:
# Enable versioning first
aws s3api put-bucket-versioning \
  --bucket my-bucket \
  --versioning-configuration Status=Enabled

# Enable MFA Delete (requires root account with MFA)
aws s3api put-bucket-versioning \
  --bucket my-bucket \
  --versioning-configuration Status=Enabled,MFADelete=Enabled \
  --mfa "arn:aws:iam::123456789012:mfa/root-account-mfa-device XXXXXX"
  1. 2.3.1 - Ensure RDS encryption enabled:
resource "aws_db_instance" "main" {
  identifier     = "mydb"
  engine         = "postgres"
  instance_class = "db.t3.medium"
  
  storage_encrypted = true
  kms_key_id        = aws_kms_key.db.arn
  
  # Other configuration...
}
  1. 3.1 - Ensure CloudTrail enabled in all regions:
resource "aws_cloudtrail" "main" {
  name                          = "organization-trail"
  s3_bucket_name                = aws_s3_bucket.cloudtrail.id
  include_global_service_events = true
  is_multi_region_trail         = true
  enable_log_file_validation    = true
  
  event_selector {
    read_write_type           = "All"
    include_management_events = true
    
    data_resource {
      type   = "AWS::S3::Object"
      values = ["arn:aws:s3:::*/"]
    }
  }
}
  1. 4.1 - Ensure no security groups allow 0.0.0.0/0 ingress to port 22:
# Find security groups with SSH open to world
aws ec2 describe-security-groups \
  --query 'SecurityGroups[?IpPermissions[?FromPort==`22` && ToPort==`22` && IpRanges[?CidrIp==`0.0.0.0/0`]]].[GroupId,GroupName]' \
  --output table

# Revoke the rule
aws ec2 revoke-security-group-ingress \
  --group-id sg-abc123 \
  --protocol tcp \
  --port 22 \
  --cidr 0.0.0.0/0

# Add restricted rule instead
aws ec2 authorize-security-group-ingress \
  --group-id sg-abc123 \
  --protocol tcp \
  --port 22 \
  --cidr 203.0.113.0/24  # Your office IP range

Azure CIS Benchmarks

# Azure Secure Score (built-in CIS compliance)
az security secure-score list

# Microsoft Defender for Cloud
az security pricing list
az security pricing create \
  --name VirtualMachines \
  --tier Standard

# Azure Policy for CIS compliance
az policy assignment create \
  --name "CIS-Azure-Benchmark" \
  --policy-set-definition "/providers/Microsoft.Authorization/policySetDefinitions/c3f5c4d9-9a1d-4a99-85c0-7f93e1b5b0e8" \
  --scope "/subscriptions/{subscription-id}"

GCP CIS Benchmarks

# Security Command Center
gcloud services enable securitycenter.googleapis.com

# Security Health Analytics
gcloud scc findings list ORGANIZATION_ID \
  --source=ORGANIZATION_ID/sources/SECURITY_HEALTH_ANALYTICS_SOURCE_ID \
  --filter="category:\"CIS_GCP_BENCHMARK\""

# Forseti Security (open-source)
git clone https://github.com/forseti-security/forseti-security.git
cd forseti-security
python3 install/gcp_installer.py

IAM Least Privilege

Principle of Least Privilege

Grant minimum permissions required for task completion. Avoid wildcard permissions and overly broad policies.

Bad Practice (wildcard everything):

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "*",
    "Resource": "*"
  }]
}

Good Practice (specific permissions):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::my-app-bucket/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:Query"
      ],
      "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/my-table"
    }
  ]
}

IAM Policy Best Practices

1. Use IAM Roles, Not Long-Term Credentials:

# EC2 instance with IAM role
resource "aws_iam_role" "ec2_app" {
  name = "ec2-app-role"
  
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "ec2.amazonaws.com"
      }
    }]
  })
}

resource "aws_iam_role_policy" "ec2_app" {
  name = "ec2-app-policy"
  role = aws_iam_role.ec2_app.id
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "s3:GetObject",
        "s3:PutObject"
      ]
      Resource = "arn:aws:s3:::my-app-bucket/*"
    }]
  })
}

resource "aws_iam_instance_profile" "ec2_app" {
  name = "ec2-app-profile"
  role = aws_iam_role.ec2_app.name
}

resource "aws_instance" "app" {
  ami                  = data.aws_ami.amazon_linux_2.id
  instance_type        = "t3.medium"
  iam_instance_profile = aws_iam_instance_profile.ec2_app.name
  
  # Application automatically uses IAM role
  # No hard-coded credentials needed
}

2. Implement Permission Boundaries:

# Boundary limits maximum permissions
resource "aws_iam_policy" "developer_boundary" {
  name = "DeveloperBoundary"
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:*",
          "dynamodb:*",
          "lambda:*",
          "ec2:Describe*"
        ]
        Resource = "*"
      },
      {
        Effect = "Deny"
        Action = [
          "iam:*",
          "organizations:*",
          "account:*"
        ]
        Resource = "*"
      }
    ]
  })
}

# Apply boundary to developer role
resource "aws_iam_role" "developer" {
  name                 = "developer-role"
  permissions_boundary = aws_iam_policy.developer_boundary.arn
  
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        AWS = "arn:aws:iam::123456789012:root"
      }
    }]
  })
}

# Developer can never exceed boundary permissions
# Even if granted additional policies

3. Use Service Control Policies (SCPs):

Organization-level policies preventing actions across all accounts:

# Deny root account usage
resource "aws_organizations_policy" "deny_root" {
  name    = "DenyRootAccountUsage"
  type    = "SERVICE_CONTROL_POLICY"
  content = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Deny"
      Action = "*"
      Resource = "*"
      Condition = {
        StringLike = {
          "aws:PrincipalArn" = "arn:aws:iam::*:root"
        }
      }
    }]
  })
}

# Prevent disabling CloudTrail
resource "aws_organizations_policy" "protect_cloudtrail" {
  name = "ProtectCloudTrail"
  type = "SERVICE_CONTROL_POLICY"
  content = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Deny"
      Action = [
        "cloudtrail:StopLogging",
        "cloudtrail:DeleteTrail"
      ]
      Resource = "*"
    }]
  })
}

# Restrict regions
resource "aws_organizations_policy" "restrict_regions" {
  name = "RestrictRegions"
  type = "SERVICE_CONTROL_POLICY"
  content = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Deny"
      NotAction = [
        "iam:*",
        "cloudfront:*",
        "route53:*",
        "support:*"
      ]
      Resource = "*"
      Condition = {
        StringNotEquals = {
          "aws:RequestedRegion" = [
            "us-east-1",
            "us-west-2"
          ]
        }
      }
    }]
  })
}

# Attach policies to organizational unit
resource "aws_organizations_policy_attachment" "production" {
  policy_id = aws_organizations_policy.deny_root.id
  target_id = aws_organizations_organizational_unit.production.id
}

4. Analyze IAM with Access Analyzer:

resource "aws_accessanalyzer_analyzer" "main" {
  analyzer_name = "organization-analyzer"
  type          = "ORGANIZATION"
  
  depends_on = [aws_organizations_organization.main]
}

# Access Analyzer identifies:
# - Resources shared with external accounts
# - Overly permissive policies
# - Unused access

Check findings:

# List findings
aws accessanalyzer list-findings \
  --analyzer-arn arn:aws:access-analyzer:us-east-1:123456789012:analyzer/organization-analyzer

# Example finding:
# {
#   "id": "abc123",
#   "resourceType": "AWS::S3::Bucket",
#   "resource": "arn:aws:s3:::my-bucket",
#   "principal": {"AWS": "123456789999"},  # External account
#   "action": ["s3:GetObject"],
#   "condition": {},
#   "isPublic": false
# }

5. Credential Rotation:

# Find old access keys
aws iam list-users --query 'Users[].UserName' --output text | while read user; do
  aws iam list-access-keys --user-name $user --query 'AccessKeyMetadata[?Status==`Active`].[UserName,AccessKeyId,CreateDate]' --output text | while read username keyid date; do
    age=$(( ($(date +%s) - $(date -d "$date" +%s)) / 86400 ))
    if [ $age -gt 90 ]; then
      echo "Key $keyid for $username is $age days old - rotate immediately"
    fi
  done
done

# Automated rotation with Lambda
# Triggered by EventBridge rule every 90 days

Terraform automated rotation:

resource "aws_iam_user" "service_account" {
  name = "app-service-account"
}

resource "aws_iam_access_key" "service_account" {
  user = aws_iam_user.service_account.name
}

# Store in Secrets Manager with rotation
resource "aws_secretsmanager_secret" "service_account" {
  name = "app-service-account-key"
  
  rotation_rules {
    automatically_after_days = 90
  }
}

resource "aws_secretsmanager_secret_version" "service_account" {
  secret_id = aws_secretsmanager_secret.service_account.id
  secret_string = jsonencode({
    access_key_id     = aws_iam_access_key.service_account.id
    secret_access_key = aws_iam_access_key.service_account.secret
  })
}

Multi-Factor Authentication (MFA)

Enforce MFA for Console Access

# IAM policy requiring MFA for all actions
resource "aws_iam_policy" "require_mfa" {
  name = "RequireMFA"
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Sid    = "DenyAllExceptListedIfNoMFA"
      Effect = "Deny"
      NotAction = [
        "iam:CreateVirtualMFADevice",
        "iam:EnableMFADevice",
        "iam:GetUser",
        "iam:ListMFADevices",
        "iam:ListVirtualMFADevices",
        "iam:ResyncMFADevice",
        "sts:GetSessionToken"
      ]
      Resource = "*"
      Condition = {
        BoolIfExists = {
          "aws:MultiFactorAuthPresent" = "false"
        }
      }
    }]
  })
}

# Attach to all users
resource "aws_iam_group" "all_users" {
  name = "AllUsers"
}

resource "aws_iam_group_policy_attachment" "require_mfa" {
  group      = aws_iam_group.all_users.name
  policy_arn = aws_iam_policy.require_mfa.arn
}

resource "aws_iam_user_group_membership" "example" {
  user   = aws_iam_user.example.name
  groups = [aws_iam_group.all_users.name]
}

Users without MFA can only:

  1. List their MFA devices
  2. Create virtual MFA device
  3. Enable MFA device
  4. Resync MFA if out of sync

All other actions denied until MFA enabled.

Enforce MFA for Root Account

# Check if root MFA enabled
aws iam get-account-summary | grep AccountMFAEnabled
# Should be 1

# Enable virtual MFA for root
# 1. Sign in as root
# 2. IAM Dashboard → Activate MFA on your root account
# 3. Choose Virtual MFA device
# 4. Scan QR code with authenticator app
# 5. Enter two consecutive MFA codes
# 6. MFA activated

MFA for API/CLI Access

Require MFA for sensitive operations:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "AllowInstanceTerminationWithMFA",
    "Effect": "Allow",
    "Action": "ec2:TerminateInstances",
    "Resource": "*",
    "Condition": {
      "Bool": {
        "aws:MultiFactorAuthPresent": "true"
      }
    }
  }]
}

Using CLI with MFA:

# Get temporary credentials with MFA
aws sts get-session-token \
  --serial-number arn:aws:iam::123456789012:mfa/john \
  --token-code 123456 \
  --duration-seconds 43200

# Output includes temporary AccessKeyId, SecretAccessKey, SessionToken
# Export these and use for next 12 hours
export AWS_ACCESS_KEY_ID=ASIAIOSFODNN7EXAMPLE
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
export AWS_SESSION_TOKEN=verylongtoken...

# Now can perform MFA-required actions
aws ec2 terminate-instances --instance-ids i-abc123

Encryption

Encryption at Rest

S3 Bucket Encryption:

# Default encryption for all objects
resource "aws_s3_bucket" "data" {
  bucket = "sensitive-data-bucket"
}

resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
  bucket = aws_s3_bucket.data.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  # Reduces KMS costs
  }
}

# Deny unencrypted uploads
resource "aws_s3_bucket_policy" "require_encryption" {
  bucket = aws_s3_bucket.data.id
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Sid    = "DenyUnencryptedObjectUploads"
      Effect = "Deny"
      Principal = "*"
      Action = "s3:PutObject"
      Resource = "${aws_s3_bucket.data.arn}/*"
      Condition = {
        StringNotEquals = {
          "s3:x-amz-server-side-encryption" = "aws:kms"
        }
      }
    }]
  })
}

EBS Volume Encryption:

# Enable EBS encryption by default for region
resource "aws_ebs_encryption_by_default" "main" {
  enabled = true
}

# All new volumes automatically encrypted
resource "aws_instance" "app" {
  ami           = data.aws_ami.amazon_linux_2.id
  instance_type = "t3.medium"
  
  root_block_device {
    encrypted   = true
    kms_key_id  = aws_kms_key.ebs.arn
    volume_size = 20
  }
}

# Encrypt existing unencrypted volume
# 1. Create snapshot
# 2. Copy snapshot with encryption
# 3. Create volume from encrypted snapshot
# 4. Detach old volume, attach new volume

RDS Encryption:

resource "aws_db_instance" "main" {
  identifier     = "mydb"
  engine         = "postgres"
  instance_class = "db.t3.medium"
  
  storage_encrypted = true
  kms_key_id        = aws_kms_key.rds.arn
  
  # Enable encryption for backups
  backup_retention_period = 7
  # Backups automatically encrypted with same key
}

# Note: Cannot enable encryption on existing unencrypted DB
# Must create encrypted snapshot and restore to new encrypted instance

KMS Key Management:

# Customer-managed key with rotation
resource "aws_kms_key" "main" {
  description             = "Application encryption key"
  deletion_window_in_days = 30
  enable_key_rotation     = true  # Automatic yearly rotation
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "Enable IAM User Permissions"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::123456789012:root"
        }
        Action   = "kms:*"
        Resource = "*"
      },
      {
        Sid    = "Allow services to use key"
        Effect = "Allow"
        Principal = {
          Service = [
            "s3.amazonaws.com",
            "rds.amazonaws.com",
            "ec2.amazonaws.com"
          ]
        }
        Action = [
          "kms:Decrypt",
          "kms:GenerateDataKey"
        ]
        Resource = "*"
      }
    ]
  })
}

resource "aws_kms_alias" "main" {
  name          = "alias/application-key"
  target_key_id = aws_kms_key.main.key_id
}

Encryption in Transit

Enforce HTTPS/TLS:

# S3 bucket policy requiring TLS
resource "aws_s3_bucket_policy" "require_tls" {
  bucket = aws_s3_bucket.data.id
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Sid    = "DenyNonHTTPS"
      Effect = "Deny"
      Principal = "*"
      Action = "s3:*"
      Resource = [
        aws_s3_bucket.data.arn,
        "${aws_s3_bucket.data.arn}/*"
      ]
      Condition = {
        Bool = {
          "aws:SecureTransport" = "false"
        }
      }
    }]
  })
}

# ALB with HTTPS listener
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS-1-2-2017-01"  # TLS 1.2+
  certificate_arn   = aws_acm_certificate.main.arn
  
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}

# Redirect HTTP to HTTPS
resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"
  
  default_action {
    type = "redirect"
    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

RDS Force SSL:

resource "aws_db_instance" "main" {
  identifier     = "mydb"
  engine         = "postgres"
  instance_class = "db.t3.medium"
  
  # PostgreSQL: require SSL
  parameter_group_name = aws_db_parameter_group.ssl_required.name
}

resource "aws_db_parameter_group" "ssl_required" {
  name   = "postgres-ssl-required"
  family = "postgres15"
  
  parameter {
    name  = "rds.force_ssl"
    value = "1"
  }
}

# MySQL: require SSL
resource "aws_db_parameter_group" "mysql_ssl" {
  name   = "mysql-ssl-required"
  family = "mysql8.0"
  
  parameter {
    name  = "require_secure_transport"
    value = "ON"
  }
}

Application TLS:

# Nginx with strong TLS configuration
server {
    listen 443 ssl http2;
    server_name example.com;
    
    ssl_certificate     /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;
    
    # TLS 1.2 and 1.3 only
    ssl_protocols TLSv1.2 TLSv1.3;
    
    # Strong ciphers
    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
    ssl_prefer_server_ciphers on;
    
    # HSTS (force HTTPS for 1 year)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    
    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    
    location / {
        proxy_pass http://backend;
        
        # Secure headers
        add_header X-Frame-Options "DENY" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    }
}

Network Security

Security Groups (Stateful Firewall)

Principle: Default deny, explicit allow

# Web tier security group
resource "aws_security_group" "web" {
  name        = "web-tier"
  description = "Allow inbound HTTPS from internet"
  vpc_id      = aws_vpc.main.id
  
  # HTTPS from internet
  ingress {
    description = "HTTPS from internet"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  # All outbound allowed (for pulling dependencies, etc.)
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  tags = {
    Name = "web-tier"
  }
}

# Application tier security group
resource "aws_security_group" "app" {
  name        = "app-tier"
  description = "Allow traffic from web tier"
  vpc_id      = aws_vpc.main.id
  
  # Only from web tier
  ingress {
    description     = "HTTP from web tier"
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.web.id]
  }
  
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  tags = {
    Name = "app-tier"
  }
}

# Database tier security group
resource "aws_security_group" "db" {
  name        = "db-tier"
  description = "Allow traffic from app tier only"
  vpc_id      = aws_vpc.main.id
  
  # PostgreSQL only from app tier
  ingress {
    description     = "PostgreSQL from app tier"
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
  }
  
  # No outbound internet access
  egress {
    description = "Allow to VPC only"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = [aws_vpc.main.cidr_block]
  }
  
  tags = {
    Name = "db-tier"
  }
}

Security Group Anti-Patterns to Avoid:

❌ SSH/RDP open to 0.0.0.0/0:

# NEVER do this
ingress {
  from_port   = 22
  to_port     = 22
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]  # Anyone on internet can try to SSH
}

✅ SSH from bastion only:

ingress {
  description     = "SSH from bastion"
  from_port       = 22
  to_port         = 22
  protocol        = "tcp"
  security_groups = [aws_security_group.bastion.id]
}

Network ACLs (Stateless Firewall)

Additional layer of defense at subnet level:

# Public subnet NACL
resource "aws_network_acl" "public" {
  vpc_id     = aws_vpc.main.id
  subnet_ids = aws_subnet.public[*].id
  
  # Allow HTTPS inbound
  ingress {
    protocol   = "tcp"
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 443
    to_port    = 443
  }
  
  # Allow HTTP inbound (for Let's Encrypt challenges)
  ingress {
    protocol   = "tcp"
    rule_no    = 110
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 80
    to_port    = 80
  }
  
  # Allow ephemeral ports inbound (for responses)
  ingress {
    protocol   = "tcp"
    rule_no    = 120
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 1024
    to_port    = 65535
  }
  
  # Allow all outbound
  egress {
    protocol   = "-1"
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }
  
  tags = {
    Name = "public-nacl"
  }
}

# Private subnet NACL (deny all inbound from internet)
resource "aws_network_acl" "private" {
  vpc_id     = aws_vpc.main.id
  subnet_ids = aws_subnet.private[*].id
  
  # Allow from VPC CIDR only
  ingress {
    protocol   = "-1"
    rule_no    = 100
    action     = "allow"
    cidr_block = aws_vpc.main.cidr_block
    from_port  = 0
    to_port    = 0
  }
  
  # Deny all other inbound
  ingress {
    protocol   = "-1"
    rule_no    = 999
    action     = "deny"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }
  
  # Allow all outbound
  egress {
    protocol   = "-1"
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }
  
  tags = {
    Name = "private-nacl"
  }
}

VPC Flow Logs

Capture all network traffic for analysis:

# Flow logs to CloudWatch
resource "aws_flow_log" "main" {
  vpc_id          = aws_vpc.main.id
  traffic_type    = "ALL"  # ALL, ACCEPT, or REJECT
  iam_role_arn    = aws_iam_role.flow_logs.arn
  log_destination = aws_cloudwatch_log_group.flow_logs.arn
}

resource "aws_cloudwatch_log_group" "flow_logs" {
  name              = "/aws/vpc/flow-logs"
  retention_in_days = 30
}

# Query flow logs with CloudWatch Insights
# Example: Find rejected connections
# fields @timestamp, srcAddr, dstAddr, dstPort, action
# | filter action = "REJECT"
# | sort @timestamp desc
# | limit 100

# Flow logs to S3 for long-term storage
resource "aws_flow_log" "s3" {
  vpc_id               = aws_vpc.main.id
  traffic_type         = "ALL"
  log_destination_type = "s3"
  log_destination      = aws_s3_bucket.flow_logs.arn
  
  destination_options {
    file_format        = "parquet"
    per_hour_partition = true
  }
}

# Query with Athena
resource "aws_athena_database" "flow_logs" {
  name   = "vpc_flow_logs"
  bucket = aws_s3_bucket.athena_results.bucket
}

resource "aws_athena_workgroup" "flow_logs" {
  name = "flow-logs-analysis"
}

Query flow logs with Athena:

-- Top talkers by bytes
SELECT srcaddr, dstaddr, SUM(bytes) as total_bytes
FROM flow_logs
WHERE date = DATE('2026-01-30')
GROUP BY srcaddr, dstaddr
ORDER BY total_bytes DESC
LIMIT 10;

-- Rejected connections (potential attacks)
SELECT srcaddr, dstport, COUNT(*) as attempts
FROM flow_logs
WHERE date = DATE('2026-01-30') AND action = 'REJECT'
GROUP BY srcaddr, dstport
HAVING attempts > 100
ORDER BY attempts DESC;

AWS WAF (Web Application Firewall)

Protect against common web exploits:

# WAF Web ACL
resource "aws_wafv2_web_acl" "main" {
  name  = "main-waf"
  scope = "REGIONAL"
  
  default_action {
    allow {}
  }
  
  # Rate limiting (prevent DDoS)
  rule {
    name     = "rate-limit"
    priority = 1
    
    action {
      block {}
    }
    
    statement {
      rate_based_statement {
        limit              = 2000
        aggregate_key_type = "IP"
      }
    }
    
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "RateLimitRule"
      sampled_requests_enabled   = true
    }
  }
  
  # SQL injection protection
  rule {
    name     = "sql-injection"
    priority = 2
    
    action {
      block {}
    }
    
    statement {
      sqli_match_statement {
        field_to_match {
          body {}
        }
        text_transformation {
          priority = 0
          type     = "URL_DECODE"
        }
      }
    }
    
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "SQLiRule"
      sampled_requests_enabled   = true
    }
  }
  
  # XSS protection
  rule {
    name     = "xss-protection"
    priority = 3
    
    action {
      block {}
    }
    
    statement {
      xss_match_statement {
        field_to_match {
          body {}
        }
        text_transformation {
          priority = 0
          type     = "URL_DECODE"
        }
      }
    }
    
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "XSSRule"
      sampled_requests_enabled   = true
    }
  }
  
  # AWS Managed Rules
  rule {
    name     = "aws-managed-rules"
    priority = 4
    
    override_action {
      none {}
    }
    
    statement {
      managed_rule_group_statement {
        vendor_name = "AWS"
        name        = "AWSManagedRulesCommonRuleSet"
      }
    }
    
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "AWSManagedRules"
      sampled_requests_enabled   = true
    }
  }
  
  # Geo-blocking (optional)
  rule {
    name     = "geo-blocking"
    priority = 5
    
    action {
      block {}
    }
    
    statement {
      geo_match_statement {
        country_codes = ["CN", "RU"]  # Block traffic from specific countries
      }
    }
    
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "GeoBlockRule"
      sampled_requests_enabled   = true
    }
  }
  
  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "MainWAF"
    sampled_requests_enabled   = true
  }
}

# Associate WAF with ALB
resource "aws_wafv2_web_acl_association" "alb" {
  resource_arn = aws_lb.main.arn
  web_acl_arn  = aws_wafv2_web_acl.main.arn
}

Threat Detection

AWS GuardDuty

Continuous threat detection service:

resource "aws_guardduty_detector" "main" {
  enable = true
  
  datasources {
    s3_logs {
      enable = true
    }
    kubernetes {
      audit_logs {
        enable = true
      }
    }
  }
}

# Send findings to SNS for alerts
resource "aws_guardduty_publishing_destination" "main" {
  detector_id     = aws_guardduty_detector.main.id
  destination_arn = aws_sns_topic.security_alerts.arn
  destination_type = "SNS"
}

resource "aws_sns_topic" "security_alerts" {
  name = "security-alerts"
}

resource "aws_sns_topic_subscription" "email" {
  topic_arn = aws_sns_topic.security_alerts.arn
  protocol  = "email"
  endpoint  = "security-team@example.com"
}

GuardDuty Finding Types:

Automated Response to GuardDuty Findings:

# Lambda function triggered by GuardDuty via EventBridge
import boto3
import json

ec2 = boto3.client('ec2')
sns = boto3.client('sns')

def lambda_handler(event, context):
    finding = event['detail']
    finding_type = finding['type']
    severity = finding['severity']
    
    # High severity findings require immediate action
    if severity >= 7:
        if 'UnauthorizedAccess:EC2/MaliciousIPCaller' in finding_type:
            # Isolate compromised instance
            instance_id = finding['resource']['instanceDetails']['instanceId']
            
            # Create forensic security group (deny all)
            forensic_sg = ec2.create_security_group(
                GroupName=f'forensic-{instance_id}',
                Description='Quarantine security group',
                VpcId=finding['resource']['instanceDetails']['networkInterfaces'][0]['vpcId']
            )
            
            # Remove all rules (default deny)
            sg_id = forensic_sg['GroupId']
            
            # Apply to instance
            ec2.modify_instance_attribute(
                InstanceId=instance_id,
                Groups=[sg_id]
            )
            
            # Snapshot volume for forensics
            volumes = ec2.describe_volumes(
                Filters=[{'Name': 'attachment.instance-id', 'Values': [instance_id]}]
            )
            for volume in volumes['Volumes']:
                ec2.create_snapshot(
                    VolumeId=volume['VolumeId'],
                    Description=f'Forensic snapshot - GuardDuty finding {finding["id"]}'
                )
            
            # Alert security team
            sns.publish(
                TopicArn='arn:aws:sns:us-east-1:123456789012:security-alerts',
                Subject=f'CRITICAL: Instance {instance_id} quarantined',
                Message=json.dumps(finding, indent=2)
            )
    
    return {'statusCode': 200}

Deploy automated response:

resource "aws_lambda_function" "guardduty_response" {
  filename      = "guardduty_response.zip"
  function_name = "guardduty-automated-response"
  role          = aws_iam_role.guardduty_response.arn
  handler       = "lambda_function.lambda_handler"
  runtime       = "python3.11"
  timeout       = 60
}

resource "aws_cloudwatch_event_rule" "guardduty" {
  name        = "guardduty-findings"
  description = "Capture GuardDuty findings"
  
  event_pattern = jsonencode({
    source      = ["aws.guardduty"]
    detail-type = ["GuardDuty Finding"]
  })
}

resource "aws_cloudwatch_event_target" "lambda" {
  rule      = aws_cloudwatch_event_rule.guardduty.name
  target_id = "GuardDutyResponse"
  arn       = aws_lambda_function.guardduty_response.arn
}

Security Hub

Centralized security findings from multiple services:

resource "aws_securityhub_account" "main" {}

# Enable standards
resource "aws_securityhub_standards_subscription" "cis" {
  standards_arn = "arn:aws:securityhub:us-east-1::standards/cis-aws-foundations-benchmark/v/1.4.0"
  depends_on    = [aws_securityhub_account.main]
}

resource "aws_securityhub_standards_subscription" "pci_dss" {
  standards_arn = "arn:aws:securityhub:us-east-1::standards/pci-dss/v/3.2.1"
  depends_on    = [aws_securityhub_account.main]
}

# Aggregate findings from GuardDuty, Inspector, Macie, etc.
# View in Console: Security Hub → Findings

Security Hub Insights:

Custom filters for common security issues:

# CLI to create insight
aws securityhub create-insight \
  --filters file://filters.json \
  --group-by-attribute "ResourceType" \
  --name "Publicly Accessible Resources"

# filters.json
{
  "ResourceType": [{"Value": "AwsS3Bucket", "Comparison": "EQUALS"}],
  "ComplianceStatus": [{"Value": "FAILED", "Comparison": "EQUALS"}],
  "RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}]
}

Secrets Management

AWS Secrets Manager

# Database credentials in Secrets Manager
resource "aws_secretsmanager_secret" "db_password" {
  name                    = "production/db/password"
  description             = "PostgreSQL database password"
  recovery_window_in_days = 30
  
  rotation_rules {
    automatically_after_days = 90
  }
}

resource "aws_secretsmanager_secret_version" "db_password" {
  secret_id     = aws_secretsmanager_secret.db_password.id
  secret_string = jsonencode({
    username = "dbadmin"
    password = random_password.db.result
    host     = aws_db_instance.main.address
    port     = 5432
    dbname   = "myapp"
  })
}

# Lambda rotation function
resource "aws_secretsmanager_secret_rotation" "db_password" {
  secret_id           = aws_secretsmanager_secret.db_password.id
  rotation_lambda_arn = aws_lambda_function.rotate_secret.arn
  
  rotation_rules {
    automatically_after_days = 90
  }
}

Application Retrieving Secret:

import boto3
import json

# Application code
secrets_client = boto3.client('secretsmanager')

def get_db_credentials():
    response = secrets_client.get_secret_value(SecretId='production/db/password')
    secret = json.loads(response['SecretString'])
    return secret

# Use credentials
creds = get_db_credentials()
conn = psycopg2.connect(
    host=creds['host'],
    port=creds['port'],
    dbname=creds['dbname'],
    user=creds['username'],
    password=creds['password']
)

Never store secrets in:

Always store secrets in:


CloudTrail Logging

Log all API calls for audit and forensics:

# Multi-region trail
resource "aws_cloudtrail" "organization" {
  name                          = "organization-trail"
  s3_bucket_name                = aws_s3_bucket.cloudtrail.id
  include_global_service_events = true
  is_multi_region_trail         = true
  is_organization_trail         = true
  enable_log_file_validation    = true
  
  # Encrypt logs
  kms_key_id = aws_kms_key.cloudtrail.arn
  
  # Log data events (S3, Lambda)
  event_selector {
    read_write_type           = "All"
    include_management_events = true
    
    data_resource {
      type   = "AWS::S3::Object"
      values = ["arn:aws:s3:::*/"]
    }
    
    data_resource {
      type   = "AWS::Lambda::Function"
      values = ["arn:aws:lambda:*:*:function/*"]
    }
  }
  
  # Send to CloudWatch Logs for real-time monitoring
  cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.cloudtrail.arn}:*"
  cloud_watch_logs_role_arn  = aws_iam_role.cloudtrail.arn
}

# S3 bucket for trail logs
resource "aws_s3_bucket" "cloudtrail" {
  bucket        = "my-organization-cloudtrail"
  force_destroy = false
}

resource "aws_s3_bucket_lifecycle_configuration" "cloudtrail" {
  bucket = aws_s3_bucket.cloudtrail.id
  
  rule {
    id     = "archive-old-logs"
    status = "Enabled"
    
    transition {
      days          = 90
      storage_class = "GLACIER"
    }
    
    expiration {
      days = 2555  # 7 years for compliance
    }
  }
}

# CloudWatch Logs for real-time alerts
resource "aws_cloudwatch_log_group" "cloudtrail" {
  name              = "/aws/cloudtrail/organization"
  retention_in_days = 90
}

# Alert on root account usage
resource "aws_cloudwatch_log_metric_filter" "root_usage" {
  name           = "RootAccountUsage"
  log_group_name = aws_cloudwatch_log_group.cloudtrail.name
  pattern        = '{ $.userIdentity.type = "Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != "AwsServiceEvent" }'
  
  metric_transformation {
    name      = "RootAccountUsageCount"
    namespace = "CloudTrail"
    value     = "1"
  }
}

resource "aws_cloudwatch_metric_alarm" "root_usage" {
  alarm_name          = "root-account-used"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "1"
  metric_name         = "RootAccountUsageCount"
  namespace           = "CloudTrail"
  period              = "60"
  statistic           = "Sum"
  threshold           = "0"
  alarm_description   = "Root account was used"
  alarm_actions       = [aws_sns_topic.security_alerts.arn]
}

# Alert on IAM policy changes
resource "aws_cloudwatch_log_metric_filter" "iam_changes" {
  name           = "IAMPolicyChanges"
  log_group_name = aws_cloudwatch_log_group.cloudtrail.name
  pattern        = '{ ($.eventName = DeleteGroupPolicy) || ($.eventName = DeleteRolePolicy) || ($.eventName = DeleteUserPolicy) || ($.eventName = PutGroupPolicy) || ($.eventName = PutRolePolicy) || ($.eventName = PutUserPolicy) || ($.eventName = CreatePolicy) || ($.eventName = DeletePolicy) || ($.eventName = CreatePolicyVersion) || ($.eventName = DeletePolicyVersion) || ($.eventName = AttachRolePolicy) || ($.eventName = DetachRolePolicy) || ($.eventName = AttachUserPolicy) || ($.eventName = DetachUserPolicy) || ($.eventName = AttachGroupPolicy) || ($.eventName = DetachGroupPolicy) }'
  
  metric_transformation {
    name      = "IAMPolicyChangesCount"
    namespace = "CloudTrail"
    value     = "1"
  }
}

Query CloudTrail with Athena:

-- Create table for CloudTrail logs
CREATE EXTERNAL TABLE cloudtrail_logs (
  eventversion STRING,
  useridentity STRUCT<
    type:STRING,
    principalid:STRING,
    arn:STRING,
    accountid:STRING,
    invokedby:STRING,
    accesskeyid:STRING,
    userName:STRING,
    sessioncontext:STRUCT<
      attributes:STRUCT<
        mfaauthenticated:STRING,
        creationdate:STRING
      >,
      sessionissuer:STRUCT<
        type:STRING,
        principalId:STRING,
        arn:STRING,
        accountId:STRING,
        userName:STRING
      >
    >
  >,
  eventtime STRING,
  eventsource STRING,
  eventname STRING,
  awsregion STRING,
  sourceipaddress STRING,
  useragent STRING,
  errorcode STRING,
  errormessage STRING,
  requestparameters STRING,
  responseelements STRING,
  additionaleventdata STRING,
  requestid STRING,
  eventid STRING,
  resources ARRAY<STRUCT<
    ARN:STRING,
    accountId:STRING,
    type:STRING
  >>,
  eventtype STRING,
  apiversion STRING,
  readonly STRING,
  recipientaccountid STRING,
  serviceeventdetails STRING,
  sharedeventid STRING,
  vpcendpointid STRING
)
ROW FORMAT SERDE 'com.amazon.emr.hive.serde.CloudTrailSerde'
STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION 's3://my-organization-cloudtrail/AWSLogs/123456789012/CloudTrail/';

-- Find all actions by specific user
SELECT eventtime, eventsource, eventname, sourceipaddress
FROM cloudtrail_logs
WHERE useridentity.username = 'john.doe'
  AND eventtime >= '2026-01-01'
ORDER BY eventtime DESC
LIMIT 100;

-- Find failed authentication attempts
SELECT eventtime, sourceipaddress, errorcode, errormessage, COUNT(*) as attempts
FROM cloudtrail_logs
WHERE errorcode IN ('UnauthorizedOperation', 'AccessDenied')
  AND eventtime >= '2026-01-30'
GROUP BY eventtime, sourceipaddress, errorcode, errormessage
HAVING attempts > 10
ORDER BY attempts DESC;

-- Track S3 bucket public access changes
SELECT eventtime, useridentity.username, requestparameters
FROM cloudtrail_logs
WHERE eventsource = 's3.amazonaws.com'
  AND eventname IN ('PutBucketAcl', 'PutBucketPolicy')
  AND eventtime >= '2026-01-01'
ORDER BY eventtime DESC;

Compliance as Code

OPA Policy Enforcement

Prevent non-compliant infrastructure before deployment:

Terraform Policy (deny public S3 buckets):

# policy/s3_public.rego
package terraform

import input as tfplan

deny[msg] {
  resource := tfplan.resource_changes[_]
  resource.type == "aws_s3_bucket_acl"
  resource.change.after.acl == "public-read"
  
  msg := sprintf("S3 bucket %s has public ACL - denied by security policy", [resource.address])
}

deny[msg] {
  resource := tfplan.resource_changes[_]
  resource.type == "aws_s3_bucket_public_access_block"
  resource.change.after.block_public_acls == false
  
  msg := sprintf("S3 bucket %s allows public ACLs - denied by security policy", [resource.address])
}

Terraform Policy (require encryption):

# policy/encryption.rego
package terraform

deny[msg] {
  resource := tfplan.resource_changes[_]
  resource.type == "aws_db_instance"
  resource.change.after.storage_encrypted != true
  
  msg := sprintf("RDS instance %s not encrypted - security policy requires encryption", [resource.address])
}

deny[msg] {
  resource := tfplan.resource_changes[_]
  resource.type == "aws_ebs_volume"
  resource.change.after.encrypted != true
  
  msg := sprintf("EBS volume %s not encrypted - security policy requires encryption", [resource.address])
}

deny[msg] {
  resource := tfplan.resource_changes[_]
  resource.type == "aws_instance"
  resource.change.after.root_block_device[_].encrypted != true
  
  msg := sprintf("EC2 instance %s root volume not encrypted", [resource.address])
}

CI/CD Integration:

# GitHub Actions workflow
name: Security Policy Check
on: [pull_request]

jobs:
  policy-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
      
      - name: Terraform Init
        run: terraform init
      
      - name: Terraform Plan
        run: terraform plan -out=tfplan.binary
      
      - name: Convert plan to JSON
        run: terraform show -json tfplan.binary > tfplan.json
      
      - name: Setup OPA
        run: |
          curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64
          chmod +x opa
      
      - name: Run Policy Check
        run: |
          ./opa eval -i tfplan.json -d policy/ "data.terraform.deny" --format pretty
          VIOLATIONS=$(./opa eval -i tfplan.json -d policy/ "data.terraform.deny" --format raw)
          if [ "$VIOLATIONS" != "[]" ]; then
            echo "Policy violations detected:"
            echo "$VIOLATIONS"
            exit 1
          fi

Kubernetes Admission Controller (Gatekeeper):

# Require resource limits
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequireresourcelimits
spec:
  crd:
    spec:
      names:
        kind: K8sRequireResourceLimits
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequireresourcelimits
        
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.resources.limits.memory
          msg := sprintf("Container %s missing memory limit", [container.name])
        }
        
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.resources.limits.cpu
          msg := sprintf("Container %s missing CPU limit", [container.name])
        }
---
# Apply constraint
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireResourceLimits
metadata:
  name: require-resource-limits
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    namespaces: ["production"]

Best Practices Summary

Security Checklist

Identity and Access:

Encryption:

Network Security:

Monitoring and Detection:

Compliance:

Data Protection:


Troubleshooting

Issue: Security Group Rule Not Working

Diagnosis:

# Check security group rules
aws ec2 describe-security-groups --group-ids sg-abc123

# Check if instance actually has security group
aws ec2 describe-instances --instance-ids i-abc123 \
  --query 'Reservations[].Instances[].SecurityGroups'

# Check NACLs (might be blocking)
aws ec2 describe-network-acls \
  --filters Name=association.subnet-id,Values=subnet-xyz789

Common Issues:

Issue: Cannot Access Secrets Manager Secret

Error: AccessDeniedException: User is not authorized

Fix: Add IAM policy:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": [
      "secretsmanager:GetSecretValue",
      "secretsmanager:DescribeSecret"
    ],
    "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:production/db/password-AbCdEf"
  }]
}

Issue: GuardDuty False Positives

Whitelist known good IPs:

resource "aws_guardduty_ipset" "trusted" {
  name        = "trusted-ips"
  detector_id = aws_guardduty_detector.main.id
  format      = "TXT"
  location    = "s3://${aws_s3_bucket.guardduty.bucket}/trusted-ips.txt"
  activate    = true
}

# trusted-ips.txt content:
# 203.0.113.0/24
# 198.51.100.0/24

What's Next?

After hardening infrastructure security:

  1. CI/CD Pipeline Security - Secure software delivery pipeline
  2. Container Security - Docker and Kubernetes hardening
  3. Zero Trust Architecture - Implement zero trust networking
  4. Incident Response - Build incident response playbooks

Additional Resources

AWS Documentation:

Tools:

Standards:


Change Log

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse