Security Hardening: Practical Steps to Lock Down Your AWS Environment

November 1, 2024 • 6 min read

Security Hardening: Practical Steps to Lock Down Your AWS Environment

Published

November 1, 2024

Reading Time

6 min

Topics

4 Tags

Security AWS IAM Compliance

What You'll Learn

This article breaks down security hardening: practical steps to lock down your aws environment into practical, actionable steps you can implement today.

Security incidents are expensive. A single breach costs an average of $4.45 million, according to IBM’s 2023 report.

Most breaches don’t require sophisticated attacks—they exploit basic misconfigurations. Public S3 buckets. Overly permissive IAM roles. Unpatched systems.

Here’s how to close those gaps.

1. IAM: Principle of Least Privilege

IAM (Identity and Access Management) is the foundation of AWS security.

Enable MFA Everywhere

Every human user needs multi-factor authentication:

# AWS CLI: Enforce MFA for all users
aws iam create-policy \
  --policy-name EnforceMFA \
  --policy-document file://enforce-mfa.json

enforce-mfa.json:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyAllExceptMFAAuth",
      "Effect": "Deny",
      "NotAction": [
        "iam:CreateVirtualMFADevice",
        "iam:EnableMFADevice",
        "iam:GetUser",
        "iam:ListMFADevices",
        "iam:ListVirtualMFADevices",
        "iam:ResyncMFADevice"
      ],
      "Resource": "*",
      "Condition": {
        "BoolIfExists": {
          "aws:MultiFactorAuthPresent": "false"
        }
      }
    }
  ]
}

Attach this policy to all users. If they don’t have MFA enabled, they can’t do anything (except set up MFA).

Use Roles, Not Access Keys

Access keys (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY) are dangerous:

  • They don’t expire automatically
  • They’re easy to leak (git commits, logs, error messages)
  • They’re hard to rotate

Instead: Use IAM roles with instance profiles or OIDC federation.

For EC2:

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

resource "aws_iam_instance_profile" "app_profile" {
  name = "app-server-profile"
  role = aws_iam_role.app_role.name
}

resource "aws_instance" "app" {
  ami                  = var.ami_id
  instance_type        = "t3.medium"
  iam_instance_profile = aws_iam_instance_profile.app_profile.name
  
  # No access keys needed!
}

The app gets temporary credentials automatically.

For GitHub Actions (OIDC):

resource "aws_iam_openid_connect_provider" "github" {
  url = "https://token.actions.githubusercontent.com"
  client_id_list = ["sts.amazonaws.com"]
  thumbprint_list = ["..."]
}

resource "aws_iam_role" "github_actions" {
  name = "github-actions-deploy"
  
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        Federated = aws_iam_openid_connect_provider.github.arn
      }
      Action = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringEquals = {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        }
        StringLike = {
          "token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:*"
        }
      }
    }]
  })
}

Now GitHub Actions uses short-lived tokens. No access keys to leak.

Audit IAM Policies Regularly

Unused permissions accumulate. Use IAM Access Analyzer to find them:

aws accessanalyzer create-analyzer \
  --analyzer-name my-account-analyzer \
  --type ACCOUNT

It identifies:

  • Unused roles and policies
  • Overly broad permissions
  • External access (resources shared outside your account)

Review quarterly and remove unused permissions.

2. Network Security

VPC Configuration

Never put databases in public subnets.

# Terraform: Secure multi-tier VPC
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
}

# Public subnets (ALB, NAT gateways)
resource "aws_subnet" "public" {
  count             = 3
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 4, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true
}

# Private subnets (app servers, databases)
resource "aws_subnet" "private" {
  count             = 3
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 4, count.index + 3)
  availability_zone = data.aws_availability_zones.available.names[count.index]
}

Databases in private subnets can’t be reached from the internet. App servers in private subnets use NAT gateways for outbound access only.

Security Groups: Default Deny

Start with zero access, then add what’s needed:

resource "aws_security_group" "app" {
  name_prefix = "app-"
  vpc_id      = aws_vpc.main.id
  
  # Allow inbound from load balancer only
  ingress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }
  
  # Allow outbound to database
  egress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.db.id]
  }
  
  # Allow outbound HTTPS for API calls
  egress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Don’t use 0.0.0.0/0 for ingress unless absolutely necessary (and even then, think twice).

Enable VPC Flow Logs

Capture all network traffic for forensics:

