← S3 compatibility matrix S3 resource guide · Access control & identity

IAM, STS, bucket policies, and presigned URLs on the XNS S3 gateway

Bucket policies, object/bucket ACLs, IAM users and groups, Public Access Block, and presigned URLs are real, enforced handlers. STS — AssumeRole, AssumeRoleWithWebIdentity (JWKS-verified OIDC), GetSessionToken, GetCallerIdentity — is implemented and tested, but scores 1/35 on the public ceph harness because 34 of those 35 tests require IAM capabilities (radosgw role/OIDC provisioning) our gateway deliberately doesn't expose. That distinction matters, and this page states it plainly.

Bucket policies & ACLs — real, enforced STS — implemented, harness-blind Cross-account IAM federation — out of model


What this area is

Access control on S3 is four independent mechanisms that stack.

Bucket policies are JSON documents attached to a bucket that allow or deny actions for named principals. ACLs are the older, per-object/bucket grant mechanism (READ, WRITE, FULL_CONTROL) predating policies. IAM is identity — users, groups, roles, and the policies attached to them, evaluated independently of the resource-side bucket policy. STS mints short-lived temporary credentials (AssumeRole, AssumeRoleWithWebIdentity, GetSessionToken) so a caller never needs a long-lived static key. Presigned URLs let a credential-holder hand out a time-limited, signed link to a single object without sharing the underlying key. AWS evaluates all of these together at request time; a gateway that only implements a subset has a different security model even where each response looks correct.

Bucket policies
JSON documents, per-bucket, scoped to named principals.
ACLs
Older per-object/bucket grants — READ, WRITE, FULL_CONTROL.
IAM
Users, groups, roles; evaluated independently of bucket policy.
STS
Short-lived temporary credentials — no long-lived static key required.
Presigned URLs
Time-limited signed link to one object, key never shared.


How XNS implements it

All four mechanisms are real handlers with a database behind them.

Bucket policies (bucket_handlers.go) are enforced via middleware.BucketPolicyEnforcement, including for anonymous requests. ACLs (acl_handlers.go) are DB-backed: GetBucketACL/PutBucketACL persist real grants, and on the object-read path, checkObjectAccess/hasPermission run an explicit bucket-policy-allow short-circuit, then an owner fast-path, then fall through to matching stored ACL grants — ACL grants are a live authorization mechanism, not superseded by policy + IAM. IAM is XNS’s own identity model (own principals, own policy evaluator, own IAM query-protocol subset on iam_query_handler.go covering 27 user/role/OIDC actions) — intra-account enforcement is proven, and it is not 1:1 AWS-IAM-policy parity. Public Access Block layers a tenant-level block above per-bucket PAB; three of the four PAB flags (RestrictPublicBuckets, BlockPublicPolicy, BlockPublicAcls) are enforced at both tenant and bucket scope, and IgnorePublicAcls is bucket-only. Presigned URLs use the SigV4 query-string variant and are covered by the test suite.

STS: what's implemented

AssumeRole (POST /?Action=AssumeRole) — caller-authenticated via SigV4, evaluates a RoleArn trust policy (Principal.AWS / Principal.Federated chaining, ExternalId confused-deputy guard), clamps MaxSessionDuration, and mints a session whose permission ceiling is sourced from the role's own permissions_policy — not a shadow IAM user. AssumeRoleWithWebIdentity exchanges a JWKS-verified OIDC token for a scoped, short-lived session with no static key; the token's signature, issuer, audience, subject, and expiry are verified, not just decoded. GetSessionToken and GetCallerIdentity are both implemented (sts_handler.go). Every signing path enforces the STS session token (X-Amz-Security-Token) with a uniform, constant-time-compared 403 InvalidToken on mismatch or absence — no state leak between “missing” and “wrong.”

GetBucketPolicy / PutBucketPolicy / DeleteBucketPolicy
Real, enforced (bucket_handlers.go, middleware.BucketPolicyEnforcement).
GetBucketACL / PutBucketACL, GetObjectACL / PutObjectACL
Real, DB-backed grants (acl_handlers.go). Live authorization mechanism on the object-read path, not decorative.
IAM users / groups / roles (own model)
Real; own principals and policy evaluator, own AWS-IAM-query-protocol subset (16 user-plane + 11 role/OIDC actions).
AssumeRole / AssumeRoleWithWebIdentity / GetSessionToken / GetCallerIdentity
Real (sts_handler.go). Covered by the private XNS capability suite, not the public ceph harness — see the conformance section alongside.
GetPublicAccessBlock / Put / Delete (bucket + tenant)
Real. 3 of 4 flags tenant-enforced; IgnorePublicAcls bucket-only.
Presigned URLs
Real; SigV4 query-string variant.


