With role-based access, every new project means a new policy listing its ARNs. Attribute-based access control (ABAC) flips that: one policy says "you can manage resources whose project tag matches your project tag". New projects need no policy change at all.
The three condition keys that matter
| Key | Meaning |
|---|---|
aws:PrincipalTag/<key> | Tag on the role or session making the call |
aws:ResourceTag/<key> | Tag on the resource being acted on |
aws:RequestTag/<key> and aws:TagKeys | Tags being set in this request (create or tag operations) |
A core ABAC policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ManageOwnProjectInstances",
"Effect": "Allow",
"Action": ["ec2:StartInstances", "ec2:StopInstances", "ec2:RebootInstances"],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": { "aws:ResourceTag/project": "${aws:PrincipalTag/project}" }
}
},
{
"Sid": "CreateOnlyWithOwnProjectTag",
"Effect": "Allow",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": { "aws:RequestTag/project": "${aws:PrincipalTag/project}" },
"ForAllValues:StringEquals": { "aws:TagKeys": ["project", "env", "owner", "Name"] }
}
},
{
"Sid": "AllowTaggingOnCreate",
"Effect": "Allow",
"Action": "ec2:CreateTags",
"Resource": "arn:aws:ec2:*:*:*/*",
"Condition": { "StringEquals": { "ec2:CreateAction": "RunInstances" } }
}
]
}
RunInstances touches several resource types (subnet, security group, volume, network interface, image). Those need their own statements without the request-tag condition, or the launch fails. Read the EC2 authorization reference for the full list.
Protect the tags
ABAC is only as strong as control over tags. Deny changing the project tag on existing resources, and deny principals from tagging themselves:
{
"Sid": "DenyRetagging",
"Effect": "Deny",
"Action": ["ec2:CreateTags", "ec2:DeleteTags"],
"Resource": "*",
"Condition": {
"Null": { "aws:ResourceTag/project": "false" },
"ForAnyValue:StringEquals": { "aws:TagKeys": ["project"] },
"StringNotEquals": { "ec2:CreateAction": "RunInstances" }
}
},
{
"Sid": "DenyPrincipalTagEdits",
"Effect": "Deny",
"Action": ["iam:TagRole", "iam:UntagRole", "iam:TagUser", "iam:UntagUser"],
"Resource": "*"
}
Put the second statement in an SCP so no account admin can bypass it except a platform role you exclude.
Caution
Test deny statements carefully. A broad tag deny in an SCP can break automation (Auto Scaling, CloudFormation) that legitimately tags resources. Exclude service-linked roles and your pipeline roles by
aws:PrincipalArn.
Where principal tags come from
- IAM roles: tags on the role itself.
aws iam tag-role --role-name dev-payments --tags Key=project,Value=payments Key=env,Value=dev
- IAM Identity Center: enable attributes for access control and map identity-source attributes (for example a
costCenterorprojectattribute from Entra ID or Okta) to session tags. One permission set then serves every project. - AssumeRole with session tags: a broker can pass
--tagsonsts assume-role; the trust policy must allowsts:TagSession.
aws sts assume-role --role-arn arn:aws:iam::111111111111:role/abac-ops \
--role-session-name alice --tags Key=project,Value=payments
Services and gaps
Not every action supports aws:ResourceTag. List and some Describe calls cannot be scoped by tag, so users will see resources they cannot change. That is usually acceptable. Check the Service Authorization Reference column "Condition keys" for each action before you design around it. S3 object-level ABAC and some data-plane APIs have their own patterns.
Test before rollout
aws iam simulate-custom-policy --policy-input-list file://abac.json \
--action-names ec2:StopInstances \
--resource-arns arn:aws:ec2:eu-west-1:111111111111:instance/i-0abc \
--context-entries "ContextKeyName=aws:ResourceTag/project,ContextKeyValues=payments,ContextKeyType=string" \
"ContextKeyName=aws:PrincipalTag/project,ContextKeyValues=payments,ContextKeyType=string" \
--query "EvaluationResults[].EvalDecision"
Run the same with mismatched values and expect implicitDeny. Then do a live test with a sandbox role per project.
Rollout
- Agree a tag dictionary: keys, allowed values, case (
projectnotProject). Values are case sensitive in most comparisons. - Enforce it with tag policies in AWS Organizations and AWS Config
required-tags. - Backfill tags on existing resources; find gaps with Resource Groups Tagging API:
aws resourcegroupstaggingapi get-resources --tag-filters Key=project --query "length(ResourceTagMappingList)"
- Deploy ABAC policies alongside existing RBAC; remove the old policies once CloudTrail shows no access depends on them.
Prevent it next time
- Treat tag keys used in ABAC as security attributes: changes go through review.
- Alert on
CreateTags/DeleteTagsevents that touch ABAC keys outside the pipeline. - Re-run the simulator tests in CI whenever the policies change.