Security in AWS isn’t a feature you add later — it’s woven into every decision from day one. This page covers identity (IAM), data protection (KMS), threat detection (Security Hub, GuardDuty), and edge protection (WAF), building from foundational principles up to automated, continuous compliance.

The Problem Security Solves

In an on-premises data center, a locked door and a firewall went a long way: an attacker had to physically reach the building or breach the network perimeter. In the cloud, every resource has a public API endpoint, and a single leaked access key or an over-permissive policy can expose your entire account from anywhere on the internet. Security in AWS is therefore less about a perimeter and more about who can do what to which resource — and proving it.

The good news: AWS gives you fine-grained controls for exactly this, and most of them cost nothing to turn on. The hard part is knowing which control addresses which risk. This guide is organized around four questions:

Question Risk it addresses Primary services
Who can act, and on what? Stolen credentials, privilege escalation IAM, IAM Identity Center, Organizations
Is the data readable if stolen? Data exfiltration, lost disks KMS, encryption defaults
Are we being attacked right now? Active intrusion, misconfiguration GuardDuty, Security Hub, Config
Can attacker traffic even reach us? DDoS, injection, scraping WAF, Shield, Security Groups

Where the Line Is Drawn: Shared Responsibility

AWS secures the cloud (physical data centers, hypervisors, the network backbone). You secure what is in the cloud (identities, configuration, data, application code). Almost every breach you read about lives on the customer side of that line — a public S3 bucket, a hardcoded key, an 0.0.0.0/0 rule. Internalizing this division tells you where to spend your effort.

The Security Maturity Path

Most teams progress through these stages. You do not need to reach the end on day one, but you should know what each stage protects against:

  1. Basic protection: MFA on the root account, individual IAM users/roles, no long-lived keys in code.
  2. Defense in depth: Private subnets, encryption at rest and in transit, CloudTrail logging on.
  3. Automated compliance: Config rules and Security Hub flag drift; GuardDuty watches for active threats.
  4. Zero trust: Assume breach. Verify every request, scope every credential, segment every network.

IAM: Who Can Do What

IAM (Identity and Access Management) is the foundation everything else rests on. Get IAM wrong and no amount of encryption or threat detection will save you. Get it right and most attacks have nowhere to go.

The Four IAM Building Blocks

Concept What it is When to use it
User A long-lived identity for a human or legacy app Rare today — prefer Identity Center for humans
Group A collection of users sharing permissions Apply policies to many users at once (e.g. Developers)
Role A temporary identity that anything can assume The default for applications, EC2, Lambda, cross-account access
Policy A JSON document granting or denying actions Attached to users, groups, or roles to define permissions

The single most important rule: prefer roles over users with access keys. A role hands out short-lived, automatically-rotated credentials; a user’s access key sits in a config file or environment variable indefinitely, waiting to leak. EC2 instances, Lambda functions, and CI/CD pipelines should all assume roles, never carry static keys.

Anatomy of an IAM Policy

Every permission decision comes down to a policy document. Read this one as “allow reading objects from a specific bucket, but only over TLS”:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "ReadAppBucketOverTLS",
    "Effect": "Allow",
    "Action": ["s3:GetObject"],
    "Resource": "arn:aws:s3:::my-app-bucket/*",
    "Condition": {
      "Bool": { "aws:SecureTransport": "true" }
    }
  }]
}
  • Effect: Allow or Deny. An explicit Deny always wins over any Allow.
  • Action: The API calls being permitted (service:Operation). Resist s3:*.
  • Resource: The exact ARNs the actions apply to. Resist "Resource": "*".
  • Condition: Extra constraints — source IP, MFA present, encryption in transit, etc.

Least Privilege in Practice

Least privilege means granting only the permissions actually needed, then widening only when something breaks. The progression below shows the journey teams typically take:

Stage What it looks like Why it is risky / better
Bad Every developer has AdministratorAccess One leaked key compromises everything
Better A shared PowerUserAccess role (no IAM rights) Limits self-escalation, but still broad
Best Per-team policies scoped to specific services and resources Blast radius of any one credential stays small

