Skip to content

IAM Policies

niksis02 edited this page Sep 4, 2026 · 1 revision

Note

Identity policies are evaluated on the S3 data plane only when the gateway runs with --iam-standalone-endpoint. The built-in IAM backends (--iam-dir, LDAP, Vault, FreeIPA, S3) have no identity-policy layer at all: under them anything short of a bucket-policy or ACL Allow yields the bare Access Denied. This page describes identity policies as implemented by the standalone IAM service.

Inline policies only

The entire policy-management surface is eight inline-policy actions — PutUserPolicy, GetUserPolicy, DeleteUserPolicy, ListUserPolicies and the four …RolePolicy equivalents. There are no policy ARNs, no versioning, no attach/detach, no permissions boundaries and no groups: CreatePolicy, AttachUserPolicy, CreateGroup, SimulatePrincipalPolicy and the rest are absent from the router, so calling one returns InvalidAction, HTTP 400, Could not find operation CreatePolicy for version 2010-05-08. IAM API has the full inventory of what is and is not routed; Incompatibilities with AWS S3 frames the AWS contrast.

Managed session policies are rejected explicitly rather than ignored: any PolicyArns.member.N on AssumeRoleWithWebIdentity returns InvalidInput, HTTP 400, PolicyArns is not supported by this implementation.

A role's trust policy (AssumeRolePolicyDocument) is a different document type with a different grammar — see STS & Web Identity.

Attaching a policy

Point the AWS CLI at the standalone IAM service's public API (the address given by the global --port flag, :7070 by default), not at the S3 gateway:

aws --endpoint-url http://iam.example.com:7070 iam put-user-policy \
  --user-name alice \
  --policy-name reports-readonly \
  --policy-document file://reports-readonly.json
  • A policy name is at most 128 characters and must match ^[A-Za-z0-9+=,.@_-]+$; a / is not allowed. Anything else gives ValidationError / HTTP 400: The specified value for policyName is invalid. It must contain only alphanumeric characters and/or the following: +=,.@_-
  • Policy names are matched exactly and case-sensitively, unlike user and role names which are matched case-insensitively. Reports and reports are two different policies.
  • A user or role cannot be deleted while it still holds inline policies: DeleteConflict, HTTP 409, Cannot delete entity, must delete policies first.

Size quotas

Limit Value
Inline policy bytes per user 2048, aggregate across all of the user's inline policies
Inline policy bytes per role 10240, aggregate across all of the role's inline policies
Session policy and trust policy 2048 bytes each

The PolicyDocument request parameter is itself capped at 131072 bytes. The per-entity quota is an aggregate, not a per-policy limit: the check sums the incoming document plus every other inline policy on that entity, so the policy being replaced is correctly excluded and re-writing an existing policy at the same size always succeeds. Overflow returns LimitExceeded, HTTP 409 Conflict, Maximum policy size of 2048 bytes exceeded for user alice. IAM API carries the full quota reference.

Important

The document is stored as the exact bytes you submitted, so pretty-printing counts against the quota. Minifying a JSON document typically frees 30–40% of it.

The policy document

An identity policy is a JSON object. The parser is strict in ways that catch real mistakes:

  • A statement may be a single object or an array of objects.
  • Unknown fields inside a statement are rejected. A typo such as "Conditon" is a hard error, not a silently unconditional Allow.
  • Duplicate JSON keys are rejected at every nesting depth — top level, inside a statement, inside a Condition operator or key map. {"Effect":"Deny","Effect":"Allow"} is refused rather than silently keeping the last one.
  • Unknown top-level document fields are accepted and ignored, so a document carrying an Id element round-trips unchanged.

Elements

