IAM: principals, groups and policies
IAM (Identity and Access Management) answers one question for every API call: may this principal perform this action on this resource, right now? This lesson is the vocabulary: who can be a principal, where permissions are written down, and the policy language itself. The next lesson is how AWS combines all of it into a decision.
Need to know: a principal is who makes the request: the root user, an IAM user, a role session, a federated user, or an AWS service. Permissions live in policies - JSON documents with statements of Effect, Action, Resource and an optional Condition. Identity-based policies are attached to users, groups and roles; resource-based policies (bucket policies, KMS key policies, role trust policies) are attached to the resource and name a Principal. Groups collect users but are not principals. Everything is denied unless some policy allows it.
Who can make a request
| Principal | Credentials | Use it for |
|---|---|---|
| Root user | the account's email + password (+ MFA) | almost nothing: billing, closing the account, a handful of root-only tasks |
| IAM user | a console password and/or up to 2 access keys (long-term) | legacy, break-glass, the odd system that cannot use roles |
| IAM role | none of its own: whoever assumes it gets temporary credentials | services, CI, cross-account access, and people (through IAM Identity Center) |
| Federated user | your IdP's sign-in, turned into a role session | people |
| AWS service | the service itself (s3.amazonaws.com, ec2.amazonaws.com) | named in resource policies and trust policies |
An IAM group is a set of users with policies attached; every member gets them. Groups cannot contain other groups, and a group is not a principal: you cannot name it in a bucket policy or trust policy. Roles are the AWS way to give a workload an identity - the next-but-one lesson is about them.
$ aws iam get-group --group-name platform-admins --query '{Group: Group.GroupName, Users: Users[].UserName}'
{
"Group": "platform-admins",
"Users": [
"learner"
]
}
$ aws iam list-attached-group-policies --group-name platform-admins
{
"AttachedPolicies": [
{
"PolicyName": "AdministratorAccess",
"PolicyArn": "arn:aws:iam::aws:policy/AdministratorAccess"
}
]
}
Where permissions are written
Identity-based policies are attached to a user, group or role and say what it may do. They come in three kinds:
- AWS managed policies: written and updated by AWS (
AdministratorAccess,ReadOnlyAccess,AmazonS3ReadOnlyAccess...), ARNarn:aws:iam::aws:policy/<name>. Broad, convenient, and they change when AWS adds services - which makes them a poor fit for least privilege. - Customer managed policies: yours, ARN
arn:aws:iam::<account>:policy/<name>, reusable, versioned (up to 5 versions, one of them the default - the one that applies). - Inline policies: embedded in one user, group or role, deleted with it, no ARN, no versions. For a permission that must never be reused elsewhere.
Resource-based policies are attached to a resource and include a Principal: S3 bucket policies, KMS key policies, the trust policy of every role, SNS/SQS/Lambda policies. They are how one account grants access to another.
$ aws iam get-policy --policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess --query 'Policy.[DefaultVersionId,AttachmentCount,UpdateDate]' --output text
v120 1 2026-09-17T15:02:11+00:00
$ aws iam get-policy-version --policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess --version-id v120 --query 'PolicyVersion.Document.Statement[0].Action[:6]'
[
"iam:Get*",
"iam:List*",
"iam:Generate*",
"s3:Get*",
"s3:List*",
"s3:Describe*"
]
get-policy returns the metadata, never the document. The document is a version: get-policy-version with the DefaultVersionId. (AWS's real ReadOnlyAccess lists thousands of actions; the lab's is trimmed to the services in the course.)
The policy language
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadDailyReports",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::try-reports-111122223333",
"arn:aws:s3:::try-reports-111122223333/daily/*"
],
"Condition": {
"IpAddress": {"aws:SourceIp": "198.51.100.0/24"}
}
}
]
}
- Version
2012-10-17- always. It is the language version, not a date you update, and policy variables such as${aws:username}only work in it. - Statement: one object or a list. Sid is an optional label.
- Effect:
AlloworDeny. - Action:
service:Operation, case-insensitive, with wildcards (s3:Get*,iam:*,*). - Resource: ARNs, case-sensitive, with
*and?. Notice the two ARNs:ListBucketacts on the bucket,GetObjecton objects (bucket/*). Grantings3:ListBucketonbucket/*grants nothing - a top-3 IAM bug. - Condition:
{operator: {key: value}}- all blocks must match. Operators includeStringEquals,StringLike,ArnLike,IpAddress,Bool,NumericLessThan,DateGreaterThan,Null, withForAnyValue:/ForAllValues:for multi-valued keys and anIfExistssuffix. - NotAction / NotResource invert the match.
"Effect": "Allow", "NotAction": "iam:*"allows everything else in AWS - read them twice. - Identity-based policies have no
Principal(the principal is whoever has the policy). Resource-based policies must have one.
Making policies
$ cd ~/oncall-lab/labs/aws
$ aws iam create-group --group-name try-analysts --query Group.Arn --output text
arn:aws:iam::111122223333:group/try-analysts
$ aws iam create-policy --policy-name try-reports-read --policy-document file://try/reports-read.json --query 'Policy.[Arn,DefaultVersionId]' --output text
arn:aws:iam::111122223333:policy/try-reports-read v1
$ aws iam attach-group-policy --group-name try-analysts --policy-arn arn:aws:iam::111122223333:policy/try-reports-read
$ aws iam create-user --user-name try-ana --query User.UserId --output text
AIDARR2VZGEZYVYPILHVW
$ aws iam add-user-to-group --group-name try-analysts --user-name try-ana
$ aws iam list-groups-for-user --user-name try-ana --query 'Groups[].GroupName' --output text
try-analysts
A new version of a customer managed policy does nothing until it is the default:
$ aws iam create-policy-version --policy-arn arn:aws:iam::111122223333:policy/try-reports-read --policy-document file://try/reports-read-v2.json --query PolicyVersion
{
"VersionId": "v2",
"IsDefaultVersion": false,
"CreateDate": "2026-09-22T20:00:05+00:00"
}
$ aws iam create-policy-version --policy-arn arn:aws:iam::111122223333:policy/try-reports-read --policy-document file://try/reports-read-v2.json --set-as-default --query PolicyVersion.VersionId --output text
v3
$ aws iam list-policy-versions --policy-arn arn:aws:iam::111122223333:policy/try-reports-read --output table
------------------------------------------------------------------
| ListPolicyVersions |
+----------------------------------------------------------------+
|| Versions ||
|+----------------------------+--------------------+------------+|
|| CreateDate | IsDefaultVersion | VersionId ||
|+----------------------------+--------------------+------------+|
|| 2026-09-22T20:00:05+00:00 | True | v3 ||
|| 2026-09-22T20:00:05+00:00 | False | v2 ||
|| 2026-09-22T20:00:04+00:00 | False | v1 ||
|+----------------------------+--------------------+------------+|
$ aws iam set-default-policy-version --policy-arn arn:aws:iam::111122223333:policy/try-reports-read --version-id v1
That is your rollback button: a bad policy change is undone by making the previous version the default again, in one call. After five versions you must delete one before creating the next (LimitExceeded). An inline policy has none of this:
$ aws iam put-user-policy --user-name try-ana --policy-name allow-own-keys --policy-document file://try/own-keys.json
$ aws iam list-user-policies --user-name try-ana
{
"PolicyNames": [
"allow-own-keys"
]
}
$ aws iam get-user-policy --user-name try-ana --policy-name allow-own-keys --query 'PolicyDocument.Statement[0].Resource'
"arn:aws:iam::*:user/${aws:username}"
$ aws iam create-policy --policy-name try-broken --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*","Principal":"*"}]}'
aws: [ERROR]: An error occurred (MalformedPolicyDocument) when calling the CreatePolicy operation: Policy document should not specify a principal.
${aws:username} is a policy variable: the same inline or managed policy lets every user manage only their own keys. And IAM checks the grammar when you save: a Principal in an identity policy is refused at once.
Unique IDs and the AWS-wide prefixes
Every IAM entity has a name, an ARN and a unique ID whose prefix tells you what it is. They show up in CloudTrail, in aws:userid, in key listings:
| Prefix | What |
|---|---|
AIDA | IAM user |
AROA | IAM role |
AGPA | group |
ANPA | managed policy |
AIPA | instance profile |
AKIA | long-term access key ID |
ASIA | temporary (STS) access key ID |
Delete a user and create one with the same name: same ARN, new unique ID. A bucket policy or trust policy saved while the old user existed does not grant the new one: AWS stored the old unique ID behind the ARN, and once that user is gone the policy shows the bare ID (AIDA...) instead of the ARN - the sign of a stale grant. Save the policy again with the ARN to point it at the new user. It is a safety feature: a new "bob" never inherits the old bob's access.
Designing for least privilege
- Start from the actions the job really calls (CloudTrail shows them; IAM Access Analyzer can generate a policy from 90 days of CloudTrail, and its unused access findings list permissions, roles and keys nobody used).
- Scope
Resourceto the ARNs, not*; add conditions where they are cheap (Region, source VPC endpoint, MFA). - Prefer roles over users, groups over per-user policies, customer managed over inline, and tags for scale: an ABAC policy can say "allow if
aws:ResourceTag/teamequalsaws:PrincipalTag/team" once, instead of one policy per team.
In an interview: "What is the difference between a managed and an inline policy?" - "A managed policy is a standalone object with an ARN and versions that you attach to many users, groups or roles; AWS managed ones are maintained by AWS. An inline policy is embedded in one principal and deleted with it. Use managed for reuse and rollback, inline for a permission that must never be shared."
You can now: name the kinds of principal and when each is right, tell identity-based from resource-based policies and managed from inline, read and write a policy statement with the right resource ARNs, version a customer managed policy and roll it back, and recognise IAM unique ID prefixes.