Three reference implementations of group-scoped, short-lived S3 credentials on a VAST cluster, using Keycloak as the OIDC identity provider and VAST's native STS service. Each variant differs only in
where user identity and group membership live; everything downstream from the Keycloak client layer to the S3 data plane is identical across the three.
Pick your variant
Each subdirectory is a self-contained demo with its own README.md,KB.md, CHEATSHEET.md, scripts, and tests. Pick the one that matches your environment.
Subdir | User Identity | Group Membership | Pick this when… |
|---|---|---|---|
LDAP (federated) | LDAP | You have an enterprise LDAP / AD that owns both users and the access-control groups you want to use. | |
LDAP (federated) | Keycloak (native) | You have enterprise LDAP for identity, but the app team wants to own its own access groups. | |
Keycloak (local) | Keycloak (native) | No LDAP, or a greenfield POC — simplest setup, no external directory. |
All three use the same end-to-end access matrix — four group → IAM role → identity policy chains against a single demo-bucket:
Keycloak group | S3 permission on |
|---|---|
| Full read/write |
| Read-only |
| Write-only |
| Explicit deny — proves the gate is real |
Each variant passes a 17-cell verification matrix (16 access assertions + 1 negative test proving Keycloak refuses to issue a token to a user who lacks the matching group).
What's shared across all three
Because VAST only sees the JWT, and the JWT's aud claim is what gates role assumption, everything from the Keycloak client layer onward is identical. These files are byte-for-byte identical across all three subdirs:
configure-keycloak-clients.sh— Phase 2 Keycloak setup: four per-group clients, each with a conditional direct-grant flow that gates token issuance on a realm role mapped from the matching KC
group.create-iam.sh— idempotent VMS REST setup: four identity policies, four IAM roles. Trust policies generated in-script from the OIDC issuer URL + per-roleaudvalue. Bakesmax_session_duration=900(15 min, VAST's floor) into each role unless overridden.iam-roles/{rw,ro,wo,na}-identity-policy.json— the four identity policies.
The only thing that varies between variants is the Phase 1 script, which sets up users and groups:
ldap-users-ldap-groups/configure-keycloak-ldap.sh— federates LDAP for users and creates agroup-ldap-mapperso LDAP groups become KC groups.ldap-users-local-groups/configure-keycloak-ldap-users-local-groups.sh— federates LDAP for users only, creates native KC groups, then maps synced users into them.local-users-local-groups/configure-keycloak-local.sh— no LDAP at all; creates users and groups natively in Keycloak.
…and the corresponding test script per variant, which differs only in its docstring.
Shared prerequisites
All three variants assume:
A VAST cluster running VAST OS 5.4+ with:
A tenant configured to hold the demo's IAM objects.
An OIDC provider on that tenant pointing at your Keycloak
realm's issuer URL.At least one S3-enabled view exposing a bucket (e.g.
demo-bucket).Admin credentials for the VMS REST API.
A Linux host that can reach both Keycloak and the VAST cluster's S3 endpoints. The host needs
bash,curl,jq,awsCLI v2,python3, andpodman(ordocker).The URL that ends up as the JWT
issclaim must be reachable from the VAST cluster's nodes, not just from your laptop. VAST fetches JWKS from that URL directly —localhostwill not work.
Variants #1 and #2 additionally need openldap reachable by Keycloak (typically as a second container on the demo host).
Architectural shape (common across all variants)
Once users and groups exist in Keycloak (however they got there),
the flow is the same for all three:
┌──────────────────────────────────────────┐
│ User identity + group membership in KC │
└──────────────────────────────────────────┘
│
│ conditional direct-grant flow
│ gates issuance on a realm role
│ mapped from the user's KC group
▼
┌──────────────────────────────────────────┐
│ Keycloak per-group client │
│ (vast-s3-rw / ro / wo / na) │
└──────────────────────────────────────────┘
│ password grant emits token
▼
┌──────────────────────────────────────────┐
│ JWT with aud = vast-s3-XX │
└──────────────────────────────────────────┘
│ trust policy on XX-role
│ matches aud = vast-s3-XX
▼
┌──────────────────────────────────────────┐
│ VAST STS: AssumeRoleWithWebIdentity │
└──────────────────────────────────────────┘
│ session bound to XX-role
▼
┌──────────────────────────────────────────┐
│ Temporary S3 credentials │
│ (AccessKeyId / SecretAccessKey / │
│ SessionToken) │
└──────────────────────────────────────────┘
│ SigV4 request
│ identity policy on XX-role
│ decides what's allowed
▼
┌──────────────────────────────────────────┐
│ VAST S3: PUT / GET on demo-bucket │
└──────────────────────────────────────────┘
The per-group client + conditional flow architecture exists because VAST's trust-policy parser accepts only aud, sub, email, andazp as condition-key claim names. Group identity is therefore projected onto the aud claim — one Keycloak client per group, each issuing tokens with a distinct aud — and VAST gates role assumption on that.
Each variant's KB.md covers this in full. If you're new to the design, start with local-users-local-groups/KB.md — it's the shortest path from "no infrastructure" to "working demo".
Credential lifetimes — important default
Every variant's create-iam.sh pins each IAM role'smax_session_duration to 900 seconds (15 minutes) — VAST's hard floor and the smallest legal value. A leaked STS session is therefore invalid within 15 minutes, minimizing blast radius.
To raise the cap (for workflows that can't re-auth mid-run):
export MAX_SESSION_DURATION=3600 # 1 hour
./create-iam.shEach variant's KB.md → ## Reference → "Credential lifetimes" subsection has the full story, including how to PATCH an existing role without re-running the script.
Layout
sts-keycloak-examples/
├── README.md # this file
│
├── ldap-users-ldap-groups/ # variant #1 — LDAP owns users AND groups
│ ├── KB.md, README.md, CHEATSHEET.md
│ ├── configure-keycloak-ldap.sh # Phase 1
│ ├── configure-keycloak-clients.sh # Phase 2 (shared byte-for-byte)
│ ├── create-iam.sh # VMS REST (shared)
│ ├── test-ldap-sts.sh # 17-cell test
│ ├── test-hidden-prefix.sh # bonus 10-cell hidden-prefix test
│ ├── bucket-policies/
│ │ └── hidden-prefix-policy.json # bonus
│ ├── ldap-files/
│ │ ├── ous.ldif
│ │ ├── users.ldif
│ │ └── groups.ldif
│ └── iam-roles/ # identity policies (shared content)
│
├── ldap-users-local-groups/ # variant #2 — LDAP owns users; KC owns groups
│ ├── KB.md, README.md, CHEATSHEET.md
│ ├── configure-keycloak-ldap-users-local-groups.sh # Phase 1
│ ├── configure-keycloak-clients.sh # Phase 2 (shared)
│ ├── create-iam.sh # (shared)
│ ├── test-ldap-users-local-groups-sts.sh
│ ├── ldap-files/ # users + OUs only (NO groups.ldif)
│ │ ├── ous.ldif
│ │ └── users.ldif
│ └── iam-roles/ # (shared)
│
└── local-users-local-groups/ # variant #3 — no LDAP at all
├── KB.md, README.md, CHEATSHEET.md
├── configure-keycloak-local.sh # Phase 1
├── configure-keycloak-clients.sh # Phase 2 (shared)
├── create-iam.sh # (shared)
├── test-local-sts.sh
└── iam-roles/ # (shared)
Credential hygiene
Every credential in every script is a placeholder or read from the environment. Before you run anything in any variant, export the listed environment variables — the scripts refuse to run with placeholder values.
ldap-files/users.ldif (variants #1 and #2) ships with each user's password as the literal string CHANGEME_BEFORE_LOAD. Substitute it before loading; each variant's walkthrough has the sed command.
Do not commit real credentials to this repo. The root .gitignore excludes .env, *.env, .claude/, and *.pdf (vendor documentation belongs elsewhere).
What's learned here
Five facts about VAST that these demos establish empirically and consistently across all three variants:
VAST's OIDC trust-policy parser accepts only
aud,sub,email,azpas condition-key JWT claim names.groupsis rejected. Consequence: the per-group client architecture.VAST trust-policy condition blocks cannot contain two operator variants from the same family (e.g.
StringEqualsandForAnyValue:StringEqualstogether).The JWT
issclaim must be reachable from the VAST cluster's nodes —localhostfails because VAST fetches JWKS directly.VAST IAM roles'
max_session_durationhas a hard floor of 900 seconds (15 minutes) and a default of 129600 seconds (36 hours) if the field is omitted. This demo pins it to the floor.Keycloak 26.x silently drops inline
email,firstName,lastName, andcredentialsat user-creation time — onlyusername+enabledare accepted on POST. Users need to be filled in with follow-up PUT calls, or they'll fail login with "Account is not fully set up". Variant #3's Phase 1 script works around this.
Each variant's KB.md has the fuller treatment of these and adds variant-specific notes (e.g., the group-LDAP-mapper switch between variants #1 and #2).