Element Notes
Version Optional. Absent, "2008-10-17" or "2012-10-17"; not defaulted when absent.
Sid Optional. Must be unique within the document when non-empty.
Effect Required. Exactly "Allow" or "Deny", case-sensitive.
Action / NotAction Exactly one is required; they are mutually exclusive. Vendor-prefixed or "*".
Resource / NotResource Exactly one is required and neither may be an empty array; they are mutually exclusive. "*" or ARN-shaped.
Principal / NotPrincipal Forbidden.
  • Always write "Version": "2012-10-17". Policy variables are substituted only under exactly that string; under any other version, and with no version at all, a ${...} in a Resource is matched as literal text and therefore never matches a real ARN.
  • Sid carries no character-set or length restriction in this implementation. Uniqueness is per document and only for non-empty Sids, so two inline policies on the same user may reuse one.
  • Action matching is case-insensitive ("S3:GETOBJECT" matches s3:GetObject); resource matching is case-sensitive. The part after the action's : is never checked against any catalogue, so a misspelled action grants nothing and reports no error — check what you wrote against Supported actions.
  • A resource ARN's partition must be aws. aws-cn, aws-us-gov and the aws-iso* partitions are deliberately not supported. The service, region, account and resource-id fields are not validated, so arn:aws:notaservice:::anything passes.
  • A wildcard crosses : and / freely, so arn:aws:s3:::team-* covers every bucket whose name starts with team- and every object in them. For a prefix-scoped grant that cannot leak into a neighbouring bucket, write the bucket name literally and put the wildcard only after the /.
  • An optional Condition block is supported; Policy Conditions has the operators, the keys and what happens when a key is absent.

Principal is forbidden

Principal and NotPrincipal are rejected with MalformedPolicyDocument: Policy document should not specify a principal., and that check runs before the Action/Resource checks — so a bucket policy pasted into put-user-policy reports the principal error even when it also has other problems.

Going the other way, a bucket policy does require a Principal, and under standalone IAM that is an AWS-style principal ARN — a user, role or assumed-role session ARN, the account root ARN or the bare account id, or * — never an access key ID and never an IAM user name. See Bucket Policies.

Rejection messages

All of these are code MalformedPolicyDocument, type Sender, HTTP 400.

Message Cause
Syntax errors in policy. Unparseable JSON, a duplicate JSON key, an unknown field inside a statement, an unsupported Version, an empty or missing Statement, a bad Effect, both Action and NotAction, both Resource and NotResource, or an unrecognized Condition operator name
Policy statement must contain actions. Neither Action nor NotAction
Policy statement must contain resources. Neither Resource nor NotResource, or "Resource": []
Policy document should not specify a principal. Principal or NotPrincipal present
Statement IDs (SID) in a single policy must be unique. Two statements share a non-empty Sid
Actions/Conditions must be prefaced by a vendor, e.g., iam, sdb, ec2, etc. An action with no :
Vendor s 3 is not valid The text before the first : does not match ^[A-Za-z0-9-]+$
Resource mybucket must be in ARN format or "*". A resource with no :
The policy failed legacy parsing An arn:-prefixed resource with fewer than five remaining :-separated fields
Partition "aws-cn" is not valid for resource "arn:aws-cn:s3:::b/*". A partition other than aws

Two other rejections happen before the grammar is even parsed, both code ValidationError / HTTP 400:

  • Over 131072 bytes: 1 validation error detected: Value at 'policyDocument' failed to satisfy constraint: Member must have length less than or equal to 131072
  • An empty document, a control character, or a character above U+00FF (an emoji, for instance): The specified value for policyDocument is invalid. It must contain only printable ASCII characters. Tab, LF and CR are allowed.

The user or role must already exist: the entity lookup runs before the document is parsed, so a PutUserPolicy for an unknown user reports NoSuchEntity even when the document is also malformed.

ARN shapes

Every ARN the service generates and every ARN you write uses the fixed account ID 000000000000 — twelve zeros. An assumed-role session uses the sts service and the assumed-role/ prefix, arn:aws:sts::000000000000:assumed-role/<RoleName>/<RoleSessionName>; that is the ARN a session's denial messages name, and the one a bucket policy uses to name one specific session.

aws:PrincipalArn for a session is the assumed role's ARN, arn:aws:iam::000000000000:role/<RoleName> — not the session ARN. The role ARN is also the bucket-policy principal that covers every session of that role. Standalone IAM has the full entity ARN table.

Supported actions

The grammar accepts any syntactically valid vendor:Action, but only the names below are ever evaluated — anything else in a policy is inert. s3:* is the catch-all over every S3 action.

