Home / AWS

AWSHow-toAdvanced

AWS IAM ABAC in practice: tag-based access control with principal and resource tag conditions

Replace dozens of per-project policies with a few attribute-based policies that compare principal tags with resource tags, protect the tags themselves, pass tags from IAM Identity Center, and test the result before rollout.

Deepak Bhardwaj4 min read

Applies toAWS IAM, IAM Identity Center attributes for access control, services that support aws:ResourceTag (check the per-service authorization reference), AWS CLI v2

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 costCenter or project attribute from Entra ID or Okta) to session tags. One permission set then serves every project.
  • AssumeRole with session tags: a broker can pass --tags on sts assume-role; the trust policy must allow sts: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

  1. Agree a tag dictionary: keys, allowed values, case (project not Project). Values are case sensitive in most comparisons.
  2. Enforce it with tag policies in AWS Organizations and AWS Config required-tags.
  3. Backfill tags on existing resources; find gaps with Resource Groups Tagging API:
aws resourcegroupstaggingapi get-resources --tag-filters Key=project --query "length(ResourceTagMappingList)"
  1. 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 / DeleteTags events that touch ABAC keys outside the pipeline.
  • Re-run the simulator tests in CI whenever the policies change.