Tighten policies with real usage data

Do not guess at permissions. IAM Access Analyzer can generate a least-privilege policy from the actual CloudTrail history of what a role used. Start broad in dev, then let the data tell you what to remove before production.

Common IAM Pitfalls

Common Pitfalls

  • Using the root account for daily work: The root user can do anything, including closing the account. Lock it behind MFA, create an admin role, and never use root again except for the handful of tasks that require it.
  • Long-lived access keys in code: Keys committed to git or baked into AMIs are the #1 source of breaches. Use roles; if you must use keys, rotate them and scan repos with tools like git-secrets.
  • Wildcards everywhere: "Action": "*" on "Resource": "*" is administrator access by another name. Scope both.
  • Forgetting MFA: A password alone is one phished email away from compromise. Enforce MFA on all human identities via an IAM policy condition.

Encryption: Unreadable If Stolen

Encryption is your fallback for when a control fails — if a disk, snapshot, or bucket leaks, encrypted data is just noise without the key. AWS makes this nearly free to enable, so the only wrong choice is leaving it off.

Layer What to enable How
At rest Default encryption on S3, EBS, RDS, DynamoDB Toggle in console/IaC; transparent to your app
In transit TLS for every connection Enforce with the aws:SecureTransport condition shown above
Key management KMS keys with automatic rotation Use AWS-managed keys to start; customer-managed keys when you need control over policy and rotation

Why KMS instead of managing keys yourself? KMS handles generation, storage, rotation, and access logging of keys, and integrates directly with S3/EBS/RDS so encryption is transparent. Every use of a key is recorded in CloudTrail, giving you an audit trail of who decrypted what.

Real-world scenario: A healthcare startup turns on default encryption and KMS rotation from day one. When the HIPAA audit arrives, encryption-at-rest is already in place across every store — turning what is often months of remediation into a checkbox.


Detection: Are We Being Attacked?

Prevention is never perfect, so you also need to see what is happening. Three services cover the spectrum, and they complement rather than replace each other:

Service What it answers What it watches
GuardDuty “Is something malicious happening?” CloudTrail, VPC flow, DNS logs analyzed by ML for threats
Config “Did a resource drift from policy?” Resource configuration changes against rules (e.g. “no public buckets”)
Security Hub “What is my overall posture?” Aggregates GuardDuty + Config + standards (CIS, PCI-DSS) into one score

Think of it as layers: Config catches misconfiguration, GuardDuty catches active intrusion, and Security Hub is the dashboard that rolls both up against a compliance benchmark so you get a single prioritized findings list instead of scattered alarms.

Security Hub: Your Compliance Command Center

Security Hub continuously evaluates your environment against industry standards (CIS, PCI-DSS, AWS Foundational Security Best Practices) and produces a real-time compliance score, so manual quarterly security reviews become continuous automated ones. The Terraform below shows the core wiring — enabling Security Hub, subscribing to standards, and routing high-severity findings to an automated remediation Lambda. The full configuration (GuardDuty, Macie, Inspector, Config rules, WAF) follows; treat it as a reference you adapt, not a block to copy wholesale.

# security-hub.tf - Security Hub configuration and custom checks

# Enable Security Hub
resource "aws_securityhub_account" "main" {
  depends_on = [
    aws_organizations_organization.main
  ]
}

# Enable security standards
resource "aws_securityhub_standards_subscription" "cis" {
  standards_arn = "arn:aws:securityhub:${var.aws_region}::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:${var.aws_region}::standards/pci-dss/v/3.2.1"
  
  depends_on = [aws_securityhub_account.main]
}

resource "aws_securityhub_standards_subscription" "aws_foundational" {
  standards_arn = "arn:aws:securityhub:${var.aws_region}::standards/aws-foundational-security-best-practices/v/1.0.0"
  
  depends_on = [aws_securityhub_account.main]
}

