Obiz Solutions

What is AWS IAM?

IAM (Identity and Access Management) is the AWS service that controls who can do what in an AWS account. Nothing in AWS — creating an S3 bucket, invalidating a CloudFront cache, launching a server — happens without an IAM check first. This site’s deploy pipeline (see Obiz Solutions — this very site) relies on IAM to let GitHub Actions push files to S3 without ever holding a password.

Core building blocks

  • User — an identity for a person or an application, usually with long-lived credentials (password, access keys).
  • Role — an identity with no credentials of its own. Something else (a person, a service, an external system) assumes it temporarily and gets short-lived credentials back. This is what GitHub Actions uses.
  • Policy — a JSON document that says which actions are allowed or denied on which resources. Attached to users, groups, or roles.
  • Group — a set of users that share the same policies.

Anatomy of a policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
      "Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"]
    }
  ]
}

Every statement answers three questions: Effect (Allow/Deny), Action (which API calls), Resource (which specific ARNs). A policy that says “allow s3:* on *” technically works but grants far more than a deploy script needs — the practice of granting only what’s actually used is called the principle of least privilege.

Roles + OIDC: no stored AWS keys

The old way to let GitHub Actions talk to AWS was to generate a long-lived IAM user access key and paste it into a GitHub secret — a credential that never expires on its own and is a real liability if the repo (or the secret) ever leaks.

The pattern this site uses instead: an OIDC identity provider for token.actions.githubusercontent.com is registered in the AWS account, and an IAM role’s trust policy allows that provider to grant sts:AssumeRoleWithWebIdentity — but only when the request comes from this specific repo:

{
  "Effect": "Allow",
  "Principal": {
    "Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
  },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringLike": {
      "token.actions.githubusercontent.com:sub": "repo:GITHUB_ORG/REPO:ref:refs/heads/main"
    }
  }
}

Each workflow run gets a fresh, short-lived credential and nothing is ever stored. The role’s permissions policy then scopes what that credential can actually do — in this repo’s case, s3:PutObject/DeleteObject/ListBucket on one bucket and cloudfront:CreateInvalidation on one distribution (see the full setup in the project README).

Rules of thumb

  • Prefer roles over long-lived user keys, especially for anything automated (CI/CD, Lambda, EC2).
  • Scope policies to specific resource ARNs instead of * wherever possible.
  • A trust policy’s Condition block is what actually restricts who can assume a role — without it, any identity trusted by the provider could.

Further reading

Official docs: docs.aws.amazon.com/IAM