Cloud Infrastructure · Identity and Access
Wildcard Semantics in Resource ARNs and Why They Span Separators
How the star and question-mark metacharacters match against ARN strings, where their reach exceeds the intent of the author, and how policy variables substitute before matching.
Every resource in these systems has a name of the form arn:partition:service:region:account:resource, and every policy statement names resources with strings that may contain two metacharacters. The metacharacters look like shell globs. They are not, and the difference is the source of a large class of over-broad grants.
Two metacharacters, one matching rule
The star matches any sequence of characters, including the empty sequence. The question mark matches exactly one character. That is the whole grammar. There is no character class syntax, no brace expansion, no alternation, and no escape character: a literal * cannot appear in a resource name in a way that a policy can distinguish from the metacharacter.
Matching is performed against the full ARN string, not against the resource portion in isolation, and it is anchored at both ends. A resource element of arn:aws:s3:::reports matches exactly that bucket name and nothing else; it does not match arn:aws:s3:::reports-2024 even though the literal string is a prefix. To match a prefix, the star must be written explicitly, and then the star consumes everything after the prefix.
The critical property, and the one that separates this from POSIX globbing, is that the star crosses the : and / separators. In glob(7) semantics, a star does not match a slash, which is why *.txt cannot match a file in a subdirectory. Here a single star in the resource field of a policy that names arn:aws:s3:::bucket/* matches bucket/a/b/c/d.txt as readily as bucket/a.txt. There is no notion of depth, and no separator that stops the match.
The same reach applies to the earlier fields. A resource element of arn:aws:iam::*:role/Deploy has a star in the account field, and it matches a role named Deploy in any account, because the star spans exactly the account identifier. The field structure of an ARN is a convention for humans reading the string; the matcher sees one string and two wildcards.
Where the reach exceeds the intent
The most common over-grant is a star placed at the end of a resource name to mean “and everything under it”. In a bucket policy, arn:aws:s3:::logs/* is written to mean the objects under the logs prefix. It does mean that. It also matches an object key containing ../ sequences, and it matches any key at any depth, which is usually the intent. But the sibling form arn:aws:s3:::logs* is written by people who mean “the logs bucket and buckets like it”, and it matches logs, logs-old, logs-public, and logs-archive, and it matches them in the object namespace as well when the action is an object action, because the resource field for an object action takes a bucket-and-key ARN.
The second over-grant is in the account field. Writing a star there is sometimes necessary, for resources that genuinely live in every account, but it removes the account dimension from the identity of the resource. A policy that allows iam:PassRole on arn:aws:iam::*:role/* allows the principal to pass any role in any account it can reach, which is the difference between a narrow delegation and a full escalation path.
The third is subtler: matching happens on the string the service supplies, and different services construct that string differently. Some services accept a bare resource identifier in place of a full ARN and normalize it; some accept both forms; some include a path component that the policy author did not anticipate. A policy written against the documented example form can be either too narrow (the request uses a variant that does not match) or too broad (the variant collapses a distinction the author believed was present).
The corrective habit is to read every star as “and possibly a colon, a slash, and an arbitrary number of other characters”, and to place stars only where the character set that follows is genuinely unconstrained.
Actions match case-insensitively, resources do not
The action element is compared without regard to case. s3:GetObject, s3:getobject, and S3:GETOBJECT are the same action for the purpose of matching, and services document the canonical spelling but do not enforce it in the comparison.
Resource matching is case-sensitive, with the exception of the service and partition fields, which are lowercase by construction. Object keys in S3 are case-sensitive, and so are most resource names. A policy that names arn:aws:s3:::Reports/annual.pdf does not grant access to reports/annual.pdf. This asymmetry is worth remembering because the two elements sit next to each other in the same statement and look like they follow the same rules.
The star also matches the empty string, which produces a specific edge case in the region field. Global services put an empty region in their ARNs, so arn:aws:iam::123456789012:role/x has two consecutive colons. A policy written with a star in the region field matches that empty field, and a policy written with a literal region name does not, even for a service that is globally scoped. Whether a service is regional or global is therefore visible in whether its ARNs match a region-specific policy, and it is a frequent cause of “the same policy works in one account and not another” when the accounts were created at different times and the service changed scope.
Variables substitute before matching, not during
Policy variables are written ${aws:username}, ${aws:PrincipalTag/team}, or ${aws:SourceVpc} and are replaced with values from the request context before the match is attempted. Substitution is textual. Whatever the context value contains becomes part of the string being matched, and the matcher has no way to tell substituted characters from literal ones.
This is where the separation between matching and evaluation matters. A policy that grants access to arn:aws:s3:::home/${aws:username}/* is safe only if the substituted username cannot contain a slash or a star. If the identity store permits a username containing /, the substitution produces a resource that names a different prefix, and the grant follows the string. If the identity store permits a username containing *, the substitution produces a resource with a metacharacter in it, and the grant expands at match time.
Most identity stores restrict the character set for names precisely for this reason, and the restriction is a security control rather than a formatting convention. Where the store does not restrict it, the policy should be paired with a condition that constrains the value, or should avoid the variable.
Substitution also fails loudly in one direction and silently in the other. A variable that resolves to an empty value produces an empty string in the resource, and the resulting ARN usually fails to match, so the request is denied. A variable that resolves to a value with a wildcard in it produces a broader match, and the request is allowed. The failure mode that produces access is the one that needs attention.
Conditions use a different matching engine
Conditions have their own operators, and the choice of operator determines whether wildcards are honoured at all. StringEquals compares literally: a policy condition of "s3:prefix": "home/*" under StringEquals matches only the literal string home/*. The same condition under StringLike performs wildcard matching. ArnEquals and ArnLike follow the same split for ARN-valued condition keys.
The two engines also differ in what they do with case and with the number of values. Condition keys can be multi-valued, in which case the operator is evaluated against each value with a documented rule for whether all or any must match. The ForAllValues and ForAnyValue set operators make that explicit, and the default without a set operator is a subtler rule that has surprised enough authors that the documentation recommends the explicit form.
A resource element and a condition can express overlapping constraints. Naming a bucket in the resource field and constraining the prefix in a condition is belt-and-braces; naming a wildcard in the resource field and relying on the condition alone is the pattern that fails when the condition is later edited by someone who did not read the resource field. The evaluator has no notion of which of the two the author considered authoritative.
Reading a policy as a matcher
A policy is best reviewed by substituting the extreme values into each wildcard and asking whether the resulting grant is one the author would sign. For a resource of arn:aws:s3:::logs/*, the extreme value is a key with a thousand path segments, all of them allowed. For an account field of *, the extreme value is every account that exists. For an action of s3:*, the extreme value includes actions that did not exist when the policy was written, since services add actions over time and a wildcard covers them on the day they ship.
That last point is the one with the longest tail. A wildcard action grant is a grant to the future. The set of actions it covers grows without any change to the document, and no review process sees the growth. Enumerating actions is more work at authoring time and less work at every subsequent review, and it is what makes an allow-list auditable rather than merely plausible.
The alternative to enumeration, for resources whose identity is carried in tags rather than in names, is to constrain by attribute and leave the resource field broad. That shifts the matching problem from strings to key-value equality, which is a different set of failure modes, described in attribute conditions and tag-based access. Neither approach removes the need to read the wildcards; both change what a wildcard is allowed to mean.
The broader composition rules that decide what happens once a statement matches are in the evaluation order, and the reason a wildcard grant cannot be narrowed by a later statement in the same document is that documents have no ordering at all.
References
- IAM JSON policy elements: Resource — AWS Identity and Access Management User Guide
- IAM policy variables — AWS Identity and Access Management User Guide
- glob(7) — Linux manual page — Linux man-pages project