skip to content

How do IAM policy variables such as ${aws:PrincipalTag/team} and ${aws:username} let one AWS policy serve many principals, and where can they be used?

level: middleimportance: nice to knowfreq 30%

answer

  1. a placeholder resolved per request
  2. only two elements accept them
  3. the old language version treats them literally
  4. roles do not have a username
  5. access follows tags, so tags need guarding

basics

~20 s

IAM policy variables are placeholders substituted from the request context at evaluation time, so one document can scope each caller to their own resources. They work in the Resource element and in string comparisons inside Condition, and require policy language version 2012-10-17.

solid answer

~50 s

A policy variable is written `${key}` and is replaced with the value of that request context key when the policy is evaluated, so a single document can behave differently for every caller. `"Resource": "arn:aws:s3:::home/${aws:username}/*"` gives each IAM user their own prefix without one policy per user. Tag-based variables are the more general form: `${aws:PrincipalTag/team}` resolves from a tag on the calling principal, and comparing it against `aws:ResourceTag/team` in a `Condition` is the standard attribute-based access control pattern — grant is by matching attributes, so adding a team means tagging, not editing policy. The constraints matter: variables are supported in `Resource` and in string comparison values inside `Condition`, not in `Action`, `Effect` or `Principal`; the document must declare `"Version": "2012-10-17"`; and if the key is absent from the request the statement simply does not match — `${aws:username}` is empty for role sessions, so a policy built on it silently grants nothing to roles.

code

json · 23 lines
json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AbacStartOwnTeamInstances",
      "Effect": "Allow",
      "Action": ["ec2:StartInstances", "ec2:StopInstances"],
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "StringEquals": { "aws:ResourceTag/team": "${aws:PrincipalTag/team}" }
      }
    },
    {
      "Sid": "ProtectTheGoverningTag",
      "Effect": "Deny",
      "Action": ["ec2:CreateTags", "ec2:DeleteTags"],
      "Resource": "*",
      "Condition": {
        "ForAnyValue:StringEquals": { "aws:TagKeys": "team" }
      }
    }
  ]
}

go deeper

for a junior

Know that ${...} in an IAM policy is a placeholder filled in from the request, letting one document scope each caller to their own resources.

for a middle

Explain that variables work in Resource and in Condition string values only, that the 2012-10-17 version is required, and that an absent key makes the statement match nothing.

for a senior

Show the attribute-based pattern comparing aws:PrincipalTag against aws:ResourceTag, and immediately state that the tagging actions must be locked down or the scheme grants itself away.

for a principal

Own the decision of whether the estate uses tag-driven access at all: who governs the tag taxonomy, how tag values are constrained, and what the review burden of a templated policy really is.

## What a variable is A policy variable is a `${...}` placeholder resolved from the **request context** at evaluation time — the same bag of key/value pairs conditions read from. It turns a policy from a fixed statement into a template evaluated per request. ```json { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject"], "Resource": "arn:aws:s3:::user-data/home/${aws:username}/*" }] } ``` One document, attached to a group of a hundred users, gives each of them exactly their own prefix. Without variables, that is a hundred near-identical policies that drift. ## Where they may appear Variables are supported in the **`Resource` element** and in **string comparison values inside `Condition`**. They are not supported in `Action`, `Effect`, `Version`, or the `Principal` element — a variable there is treated as a literal, which is worse than an error because the policy saves and quietly matches nothing. The document must declare `"Version": "2012-10-17"`. Under the older `2008-10-17` language version, `${...}` is a literal string, and a policy copied from an old example can therefore fail in a way that gives no clue. To write a literal `$`, `{` or `}` inside a policy, the escapes `${$}`, `${?}` and `${*}` exist — `${*}` and `${?}` are how you express a literal asterisk or question mark that should not act as a wildcard. ## The identity keys and their trap `${aws:username}` resolves to the IAM user's friendly name. `${aws:userid}` resolves to a unique identifier — for a role session it takes the form of the role ID plus the session name, which is why it is stable in ways the friendly name is not. The trap: **`aws:username` exists only for IAM user principals.** Assume a role and the key is absent, the substitution has nothing to resolve, and the statement does not match — no error, no log entry saying why, just an AccessDenied that looks unrelated to the policy you were proud of. Since production workloads use roles rather than users, `${aws:username}` is largely a legacy-console pattern. For roles, use principal tags. ## Tags: the general form `${aws:PrincipalTag/<key>}` resolves from a tag on the calling identity — a tag on the role, or a session tag passed at assume-role time. This is the mechanism behind attribute-based access control: ```json { "Effect": "Allow", "Action": "ec2:StartInstances", "Resource": "*", "Condition": { "StringEquals": { "aws:ResourceTag/team": "${aws:PrincipalTag/team}" } } } ``` One statement covers every team: a principal tagged `team=payments` can start instances tagged `team=payments`, and onboarding a new team is a tagging operation, not a policy change. Related keys complete the pattern — `aws:RequestTag/<key>` constrains the tags supplied on a create or tag call, and `aws:TagKeys` constrains which tag keys may appear at all. ## Why tags must then be governed The consequence is unavoidable and is the question a good interviewer asks next: if access follows tags, **whoever can set tags can grant themselves access**. A principal that may call `ec2:CreateTags` or `iam:TagRole` freely can retag a resource, or its own role, into another team's scope. Attribute-based access control is only as strong as control over the attributes, so the tagging actions have to be restricted — typically by denying tag modification on the governing tag keys except to a narrow administrative path, using `aws:TagKeys` and `aws:RequestTag` conditions. ## Practical cautions - Values are substituted verbatim, so a tag value containing characters meaningful in an ARN can produce a resource pattern you did not intend. Constrain the allowed values of governing tags. - Because an absent key makes the statement inert, test with the credential type that will actually be used — a role session, not a console user. - Variables do not make a policy shorter to reason about; they make it uniform. A reviewer must now think about the whole space of values the key can take, which is a good trade only when the tags are governed.

  • What happens if the context key behind a policy variable is not present in the request?
    The variable has nothing to resolve to and the statement does not match, so it grants nothing. There is no explicit error — you get an AccessDenied that does not mention the variable. This is why `${aws:username}` in a policy used by role sessions fails confusingly: roles have no username key.
  • Why does attribute-based access control force you to restrict tagging actions?
    Because permission follows the tag. A principal that can call the tagging APIs can move a resource, or its own role, into a scope it should not reach — self-granting access without touching a policy. So the governing tag keys must be protected, typically with a Deny on tag modification conditioned on `aws:TagKeys`, leaving changes to a narrow administrative path.
  • Can you use a policy variable in the Principal element of a bucket policy?
    No. Variables are supported in `Resource` and in string comparison values inside `Condition` only. In `Principal` the text is taken literally, so the statement matches no real principal — and the policy still saves, which makes the failure quiet. Express principal-dependent logic with a condition on a principal key instead.

saying these in an interview costs you the question

  • Uses ${aws:username} in a policy for a role
  • Puts a variable in the Action or Principal element
  • Omits Version 2012-10-17 and expects substitution
  • Ignores who can call CreateTags on governing tags
  • Assumes an unresolved variable produces an explicit error

context