# Custom Security Hub action
resource "aws_securityhub_action_target" "remediate" {
  name        = "Remediate"
  identifier  = "Remediate"
  description = "Trigger automated remediation"
  
  depends_on = [aws_securityhub_account.main]
}

# Lambda for custom security checks
resource "aws_lambda_function" "security_checker" {
  filename         = "security_checker.zip"
  function_name    = "${var.environment}-custom-security-checks"
  role            = aws_iam_role.security_checker.arn
  handler         = "index.handler"
  runtime         = "python3.12"
  timeout         = 900
  memory_size     = 3008
  
  environment {
    variables = {
      SECURITY_HUB_PRODUCT_ARN = "arn:aws:securityhub:${var.aws_region}:${data.aws_caller_identity.current.account_id}:product/${data.aws_caller_identity.current.account_id}/default"
      ENVIRONMENT              = var.environment
    }
  }
  
  vpc_config {
    subnet_ids         = var.private_subnet_ids
    security_group_ids = [aws_security_group.lambda.id]
  }
}

# EventBridge rule to trigger security checks
resource "aws_cloudwatch_event_rule" "security_checks" {
  name                = "${var.environment}-security-checks"
  description         = "Trigger custom security checks"
  schedule_expression = "rate(6 hours)"
}

resource "aws_cloudwatch_event_target" "security_checker" {
  rule      = aws_cloudwatch_event_rule.security_checks.name
  target_id = "SecurityChecker"
  arn       = aws_lambda_function.security_checker.arn
}

# Config Rules for compliance
resource "aws_config_config_rule" "encrypted_volumes" {
  name = "${var.environment}-encrypted-volumes"
  
  source {
    owner             = "AWS"
    source_identifier = "ENCRYPTED_VOLUMES"
  }
  
  depends_on = [aws_config_configuration_recorder.main]
}

resource "aws_config_config_rule" "restricted_ssh" {
  name = "${var.environment}-restricted-ssh"
  
  source {
    owner             = "AWS"
    source_identifier = "INCOMING_SSH_DISABLED"
  }
  
  depends_on = [aws_config_configuration_recorder.main]
}

resource "aws_config_config_rule" "s3_bucket_encryption" {
  name = "${var.environment}-s3-bucket-encryption"
  
  source {
    owner             = "AWS"
    source_identifier = "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"
  }
  
  depends_on = [aws_config_configuration_recorder.main]
}

# Custom Config rule with Lambda
resource "aws_config_config_rule" "custom_security_check" {
  name = "${var.environment}-custom-security-check"
  
  source {
    owner             = "LAMBDA"
    source_identifier = aws_lambda_function.config_rule_evaluator.arn
    
    source_detail {
      message_type = "ConfigurationItemChangeNotification"
    }
    
    source_detail {
      message_type = "OversizedConfigurationItemChangeNotification"
    }
  }
  
  depends_on = [aws_config_configuration_recorder.main]
}

# GuardDuty configuration
resource "aws_guardduty_detector" "main" {
  enable                       = true
  finding_publishing_frequency = "FIFTEEN_MINUTES"
  
  datasources {
    s3_logs {
      enable = true
    }
    kubernetes {
      audit_logs {
        enable = true
      }
    }
    malware_protection {
      scan_ec2_instance_with_findings {
        ebs_volumes {
          enable = true
        }
      }
    }
  }
}

# GuardDuty threat intelligence sets
resource "aws_s3_bucket" "threat_intel" {
  bucket = "${var.environment}-threat-intel-${data.aws_caller_identity.current.account_id}"
}

resource "aws_s3_bucket_versioning" "threat_intel" {
  bucket = aws_s3_bucket.threat_intel.id
  
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_object" "threat_list" {
  bucket  = aws_s3_bucket.threat_intel.id
  key     = "threat-lists/malicious-ips.txt"
  content = file("${path.module}/threat-lists/malicious-ips.txt")
  etag    = filemd5("${path.module}/threat-lists/malicious-ips.txt")
}

