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
Conditionblock 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