Object-level, evaluated against arn:aws:s3:::<bucket>/<key>: s3:GetObject, s3:GetObjectVersion, s3:PutObject, s3:DeleteObject, s3:DeleteObjectVersion, s3:GetObjectAcl, s3:PutObjectAcl, s3:GetObjectAttributes, s3:GetObjectVersionAttributes, s3:RestoreObject, s3:GetObjectTagging, s3:PutObjectTagging, s3:DeleteObjectTagging, s3:GetObjectVersionTagging, s3:PutObjectVersionTagging, s3:DeleteObjectVersionTagging, s3:GetObjectLegalHold, s3:PutObjectLegalHold, s3:GetObjectRetention, s3:PutObjectRetention, s3:BypassGovernanceRetention, s3:AbortMultipartUpload, s3:ListMultipartUploadParts

Bucket-level, evaluated against arn:aws:s3:::<bucket>: s3:CreateBucket, s3:DeleteBucket, s3:ListBucket, s3:ListBucketVersions, s3:ListBucketMultipartUploads, s3:GetBucketLocation, s3:GetBucketPolicyStatus, and the paired Get/Put actions s3:GetBucketAcl, s3:GetBucketVersioning, s3:GetBucketTagging, s3:GetBucketObjectLockConfiguration, s3:GetBucketOwnershipControls, s3:GetBucketCORS, s3:GetAnalyticsConfiguration, s3:GetEncryptionConfiguration, s3:GetIntelligentTieringConfiguration, s3:GetInventoryConfiguration, s3:GetLifecycleConfiguration, s3:GetBucketLogging, s3:GetBucketRequestPayment, s3:GetMetricsConfiguration, s3:GetReplicationConfiguration, s3:GetBucketPublicAccessBlock, s3:GetBucketNotification, s3:GetAccelerateConfiguration, s3:GetBucketWebsite — each with its Put… counterpart, plus s3:DeleteBucketPolicy, s3:DeleteBucketWebsite and s3:GetBucketPolicy / s3:PutBucketPolicy

Account-level, evaluated against arn:aws:s3:::*: s3:ListAllMyBuckets

The mappings that are not obvious from the name:

  • s3:ListBucket also covers HeadBucket; s3:ListBucketVersions covers version listing.
  • s3:CreateBucket is evaluated against arn:aws:s3:::<name> — the bucket does not exist yet, so no bucket policy is in play.
  • s3:GetBucketCORS / s3:PutBucketCORS use the uppercase CORS spelling.
  • Starting and continuing a multipart upload is authorized as s3:PutObject on the object ARN.
  • s3:DeleteObject and s3:DeleteObjectVersion are separate permissions. Any request carrying a versionId is authorized against s3:DeleteObjectVersion, and because the check runs at authorization time before the backend resolves the version, even a nonsense versionId routes to the versioned permission.
  • s3:prefix is populated only when the request actually carries a prefix query parameter, so a StringLike condition on it does not match a bare aws s3 ls s3://bucket and that listing is denied. See Policy Conditions.

s3:ListAllMyBuckets behaves unlike every other action here:

  • It is identity-policy only, deliberately absent from the bucket-policy action list, so a PutBucketPolicy naming it is rejected with MalformedPolicy / Policy has invalid action.
  • It is always evaluated against arn:aws:s3:::*, never a particular bucket. An Allow on arn:aws:s3:::mybucket grants nothing; use arn:aws:s3:::* or "*".
  • The listing is not filtered per user: a caller holding it sees every bucket on the gateway, each reporting the root account's access key as its owner. See Standalone IAM.

IAM and STS actions