resource "aws_flow_log" "main" {
  log_destination = aws_s3_bucket.flow_logs.arn
  log_destination_type = "s3"
  traffic_type = "ALL"
  vpc_id = aws_vpc.main.id
}

When something goes wrong, flow logs show what connected to what.

3. Data Protection

Encrypt Everything at Rest

S3:

resource "aws_s3_bucket" "data" {
  bucket = "my-secure-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.data.arn
    }
  }
}

# Block public access
resource "aws_s3_bucket_public_access_block" "data" {
  bucket = aws_s3_bucket.data.id
  
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

RDS:

resource "aws_db_instance" "main" {
  identifier = "myapp-db"
  engine     = "postgres"
  
  storage_encrypted = true
  kms_key_id        = aws_kms_key.rds.arn
  
  # Snapshots are encrypted too
}

Use Secrets Manager or Parameter Store

Never hardcode secrets:

resource "aws_secretsmanager_secret" "db_password" {
  name = "prod/db/master-password"
}

resource "aws_secretsmanager_secret_version" "db_password" {
  secret_id     = aws_secretsmanager_secret.db_password.id
  secret_string = jsonencode({
    username = "admin"
    password = random_password.db.result
  })
}

# App reads from Secrets Manager
# Credentials rotate automatically

Enable automatic rotation for database credentials.

4. Logging and Monitoring

Enable CloudTrail

CloudTrail logs all API calls—essential for auditing and forensics:

resource "aws_cloudtrail" "main" {
  name                          = "main-trail"
  s3_bucket_name                = aws_s3_bucket.cloudtrail.id
  include_global_service_events = true
  is_multi_region_trail         = true
  enable_log_file_validation    = true
}

Log file validation ensures logs haven’t been tampered with.

Set Up AWS Config

AWS Config tracks configuration changes:

resource "aws_config_configuration_recorder" "main" {
  name     = "main-recorder"
  role_arn = aws_iam_role.config.arn
  
  recording_group {
    all_supported = true
  }
}

You can write rules like:

  • “Alert if any S3 bucket becomes public”
  • “Alert if MFA is disabled on root account”
  • “Alert if security groups allow 0.0.0.0/0 on SSH”

GuardDuty for Threat Detection

Enable GuardDuty—it analyzes CloudTrail, VPC Flow Logs, and DNS logs to detect threats:

aws guardduty create-detector --enable

It alerts on things like:

  • EC2 instances communicating with known malicious IPs
  • Unusual API calls (e.g., someone trying to exfiltrate data)
  • Compromised credentials

5. Patch Management

Unpatched systems are low-hanging fruit for attackers.

Use AWS Systems Manager

Automate patching:

resource "aws_ssm_maintenance_window" "patching" {
  name     = "patching-window"
  schedule = "cron(0 2 ? * SUN *)"  # 2 AM every Sunday
  duration = 3
  cutoff   = 1
}

resource "aws_ssm_maintenance_window_task" "patch" {
  window_id        = aws_ssm_maintenance_window.patching.id
  task_type        = "RUN_COMMAND"
  task_arn         = "AWS-RunPatchBaseline"
  priority         = 1
  service_role_arn = aws_iam_role.ssm.arn
  
  targets {
    key    = "tag:Environment"
    values = ["production"]
  }
}

Patches are applied weekly. Automatically.

6. Compliance Frameworks

SOC 2 / ISO 27001 Readiness

If you’re pursuing compliance:

Must-haves:

  • Encryption at rest and in transit
  • MFA for all human access
  • Audit logging (CloudTrail, VPC Flow Logs, application logs)
  • Access reviews (quarterly)
  • Incident response plan
  • Disaster recovery plan

Use tools like AWS Security Hub or Prowler to audit your setup against compliance frameworks.

Quick Wins

If you do nothing else, do these:

  1. Enable MFA for all users
  2. Block public access on all S3 buckets
  3. Enable CloudTrail
  4. Use IAM roles instead of access keys
  5. Encrypt everything (RDS, S3, EBS)

These five changes prevent 80% of common security issues.

The Bottom Line

Security isn’t a one-time project—it’s ongoing. But starting with solid fundamentals makes a massive difference.

Review your security posture quarterly. Threats evolve; so should your defenses.

Questions About This?

We implement these strategies for clients every day. Want to discuss how they apply to your infrastructure?

Let's Talk

Need Help Implementing This?

Let's talk. We'll figure out how to apply these concepts to your infrastructure.

Book a Free Call