resource "aws_guardduty_threatintelset" "malicious_ips" {
  activate    = true
  detector_id = aws_guardduty_detector.main.id
  format      = "TXT"
  location    = "s3://${aws_s3_bucket.threat_intel.id}/${aws_s3_object.threat_list.key}"
  name        = "malicious-ips"
  
  depends_on = [aws_s3_object.threat_list]
}

# GuardDuty member accounts
resource "aws_guardduty_member" "member_accounts" {
  for_each = var.member_accounts
  
  account_id                 = each.value.account_id
  detector_id                = aws_guardduty_detector.main.id
  email                      = each.value.email
  invite                     = true
  invitation_message         = "You are invited to join GuardDuty"
  disable_email_notification = false
}

# Inspector v2
resource "aws_inspector2_enabler" "main" {
  account_ids    = [data.aws_caller_identity.current.account_id]
  resource_types = ["EC2", "ECR", "LAMBDA"]
}

# Macie for S3 data protection
resource "aws_macie2_account" "main" {
  finding_publishing_frequency = "FIFTEEN_MINUTES"
  status                       = "ENABLED"
}

resource "aws_macie2_classification_job" "s3_scan" {
  job_type = "ONE_TIME"
  name     = "${var.environment}-s3-sensitive-data-scan"
  
  s3_job_definition {
    bucket_definitions {
      account_id = data.aws_caller_identity.current.account_id
      buckets    = [aws_s3_bucket.data.id]
    }
  }
  
  depends_on = [aws_macie2_account.main]
}

# Access Analyzer
resource "aws_accessanalyzer_analyzer" "main" {
  analyzer_name = "${var.environment}-access-analyzer"
  type          = "ACCOUNT"  # or "ORGANIZATION"
  
  tags = {
    Environment = var.environment
  }
}

# Systems Manager compliance
resource "aws_ssm_association" "patch_baseline" {
  name = "AWS-RunPatchBaseline"
  
  targets {
    key    = "tag:Environment"
    values = [var.environment]
  }
  
  schedule_expression = "cron(0 2 ? * SUN *)"
  
  parameters = {
    Operation    = "Install"
    RebootOption = "RebootIfNeeded"
  }
}

# KMS key policies for security
data "aws_iam_policy_document" "kms_key_policy" {
  statement {
    sid    = "Enable IAM User Permissions"
    effect = "Allow"
    
    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"]
    }
    
    actions   = ["kms:*"]
    resources = ["*"]
  }
  
  statement {
    sid    = "Allow use of the key for encryption"
    effect = "Allow"
    
    principals {
      type        = "Service"
      identifiers = [
        "logs.${var.aws_region}.amazonaws.com",
        "s3.amazonaws.com",
        "rds.amazonaws.com"
      ]
    }
    
    actions = [
      "kms:Decrypt",
      "kms:GenerateDataKey",
      "kms:CreateGrant",
      "kms:DescribeKey"
    ]
    
    resources = ["*"]
    
    condition {
      test     = "StringEquals"
      variable = "kms:ViaService"
      values = [
        "s3.${var.aws_region}.amazonaws.com",
        "rds.${var.aws_region}.amazonaws.com"
      ]
    }
  }
}

resource "aws_kms_key" "main" {
  description             = "${var.environment} master key"
  deletion_window_in_days = 30
  enable_key_rotation     = true
  policy                  = data.aws_iam_policy_document.kms_key_policy.json
  
  tags = {
    Environment = var.environment
  }
}

# Security automation with EventBridge and Lambda
resource "aws_cloudwatch_event_rule" "security_findings" {
  name        = "${var.environment}-security-findings"
  description = "Capture Security Hub findings for automated remediation"
  
  event_pattern = jsonencode({
    source      = ["aws.securityhub"]
    detail-type = ["Security Hub Findings - Imported"]
    detail = {
      findings = {
        Severity = {
          Label = ["CRITICAL", "HIGH"]
        }
        Workflow = {
          Status = ["NEW"]
        }
      }
    }
  })
}

