--- title: "STS Keycloak Examples" slug: "sts-keycloak-examples" updated: 2026-10-28T20:02:04Z published: 2026-10-28T20:02:04Z canonical: "docs.vastdata.com/sts-keycloak-examples" --- > ## Documentation Index > Fetch the complete documentation index at: https://docs.vastdata.com/llms.txt > Use this file to discover all available pages before exploring further. # STS Keycloak Examples 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-users-ldap-groups](/kb/docs/ldap-users-and-groups-ldap-driven-group-scoped-sts-demo) | LDAP (federated) | LDAP | You have an enterprise LDAP / AD that owns both users and the access-control groups you want to use. | | [ldap-users-local-groups](/kb/docs/ldap-users-local-groups-hybrid-sts-demo) | LDAP (federated) | Keycloak (native) | You have enterprise LDAP for identity, but the app team wants to own its own access groups. | | [local-users-local-groups](/kb/docs/local-users-and-groups-sts-demo-with-local-keycloak-users-and-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 `demo-bucket` | | --- | --- | | `rw-group` | Full read/write | | `ro-group` | Read-only | | `wo-group` | Write-only | | `no-access-group` | 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-role `aud` value. Bakes `max_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 a `group-ldap-mapper` so 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: 1. **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. 2. **A Linux host** that can reach both Keycloak and the VAST cluster's S3 endpoints. The host needs `bash`, `curl`, `jq`,`aws` CLI v2, `python3`, and `podman` (or `docker`). 3. **The URL that ends up as the JWT** `iss` **claim** must be reachable *from the VAST cluster's nodes*, not just from your laptop. VAST fetches JWKS from that URL directly — `localhost` will 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: ```plaintext      ┌──────────────────────────────────────────┐      │  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`, and`azp` 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's`max_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): ```bash export MAX_SESSION_DURATION=3600   # 1 hour ./create-iam.sh ``` Each 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 ```plaintext 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: 1. VAST's OIDC trust-policy parser accepts only `aud`, `sub`,`email`, `azp` as condition-key JWT claim names. `groups` is rejected. Consequence: the per-group client architecture. 2. VAST trust-policy condition blocks cannot contain two operator variants from the same family (e.g. `StringEquals` and`ForAnyValue:StringEquals` together). 3. The JWT `iss` claim must be reachable *from the VAST cluster's nodes* — `localhost` fails because VAST fetches JWKS directly. 4. VAST IAM roles' `max_session_duration` has 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. 5. Keycloak 26.x silently drops inline `email`, `firstName`,`lastName`, and `credentials` at user-creation time — only`username` + `enabled` are 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).