Identity and SSO
People sign in once, receive the permissions their role requires, and use short-lived credentials. Machines assume roles through OIDC. Long-lived access keys are the exception we design out.
Human access
IAM Identity Center provides single sign-on to every account in the organisation.
Permission sets
| Permission set | Typical population | Scope |
|---|---|---|
| ReadOnly | All engineering | View resources and metadata across accounts |
| Developer | Application engineers | Deploy through pipelines; read infrastructure; no IAM changes |
| PowerUser | Senior engineers | Most services; cannot manage users, groups or organisation |
| Administrator | Platform team | Full access, restricted to non-production by default |
| BreakGlass | Two named individuals | Full access to production; MFA, time-bound, alarmed |
| Billing | Finance | Billing and cost management only |
Rule. Permissions are assigned to groups, never to individuals. Joining or leaving a team changes group membership in one place, and the access footprint follows.
Production access
Direct production access is deliberately narrow:
- Routine work happens through pipelines, not interactive sessions.
- Where interactive access is required, it is through a role with a recorded session and a business reason.
- Break-glass access is time-bound, requires MFA, and raises an alarm to the security channel on assumption.
- Root credentials are not used for operational work. Root is protected with MFA and used only for the tasks that require it.
Machine access
Applications and pipelines assume IAM roles. Static access keys are not issued for new workloads.
CI/CD via OIDC
The pipeline provider's OIDC token is exchanged for a short-lived role session:
data "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
}
resource "aws_iam_role" "deploy" {
name = "github-deploy-${var.environment}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = data.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:${var.org}/${var.repo}:ref:refs/heads/${var.branch}"
}
}
}]
})
}
The condition binds the role to a specific repository and branch. A compromised token from another repository cannot assume it.
Workload roles
- EC2 and ECS tasks use instance roles and task roles, scoped to the resources the workload actually touches.
- Lambda uses execution roles, one per function, with no wildcard actions.
- Cross-account access uses role assumption with an external ID where a third party is involved.
IAM hygiene
| Practice | Rule |
|---|---|
| Least privilege | Policies grant specific actions on specific resources; * on both action and resource is prohibited |
| Roles over users | IAM users are not created for applications or humans |
| Key rotation | Existing access keys rotate on a 90-day maximum; new workloads use roles |
| Root | MFA enabled, no access keys, no daily use |
| Policy review | IAM Access Analyzer findings reviewed weekly |
| Unused permissions | Access Analyzer unused-permission findings drive policy tightening |
| Wildcards | Custom policies are checked for * in the policy pipeline |
Evidence
- Identity Center configuration in version control.
- Permission set assignments exportable per account.
- CloudTrail record of every role assumption and console sign-in.
- Access Analyzer findings and their resolution.
- MFA enforcement report.