S5 Sample · IAM hardening

← Samples

IAM hardening checklist.

Severity-tagged checklist we run during security engagements. AWS and GCP variants inline. Copy what applies, skip what does not.

Template - copy freely

How to use this

Each item is tagged with a severity (Critical, High, Medium, Low). Fix Critical and High within one sprint. Fix Medium within a quarter. Low is backlog unless you find a specific reason to prioritize.

Inline Terraform snippets are illustrative. Adapt to your module structure. See the field manual for the reasoning behind each item.

1. Root and organization hygiene

  • [C] AWS root account has MFA enabled with a hardware key. No access keys on root.
  • [C] Root account email is a shared mailbox with 2+ owners, not one individual.
  • [H] AWS Organizations enabled. Production is in a separate account from dev and sandbox.
  • [H] SCPs (Service Control Policies) deny dangerous actions at the org level: disabling CloudTrail, leaving known regions.
  • [M] Account-level contact info, security contact, and billing contact all set.
  • [L] AWS Control Tower (or equivalent) managing account baselines.

2. Human identity

  • [C] No IAM users with static access keys for humans. Use Identity Center (SSO) only.
  • [C] MFA required for console access. Hardware-backed where possible for privileged roles.
  • [H] Permission sets are named by role, not by person. “DeveloperAccess” not “Priya’s stuff.”
  • [H] Offboarding playbook removes all access within 1 hour of termination. Documented, tested.
  • [M] Session duration capped at 8 hours for normal roles, 1 hour for admin.
  • [M] Quarterly access review: confirm each permission set still maps to someone who needs it.

3. Workload identity

  • [C] No static AWS access keys stored in CI systems. Use OIDC federation.
  • [C] No static AWS access keys in .env files, config files, or code.
  • [H] All workloads use IAM roles via instance profile, Fargate task role, EKS IRSA, or Lambda execution role.
  • [H] Each workload has its own IAM role with least-privilege policy. No shared “app” role.
  • [M] Role trust policies use narrowest principal possible.

Terraform: GitHub OIDC provider and deploy role (AWS)

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

data "aws_iam_policy_document" "github_deploy_trust" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]
    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github.arn]
    }
    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }
    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values   = ["repo:yourorg/yourrepo:ref:refs/heads/main"]
    }
  }
}

resource "aws_iam_role" "github_deploy" {
  name               = "github-deploy"
  assume_role_policy = data.aws_iam_policy_document.github_deploy_trust.json
  max_session_duration = 3600
}

# Attach a least-privilege policy - do NOT use AdministratorAccess.
resource "aws_iam_role_policy_attachment" "github_deploy" {
  role       = aws_iam_role.github_deploy.name
  policy_arn = aws_iam_policy.deploy_policy.arn
}

4. Least-privilege policies

  • [C] No IAM entity has *:*. No entity has AdministratorAccess as a baseline.
  • [H] Policies enumerate specific actions (s3:GetObject, not s3:*).
  • [H] Resource scopes narrow wherever possible (specific bucket ARNs, not *).
  • [H] AWS IAM Access Analyzer enabled. Findings reviewed weekly.
  • [M] Permissions boundaries applied on roles developers can create.
  • [M] iam:PassRole limited to specific target roles, not *.

Terraform: example least-privilege deploy policy

data "aws_iam_policy_document" "deploy" {
  statement {
    sid     = "EcsDeploy"
    effect  = "Allow"
    actions = [
      "ecs:UpdateService",
      "ecs:DescribeServices",
      "ecs:DescribeTaskDefinition",
      "ecs:RegisterTaskDefinition",
    ]
    resources = ["arn:aws:ecs:us-east-1:${var.account_id}:service/prod-cluster/app"]
  }

  statement {
    sid     = "EcrPush"
    effect  = "Allow"
    actions = [
      "ecr:GetAuthorizationToken",
      "ecr:BatchCheckLayerAvailability",
      "ecr:InitiateLayerUpload",
      "ecr:UploadLayerPart",
      "ecr:CompleteLayerUpload",
      "ecr:PutImage",
    ]
    resources = ["arn:aws:ecr:us-east-1:${var.account_id}:repository/app"]
  }

  statement {
    sid     = "PassTaskRole"
    effect  = "Allow"
    actions = ["iam:PassRole"]
    resources = [aws_iam_role.ecs_task.arn]
  }
}

resource "aws_iam_policy" "deploy_policy" {
  name   = "github-deploy-policy"
  policy = data.aws_iam_policy_document.deploy.json
}

5. Drift and review

  • [H] All IAM resources managed by IaC (Terraform, CloudFormation, Pulumi). No click-ops in prod.
  • [H] CloudTrail enabled in every region. Logs stored in separate, write-only account.
  • [H] Alert on creation of any new IAM user, any new access key, any modification to privileged policies.
  • [M] Quarterly access review of all roles and policies. Remove stale.
  • [M] Annual IAM audit (internal or external).

6. GCP variants (condensed)

  • [C] Organization admin role members < 3. MFA required.
  • [C] No service account keys for workloads. Use workload identity federation.
  • [H] Cloud Audit Logs enabled for all services. Retention >= 400 days.
  • [H] IAM Recommender findings reviewed monthly. Over-privileged roles rightsized.
  • [H] Folders separate prod from non-prod. Deny policies at org level for dangerous actions.

Terraform: GitHub OIDC workload identity (GCP)

resource "google_iam_workload_identity_pool" "github" {
  workload_identity_pool_id = "github-pool"
}

resource "google_iam_workload_identity_pool_provider" "github" {
  workload_identity_pool_id          = google_iam_workload_identity_pool.github.workload_identity_pool_id
  workload_identity_pool_provider_id = "github-provider"
  attribute_mapping = {
    "google.subject"       = "assertion.sub"
    "attribute.repository" = "assertion.repository"
  }
  attribute_condition = "assertion.repository == 'yourorg/yourrepo'"
  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }
}

resource "google_service_account" "github_deploy" {
  account_id   = "github-deploy"
  display_name = "GitHub Actions deploy"
}

resource "google_service_account_iam_binding" "github_impersonate" {
  service_account_id = google_service_account.github_deploy.name
  role               = "roles/iam.workloadIdentityUser"
  members            = [
    "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/attribute.repository/yourorg/yourrepo",
  ]
}

7. Things we will not check off

Some items often appear on IAM checklists that we do not include because they are not worth the cost for most SMB/mid-market companies:

  • Custom HSM for IAM signing. AWS KMS is sufficient for everyone below regulated industries.
  • Cross-account assume-role chains beyond 2 hops. Operational complexity exceeds marginal security gain.
  • Hand-rolled permission-as-code engines. Terraform + Access Analyzer is enough.

8. When done

A team that has checked off everything Critical and High in this list has closed the majority of IAM-related attack paths at a Series A / B stage. Medium items close the rest. Low items are polish. See the field manual for full reasoning on every item.

Related sampleReport

Pentest report example

Where this checklist’s absence shows up - F-02 is a failure against item 3.

Related serviceEngagement

Cybersecurity

Audits, threat models, IAM work.