How we conform to the S3 protocol

STS scores 1/35 on the public harness. That number needs its caveat in the same breath.

Bucket policies, ACLs, IAM users/groups, Public Access Block, and presigned URLs are measured against the public ceph/s3-tests suite and pass, test by test. We publish every result, including provenance, on the compatibility matrix rather than duplicating it here.

Source: ceph/s3-tests run baseline-53dedd8d / fullsuite-53dedd8d, build 53dedd8d, 2026-08-11, classified via our per-test verdict ledger (v2.2, status_2026-08-11 / segment columns). STS capability evidence: our private capability suite. Single run, no cross-run blending.

Fact 1 — what's actually implemented

AssumeRole, AssumeRoleWithWebIdentity (JWKS-verified OIDC trust chain), GetSessionToken, and GetCallerIdentity are all implemented (sts_handler.go) and exercised by our private capability suite — boto3 assertions against XNS's own contract, independent of the ceph fixtures that can't reach it, each test citing the merged commit and capability it credits. A stock AWS SDK calling assume_role_with_web_identity() with an OIDC token gets back temporary credentials exactly per the AWS wire spec: signature, issuer, audience, and expiry verified against the IdP's JWKS, trust-policy chain evaluated, session policies enforced as a ceiling.

Fact 2 — the raw harness number, with its caveat

STS scores 1 / 35 (2.9%) on the public ceph/s3-tests STS tier, run fullsuite-53dedd8d, 2026-08-11. 34 of those 35 are classified unreachable-by-harness, not failed capability: the ceph suite provisions STS roles and OIDC identities through radosgw IAM administrative capabilities this gateway deliberately does not expose, and the suite's own conditioning helper crashes with KeyError: 'Code' before the assumed-role test body ever runs. The harness measures AWS IAM account-administration compatibility, not federation — it cannot reach the surface it's meant to test.

Why we built it this way

The gateway is single-account by architecture — one owner per relayer, tenants scoped by cost center, not AWS-style sub-accounts. An IAM control plane that provisions and manages thousands of accounts solves a problem XNS doesn't have; carrying one would add exposed surface with no benefit to the box owner. We kept the wire-compatible federation data path and replaced AWS's IAM-API provisioning step with a single admin call: the box owner registers an OIDC identity provider once via POST /admin/v1/idp. After that, a stock AWS SDK federates identically to AWS — same request shape, same verification chain, same temporary credentials.


How we compare

Same rows as the matrix — AWS S3, Ceph, MinIO, Wasabi, B2, Storj.

Source: the same competitor cells published on the S3 compatibility matrix — “Access control & identity” section — researched from each vendor’s own documentation, 2026-06-14.

Competitor figures from each vendor’s official documentation, researched 2026-06-14. Reused verbatim from the S3 compatibility matrix.
FeatureXNSAWS S3Ceph RadosGWMinIOWasabiB2Storj DCS
Bucket policies✓ private-by-default; explicit opt-in for public reads◐ Gateway-ST public-read flag only
Object / bucket ACLs✓ owner-collapse; private-by-default✗ IAM/policies only, no ACLs◐ deprecated, legacy compat◐ canned private/public-read only
IAM users / access keys✓ own IAM model; intra-account enforcement proven✗ App Keys, not IAM✗ macaroon access grants
IAM groups / roles✓ own IAM model
STS temporary credentials✓ AssumeRole + AssumeRoleWithWebIdentity (OIDC) + GetSessionToken + GetCallerIdentity, trust-policy + JWKS verified
Public Access Block
Presigned URLs


What STS means for you

One capability, three audiences.

IT admin

Your OIDC provider (Keycloak, Okta, Auth0…) plugs in via one admin call, not IAM API calls. Terraform modules that create aws_iam_openid_connect_provider resources won't apply here — the admin endpoint replaces that provisioning step. Everything after login runs the standard AWS trust-policy path.

S3 app developer

assume_role_with_web_identity() in any AWS SDK works unmodified once the admin has registered the IdP. If your app creates roles or IdPs at runtime through the IAM API, it won't work here — that's a control-plane dependency, and we'd rather you ask us first than hit a wall in production.

CFO / evaluator

Workload identity federation: supported. AWS IAM account-administration compatibility: not offered, by design — single-owner boxes don't need a plane for managing thousands of accounts. The 1/35 conformance figure on this page measures the latter, not the former.


How applications use it

Assume a role, get a scoped session, hand out a presigned link.

boto3 — AssumeRole, then use the scoped session