resource "aws_cloudwatch_event_target" "remediation" {
  rule      = aws_cloudwatch_event_rule.security_findings.name
  target_id = "RemediationFunction"
  arn       = aws_lambda_function.auto_remediation.arn
}

resource "aws_lambda_function" "auto_remediation" {
  filename         = "auto_remediation.zip"
  function_name    = "${var.environment}-security-auto-remediation"
  role            = aws_iam_role.remediation.arn
  handler         = "index.handler"
  runtime         = "python3.12"
  timeout         = 300
  
  environment {
    variables = {
      ENVIRONMENT = var.environment
    }
  }
}

# WAF rules for application protection
resource "aws_wafv2_web_acl" "main" {
  name  = "${var.environment}-waf-acl"
  scope = "REGIONAL"  # or "CLOUDFRONT"
  
  default_action {
    allow {}
  }
  
  rule {
    name     = "RateLimitRule"
    priority = 1
    
    statement {
      rate_based_statement {
        limit              = 2000
        aggregate_key_type = "IP"
      }
    }
    
    action {
      block {}
    }
    
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "RateLimitRule"
      sampled_requests_enabled   = true
    }
  }
  
  rule {
    name     = "ManagedRuleGroup"
    priority = 2
    
    override_action {
      none {}
    }
    
    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesKnownBadInputsRuleSet"
        vendor_name = "AWS"
      }
    }
    
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "ManagedRuleGroup"
      sampled_requests_enabled   = true
    }
  }
  
  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "${var.environment}-waf-acl"
    sampled_requests_enabled   = true
  }
  
  tags = {
    Environment = var.environment
  }
}

Edge Protection: Can Attacker Traffic Reach Us?

The final layer filters traffic before it touches your application. The aws_wafv2_web_acl above sits in front of CloudFront or an ALB and drops bad requests at the edge:

Control Protects against Where it sits
WAF SQL injection, XSS, bot scraping, request floods CloudFront / ALB / API Gateway
Shield Standard Common network/transport DDoS (free, always on) Edge, automatic
Shield Advanced Large/sophisticated DDoS, with cost protection Edge, paid
Security Groups Unwanted inbound/outbound to a resource Per ENI (stateful firewall)

The WAF rules above combine a rate-based rule (block any IP exceeding 2,000 requests in the evaluation window — a cheap defense against scraping and brute force) with an AWS-managed rule group (KnownBadInputs) that catches common exploit payloads without you maintaining signatures. Start with managed rule groups; write custom rules only for application-specific patterns.

How the Layers Fit Together

Security is not one control but a stack — a request must clear every layer, and a breach of one is contained by the next:

flowchart TB
    Req([Incoming request]) --> WAF["WAF + Shield<br/>filter malicious traffic"]
    WAF --> SG["Security Group<br/>allow only expected ports"]
    SG --> App["Application / EC2 / Lambda"]
    App --> IAM["IAM role<br/>scoped, temporary credentials"]
    IAM --> Data["Encrypted data<br/>KMS keys, TLS in transit"]
    GD["GuardDuty + Config + Security Hub"] -. "watch every layer" .-> WAF
    GD -. .-> App
    GD -. .-> Data

If WAF misses something, the security group still limits exposure. If a credential leaks, IAM scoping limits the blast radius. If data is exfiltrated, encryption renders it useless. And throughout, the detection services watch every layer for anomalies.

Key Takeaways

  • Least privilege always. Grant the minimum permissions each user or service needs. Prefer scoped custom policies and IAM roles over broad managed policies like AdministratorAccess.
  • Encrypt everywhere. Enable encryption at rest (S3, EBS, RDS) and in transit (TLS). Let KMS handle key management and rotation.
  • Shared responsibility. AWS secures the cloud; you secure what’s in it — access control, configuration, and data. Know which side of the line each control sits on.
  • Detect and automate. Turn on GuardDuty and Security Hub for continuous threat detection, and automate remediation so misconfigurations are caught fast.

See Also