An identity policy also governs the IAM control plane, where the action string is iam: plus the API action name. The set of names is exactly the actions indexed in IAM API, which groups them identically. The resource each is evaluated against depends on the action:

  • Actions naming a specific user or role are evaluated against that entity's stored ARN, with its tags available as iam:ResourceTag/<key> and aws:ResourceTag/<key>.
  • iam:GetUser, iam:CreateAccessKey, iam:UpdateAccessKey, iam:DeleteAccessKey and iam:ListAccessKeys accept an omitted UserName and are then evaluated against the caller's own stored user ARN and tags, so a self-service policy scoped to arn:aws:iam::000000000000:user/<self> authorizes them without a wildcard Resource. A caller with no IAM user of its own — an assumed-role session, or root — resolves to no resource, which only a wildcard Resource statement matches; the controller then rejects the request anyway with ValidationError / Must specify userName when calling with non-User credentials.
  • iam:CreateUser and iam:CreateRole are evaluated against the ARN the request would create, built from the UserName/RoleName and Path parameters.
  • Every OIDC provider action carrying an OpenIDConnectProviderArn — iam:GetOpenIDConnectProvider, iam:DeleteOpenIDConnectProvider, iam:AddClientIDToOpenIDConnectProvider, iam:RemoveClientIDFromOpenIDConnectProvider, iam:UpdateOpenIDConnectProviderThumbprint, iam:TagOpenIDConnectProvider, iam:UntagOpenIDConnectProvider and iam:ListOpenIDConnectProviderTags — is evaluated against that raw ARN parameter, with the stored provider's own tags exposed as iam:ResourceTag/<key> and aws:ResourceTag/<key>. iam:CreateOpenIDConnectProvider is evaluated against the ARN built from the validated Url. A Resource of arn:aws:iam::000000000000:oidc-provider/* therefore does scope provider management.
  • iam:ListUsers, iam:ListRoles, iam:ListOpenIDConnectProviders and anything the middleware does not specifically recognize are evaluated against "*".
  • iam:UpdateUser is authorized twice — once against the user's current ARN and once against the ARN a rename or path change would produce. Both must Allow.

aws:RequestTag/<key> and aws:TagKeys are populated from the request's Tags parameter for iam:CreateUser, iam:CreateRole, iam:TagUser, iam:TagRole, iam:CreateOpenIDConnectProvider and iam:TagOpenIDConnectProvider; iam:UntagUser, iam:UntagRole and iam:UntagOpenIDConnectProvider supply keys with no values and so populate aws:TagKeys alone. That is what lets a tag-scoped Condition govern which tags a caller may set or remove, not only which tags a caller may create an entity with. A malformed Tags/TagKeys parameter leaves these keys unpopulated here; the controller rejects the request with the specific tag error afterwards.

The two STS actions, sts:AssumeRoleWithWebIdentity and sts:GetCallerIdentity, are exempt from identity-policy enforcement and reach the controller directly, matching real AWS. sts: is the only vendor prefix allowed in a trust policy (see STS & Web Identity), but writing sts: actions into an identity policy has no effect.

The root account bypasses identity-policy evaluation entirely on both the IAM control plane and the S3 data plane. Root never needs a policy and cannot be restricted by one.

Policy variables

A ${key} reference is substituted with the single value of that condition key from the request context. Substitution happens in Resource/NotResource patterns and in string and ARN condition operands only — never in Action/NotAction, and never in a trust policy's Principal — and only under "Version": "2012-10-17" exactly. A ${...} naming a key that is absent, or that has more than one value, is left as literal text rather than dropped, so the statement stops matching instead of silently widening.

