Skip to content

Roles

Roles define trust policies for OIDC token exchange via AssumeRoleWithWebIdentity. Each role specifies which identity providers to trust, what subject constraints to enforce, and what access scopes to grant.

Configuration

toml
[[roles]]
role_id = "github-actions-deployer"
name = "GitHub Actions Deploy Role"
trusted_oidc_issuers = ["https://token.actions.githubusercontent.com"]
required_audiences = ["sts.s3proxy.example.com"]
subject_conditions = [
    "repo:myorg/myapp:ref:refs/heads/main",
    "repo:myorg/infrastructure:*",
]
max_session_duration_secs = 3600

[[roles.allowed_scopes]]
bucket = "deploy-bundles"
prefixes = []
actions = ["get_object", "head_object", "put_object"]

[[roles.allowed_scopes]]
bucket = "ml-artifacts"
prefixes = ["models/", "datasets/"]
actions = ["get_object", "head_object"]

Fields

FieldTypeRequiredDescription
role_idstringYesIdentifier used as the RoleArn in STS requests
namestringYesHuman-readable display name
trusted_oidc_issuersstring[]Validated as requiredOIDC provider URLs whose tokens are accepted. Deserializes fine when absent, but config validation rejects a role with no issuers (it could never accept a token).
required_audiencesstring | string[]Validated as requiredAccepted aud claim values. A token passes if its aud matches any entry. Empty or omitted accepts no token — the audience is what keeps a token minted for another service from being exchanged here — and config validation rejects the role. Accepts a single string or a list. The legacy required_audience key (single string) is still accepted for backward compatibility — set one key or the other, not both.
subject_conditionsstring[]Validated as requiredGlob patterns matched against the sub claim. Empty or omitted accepts no subject, and config validation rejects the role; to accept every subject, say so with "*".
allow_missing_exp_fromstring[]NoIssuers whose tokens may omit exp because the host tracks their validity itself — its own long-lived API keys with server-side revocation, say. Tokens from every other issuer must carry exp.
max_session_duration_secsintegerYesMaximum session lifetime granted by this role
allowed_scopesAccessScope[]YesBuckets, prefixes, and actions granted

Trust Policy Evaluation

When a client calls AssumeRoleWithWebIdentity, the proxy evaluates the JWT against the role's trust policy in this order:

  1. Issuer — The JWT's iss claim must match one of trusted_oidc_issuers
  2. Algorithm — Only RS256 is supported
  3. Signature — Verified against the issuer's JWKS (fetched and cached)
  4. Token type — If the JWT header carries typ, it must be JWT; access tokens (at+jwt) and other typed tokens are not identity tokens
  5. Audience — The JWT's aud claim must match at least one of required_audiences; a role with none accepts no token
  6. Expiry — The JWT must carry exp (validated with 60 seconds of clock skew), unless its issuer is listed in allow_missing_exp_from
  7. Subject — The JWT's sub claim must match at least one of subject_conditions; a role with none accepts no subject

If any check fails, the STS request returns an error.

Subject Conditions

Subject conditions use glob-style matching where * matches any sequence of characters:

toml
subject_conditions = [
    "repo:myorg/myapp:ref:refs/heads/main",      # Exact match
    "repo:myorg/myapp:ref:refs/heads/release/*",  # Prefix match
    "repo:myorg/*",                                # Any repo in the org
    "*",                                           # Any subject
]

The sub claim only needs to match one of the patterns. An empty list matches nothing — "any subject" is written "*", so that a role which forgot its conditions fails closed rather than open.

Session Duration

max_session_duration_secs is the maximum session lifetime this role grants. At mint time, the caller's requested DurationSeconds is clamped into the range [900, max_session_duration_secs] — the 900-second floor is a clamp applied to the requested session length (matching AWS's STS minimum), not a validated minimum on the field itself. If no duration is requested, 3600s is used (subject to the same clamp).

Access Scopes

Each scope grants access to a specific bucket with optional prefix and action restrictions:

toml
[[roles.allowed_scopes]]
bucket = "deploy-bundles"
prefixes = ["releases/", "staging/"]
actions = ["get_object", "head_object", "put_object"]
FieldTypeDescription
bucketstringVirtual bucket name (or template variable)
prefixesstring[]Allowed key prefixes (empty = full bucket access)
actionsstring[]Allowed S3 operations

Available Actions

ActionS3 Operation
get_objectGET (download)
get_object_versionGET/copy of a specific object version (?versionId=)
head_objectHEAD (metadata)
put_objectPUT (upload)
delete_objectDELETE
list_bucketLIST (list objects)
create_multipart_uploadPOST (initiate multipart)
upload_partPUT with partNumber (upload part)
complete_multipart_uploadPOST with uploadId (complete multipart)
abort_multipart_uploadDELETE with uploadId (abort multipart)

Prefix Matching

Prefix matching follows these rules:

  • If the prefix ends with / or is empty: the key must start with the prefix
  • Otherwise: the key must equal the prefix exactly, or start with the prefix followed by /

IMPORTANT

A prefix without a trailing / must match exactly or be followed by /. This prevents data from matching data-private/secret.txt. Use data/ to restrict to that directory.

Template Variables

Scope bucket and prefixes values support {claim_name} template variables that are resolved from the JWT claims at credential mint time:

toml
[[roles]]
role_id = "user-role"
trusted_oidc_issuers = ["https://auth.example.com"]
subject_conditions = ["*"]
max_session_duration_secs = 3600

# Each user gets access to a bucket matching their subject claim
[[roles.allowed_scopes]]
bucket = "{sub}"
prefixes = []
actions = ["get_object", "head_object", "put_object", "list_bucket"]

A user with sub = "alice" receives credentials scoped to bucket = "alice". Any string claim from the JWT can be referenced — {email}, {org}, etc.

A claim the template names that is missing from the token, or is not a string, is an error at mint time: an empty prefix would match every key in the bucket, so an unresolvable template refuses to mint rather than widen.

Examples

Per-user bucket access:

toml
bucket = "{sub}"

Organization-scoped prefix:

toml
bucket = "shared-data"
prefixes = ["{org}/"]

Read-only access to all buckets:

toml
bucket = "*"
prefixes = []
actions = ["get_object", "head_object", "list_bucket"]