import boto3
sts = boto3.client("sts", endpoint_url="https://your-gateway:9000",
                    aws_access_key_id="AKIA...", aws_secret_access_key="...")
# 1. Assume a role scoped to one bucket, 1-hour session
resp = sts.assume_role(
    RoleArn="arn:aws:iam::acct:role/read-only-reports",
    RoleSessionName="agent-session-1",
    DurationSeconds=3600,
)
creds = resp["Credentials"]
# 2. Use the temporary session — X-Amz-Security-Token is enforced
s3 = boto3.client(
    "s3", endpoint_url="https://your-gateway:9000",
    aws_access_key_id=creds["AccessKeyId"],
    aws_secret_access_key=creds["SecretAccessKey"],
    aws_session_token=creds["SessionToken"],
)
s3.list_objects_v2(Bucket="reports")
# 3. Hand out a time-limited link without sharing any credential
url = s3.generate_presigned_url(
    "get_object", Params={"Bucket": "reports", "Key": "q3.pdf"}, ExpiresIn=3600
)
# 4. Or federate via OIDC — no static key at all
resp = sts.assume_role_with_web_identity(
    RoleArn="arn:aws:iam::acct:role/ci-pipeline",
    RoleSessionName="ci-run-482",
    WebIdentityToken=oidc_id_token,  # JWKS-verified: sig, iss, aud, sub, exp
)

AWS CLI

aws sts assume-role --endpoint-url https://your-gateway:9000 \
  --role-arn arn:aws:iam::acct:role/read-only-reports \
  --role-session-name cli-session
aws s3 presign s3://reports/q3.pdf \
  --endpoint-url https://your-gateway:9000 --expires-in 3600
aws s3api put-bucket-policy --endpoint-url https://your-gateway:9000 \
  --bucket reports --policy file://bucket-policy.json


Use cases

Where this matters in practice.

CI/CD without long-lived keys

A pipeline exchanges its platform's OIDC token for a scoped, short-lived S3 session via AssumeRoleWithWebIdentity — no static key checked into any secret store.

Sharing a single object, not a credential

A presigned URL hands a time-limited link to one object; the recipient never sees an access key.

Per-team IAM isolation on one account

Separate IAM users and bucket policies keep one team's read access from becoming another team's write access, enforced by real ACL + policy evaluation, not just documentation.


Applications that lean on it heavily

Real software, named.

Terraform — aws_iam / bucket policy resources

Creates IAM users, attaches policies, and applies bucket policies through XNS's own bucket-policy and IAM API surface.

GitHub Actions / GitLab CI OIDC integrations

Configure-credentials style actions that exchange a workflow's OIDC token for temporary S3 credentials via AssumeRoleWithWebIdentity.


Troubleshooting

Two symptoms, two known causes.

What to check before opening a ticket
SymptomCause & fix
AccessDenied on CreateOpenIDConnectProvider / CreateRoleExpected. Those IAM-provisioning calls aren't exposed by design — register the IdP via POST /admin/v1/idp instead.
InvalidIdentityToken 403 on AssumeRoleWithWebIdentityThe IdP isn't registered yet, or the token's issuer/audience doesn't match what was registered. Confirm both with the admin who ran /admin/v1/idp.


FAQ

Access control questions, answered directly.

Because the public ceph/s3-tests harness provisions its STS tests through radosgw IAM administrative capabilities this gateway deliberately does not expose, and its own conditioning helper crashes before 34 of those 35 tests can run. That is a harness reachability gap, not a measured capability failure. AssumeRole, AssumeRoleWithWebIdentity, GetSessionToken, and GetCallerIdentity are implemented and are verified by a private capability suite that doesn’t depend on the blocked radosgw provisioning path.

XNS enforces its own IAM model — users, groups, roles, and policies, with intra-account enforcement proven by test. It is not 1:1 AWS-IAM-policy parity; an AWS-IAM-query-protocol subset (27 actions) is also exposed for tools that speak that wire format.

Yes, via AssumeRoleWithWebIdentity. The token is JWKS-verified (signature, issuer, audience, subject, expiry), not just decoded, and mints a scoped, short-lived session with no static key.

Enforced. Bucket policy enforcement runs on every request via middleware; ACL grants are a live authorization mechanism on the object-read path, matched after an explicit-policy-allow short-circuit and an owner fast-path.

No — that is a distinct axis from XNS’s tenant isolation model and remains out of model.



See the full conformance picture.

The access control rows above are one section of the full S3 compatibility matrix — object operations, versioning, lifecycle, encryption, replication, and integration all get the same treatment.


Claims on this page last verified
© Copyright - SCP, Corp | Xa Net Services and Affiliates