${aws:username} is not set for an assumed-role session, so the classic home-prefix pattern arn:aws:s3:::home/${aws:username}/* silently stops working once a caller assumes a role. Use ${aws:userid} or a ${aws:PrincipalTag/<key>} value there instead. The full variable and condition-key inventory is in Policy Conditions.

Evaluation order

This is what actually decides an S3 request.

  1. Readonly gate. Under --readonly, a request needing write or write-ACL permission is denied with the bare AccessDenied / Access Denied. for everyone, root and admin included, before any bypass and with no policy able to grant past it — including CopyObject, which previously returned early for root and admin and so escaped the check.
  2. Root, admin and public bypass. Policy and ACL evaluation is skipped for the root account, for any account carrying the admin role, and for an anonymous request already authorized by the public-bucket check; object locks still apply to all of them. Under standalone IAM every resolved account is a plain user, so in practice only root bypasses.
  3. Resource decision. The bucket policy is fetched once. If one exists it decides and the bucket ACL is not consulted at all; with no bucket policy the ACL decides and can produce only Allow or NoMatch, since ACLs have no explicit deny and their verdict applies uniformly to the whole bucket. For a multi-action request a Deny on any one action wins immediately, and Allow requires every one of them.
  4. A bucket-policy explicit Deny short-circuits a single-resource request: it fails there, and no evaluate-policy call is made to the standalone IAM service at all. In a DeleteObjects batch the deny settles only the keys it matches and the round trip is still made — see DeleteObjects partial success.
  5. Identity decision. Performed only under --iam-standalone-endpoint: one evaluate-policy request carrying the whole actions × resources matrix over the private endpoint. With a built-in IAM backend this step does not exist, and anything short of a bucket-policy or ACL Allow is the bare Access Denied.
  6. An explicit Deny from the identity policy or from a session policy denies the request. An explicit deny in any layer beats every Allow in every other layer.
  7. Allow is granted if either the resource decision or the identity decision is Allow. They are independent sources; neither has to agree with the other.
  8. A session policy filters everything, including a grant that came from the bucket policy rather than from the role.
  9. Otherwise: implicit deny.
  10. Object locks are checked separately, after and independently of all of the above, and they apply to root as well — see Object lock interaction.

Identity policy combined with bucket policy

Identity policy Bucket policy Outcome
silent silent Implicit deny, naming the identity ARN
Allow silent Allowed
silent Allow Allowed
Allow Allow Allowed
Deny silent Explicit deny in an identity-based policy
Deny Allow Explicit deny in an identity-based policy
silent Deny Explicit deny in a resource-based policy
Allow Deny Explicit deny in a resource-based policy
Deny Deny Explicit deny in a resource-based policy — the bucket policy is evaluated first and returns immediately

"Silent" means the policy exists but no statement matched, or there is no policy at all.

  • Under standalone IAM a freshly created bucket is owned by root and grants an ordinary user nothing through its ACL, so the resource decision is NoMatch by default. The identity policy must Allow, or the request is denied.
  • A bucket policy can grant access to a user with no identity policy at all — but only from a statement naming the caller directly (its ARN, its role's ARN, or *). A statement whose Principal names the account, arn:aws:iam::000000000000:root or the bare 000000000000, is a delegation: under Allow it contributes nothing and the identity policy must still allow the request, while under Deny it denies every principal in the account outright. See Bucket Policies.

Reading a denial message

Every denial is code AccessDenied with HTTP 403, and the message tells you which layer decided. The first three share the prefix User: <principal> is not authorized to perform: <action> on resource: "<arn>":

Suffix, or whole message What happened
with an explicit deny in a resource-based policy A Deny statement in the bucket policy matched
with an explicit deny in an identity-based policy A Deny statement in an inline identity policy or a session policy matched
because no identity-based policy allows the <action> action Nothing granted it — an implicit deny
Access Denied. Either --readonly blocked a write, or the gateway is running a built-in IAM backend with no identity-policy layer
Access Denied because object protected by object lock. An object lock, not a policy

<principal> is the caller's ARN — arn:aws:iam::000000000000:user/alice, or arn:aws:sts::000000000000:assumed-role/data-reader/session-1 for a session — in every case, resource-based denials included. The raw access key id appears instead only when the IAM backend gives the caller no ARN, which is every backend except the standalone service.

The <action> named in an implicit deny is the first action of the request that lacked an Allow. A request such as PutObject with an x-amz-tagging header is authorized against s3:PutObject and s3:PutObjectTagging at once, and all of them must pass — so a denial naming s3:PutObjectTagging on what looked like a plain upload is not a bug.

Fail-closed cases

The evaluator refuses to guess. Each of these produces a Deny for the whole evaluation, not merely a skipped statement:

  • A stored policy document that cannot be parsed as JSON.
  • A stored policy document that fails validation. Validation is re-run at the security boundary, not just at write time, so an old document written before a validation rule existed is caught. A single invalid statement invalidates the entire document for every request, including requests that statement would never have covered.
  • A Condition block that cannot be evaluated — an unparseable block or an unrecognized operator name. This is not Deny-statement-specific: an unknown operator in an unrelated Allow denies everything the document might otherwise have granted.
  • A mismatch between the decision matrix the IAM service returns and the request the gateway sent.
  • The standalone IAM service being unreachable, which surfaces as InternalError / HTTP 500 rather than as an allow.

So if one user suddenly cannot do anything at all, suspect one broken statement in one of their inline policies rather than a missing grant.

Session credentials

Temporary credentials minted by AssumeRoleWithWebIdentity (access key IDs prefixed ASIA…) draw their permissions from the role's inline policies, not from any user. If the call supplied a Policy parameter, that session policy is evaluated as its own document with the same grammar and semantics, and the two are intersected:

effective = (role identity policies  ∪  bucket policy)  ∩  session policy

An explicit Deny on either side denies outright, and a session policy can only narrow, never widen. It filters everything, including a grant that came from the bucket policy rather than from the role: a role with no identity policy, a bucket policy granting both s3:GetObject and s3:PutObject, and a session policy allowing only s3:GetObject yields a working GetObject and a denied PutObject.

On the S3 data plane an expired session is reported as InvalidAccessKeyId, not as an authorization failure. STS & Web Identity covers the rest: the intersection table, the 2048-byte session-policy cap, why an ASIA… key can never be an ACL grantee or a bucket owner while a session is still nameable by ARN in a bucket policy, why the role's tags are the session's tags for its whole lifetime, and what happens to live sessions once their role is deleted.

DeleteObjects partial success

DeleteObjects is the one write path that supports partial success, and the gateway authorizes it accordingly. Every key in the batch is evaluated independently, against its own object ARN, so a denial removes that key from the batch rather than failing the whole request:

  • The XML body is parsed before authorization, because the keys are part of what is being authorized.
  • The batch is split into keys with no versionId (authorized as s3:DeleteObject) and keys with one (s3:DeleteObjectVersion) — one evaluate-policy round trip per group, never one per object.
  • Object lock state is loaded once and applied per key, skipping keys already denied by policy so a policy denial is never masked by a less specific lock error. Only error-free keys reach the backend, and the results are merged preserving request order across both lists.

The client sees a normal 200 with a DeleteResult body carrying both outcomes; a denied key appears as:

<Error>
  <Key>denied/three</Key>
  <Code>AccessDenied</Code>
  <Message>User: arn:aws:iam::000000000000:user/alice is not authorized to perform: s3:DeleteObject on resource: "arn:aws:s3:::mybucket/denied/three" because no identity-based policy allows the s3:DeleteObject action</Message>
</Error>

A governance-locked key in the same batch is reported the same way, with Access Denied because object protected by object lock.

Only two things fail the whole request rather than one key: --readonly mode, and an error resolving the bucket policy or lock configuration.

A bucket-policy explicit Deny is per-key like every other denial. It settles the key it matches — reported with the resource-based message, and never overridden by an identity policy that would have allowed it — and nothing else: every other key in the batch is still evaluated on its own, identity policy included. A batch in which the bucket policy denies every key is the one case that skips the evaluate-policy round trip, since no identity-policy answer could change any of the results.

Note

In v1.7.0 and earlier this short-circuited: the first denied key ended the batch's evaluation, and every later key in that action group was sent to the backend without its identity policy ever being evaluated. On those versions, do not rely on a bucket-policy Deny as the only control on a bucket that also accepts multi-object deletes — put the Deny in the identity policy as well.

Object lock interaction

Object locks are enforced after policy, and unlike policy they are not bypassed by root. Whether a GOVERNANCE retention can be overridden depends on how the request arrived, which the gateway models as three bypass modes:

Mode When
BypassNone The request sent no x-amz-bypass-governance-retention header, so any unexpired retention blocks it
BypassRequested DeleteObject, DeleteObjects or PutObjectRetention with x-amz-bypass-governance-retention: true
BypassOverwrite PutObject, CopyObject or POST Object silently replacing a locked object — none of these has a bypass header for a client to send, so the gateway supplies the mode itself

BypassOverwrite is a versitygw extension with no AWS analogue: real S3 only allows object lock on versioned buckets, where an overwrite creates a new version instead of replacing a locked object. On an unversioned bucket the gateway would otherwise destroy a locked object without anybody having asked to.

Who may proceed:

Situation Root and admin Ordinary caller
GOVERNANCE object, bypass header sent (BypassRequested) Always allowed Needs an Allow for s3:BypassGovernanceRetention on the object
GOVERNANCE object, no bypass header (BypassNone) Blocked Blocked — Access Denied because object protected by object lock.
GOVERNANCE object replaced by a PutObject/CopyObject/POST (BypassOverwrite) Needs a bucket-policy grant for s3:BypassGovernanceRetention Needs an Allow for s3:BypassGovernanceRetention
COMPLIANCE object, any mode Blocked Blocked
Legal hold ON, any mode Blocked Blocked
  • Root's blanket override applies only to BypassRequested, so an accidental overwrite of a locked object is refused even for root. Because root is not an IAM user, its BypassOverwrite check falls back to the bucket policy alone and needs an explicit bucket-policy Allow for s3:BypassGovernanceRetention.
  • The denial naming s3:BypassGovernanceRetention appears only when the bypass header was sent and permission was missing. A request with no bypass header, an anonymous request, or a gateway with no identity-policy layer gets the generic Access Denied because object protected by object lock. instead.

Extending versus weakening a retention

PutObjectRetention distinguishes strengthening a lock from weakening one:

Stored retention Requested retention Required
none anything s3:PutObjectRetention
GOVERNANCE until T GOVERNANCE until ≥ T s3:PutObjectRetention — an extension, no header needed
COMPLIANCE until T COMPLIANCE until ≥ T s3:PutObjectRetention — an extension, no header needed
GOVERNANCE until T GOVERNANCE until < T Bypass header and s3:BypassGovernanceRetention
GOVERNANCE until T COMPLIANCE, any date Bypass header and s3:BypassGovernanceRetention — a mode change is never an extension
COMPLIANCE until T anything shorter, or GOVERNANCE at any date Refused for everyone, header or not

The same date counts as an extension, not a weakening. A stored retention carrying no date at all is treated as weakenable, failing closed. A RetainUntilDate in the past is rejected outright as an invalid argument.

Worked example: tag-scoped IAM administration

Delegates user administration for one team, using iam:ResourceTag to scope management to users tagged team=alpha and aws:RequestTag to force new users to carry that tag.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "InspectAnyUser",
      "Effect": "Allow",
      "Action": ["iam:ListUsers", "iam:GetUser"],
      "Resource": "*"
    },
    {
      "Sid": "CreateOnlyTeamAlphaUsers",
      "Effect": "Allow",
      "Action": "iam:CreateUser",
      "Resource": "arn:aws:iam::000000000000:user/*",
      "Condition": {
        "StringEquals": { "aws:RequestTag/team": "alpha" }
      }
    },
    {
      "Sid": "ManageTeamAlphaUsers",
      "Effect": "Allow",
      "Action": [
        "iam:CreateAccessKey",
        "iam:DeleteAccessKey",
        "iam:ListAccessKeys",
        "iam:PutUserPolicy",
        "iam:GetUserPolicy",
        "iam:DeleteUserPolicy",
        "iam:ListUserPolicies"
      ],
      "Resource": "arn:aws:iam::000000000000:user/*",
      "Condition": {
        "StringEquals": { "iam:ResourceTag/team": "alpha" }
      }
    }
  ]
}
  • By the resource rules above, a create-user call without Tags=[{Key=team,Value=alpha}] is denied, and the management statement reaches only users already tagged team=alpha. An untagged user has no iam:ResourceTag/team key at all, so StringEquals fails and the statement does not match.
  • iam:ListUsers is always evaluated against "*", which is why it needs its own statement — a Resource of arn:aws:iam::000000000000:user/* would not match it.
  • The delegation is deliberately narrow: it grants no iam:TagUser, iam:UntagUser or iam:DeleteUser, so the delegated admin cannot retag a user into or out of their own scope. Adding iam:TagUser would let them widen it.

See Also

Clone this wiki locally