--- title: "VAST STS + LDAP + Keycloak — Field Cheat Sheet (cheatsheet.md)" slug: "vast-sts-ldap-keycloak-field-cheat-sheet" description: "Set up a demo in 15 minutes with LDAP and Keycloak, managing roles and S3 actions seamlessly. Follow our step-by-step guide for a smooth experience." updated: 2026-10-28T20:02:03Z published: 2026-10-28T20:02:03Z canonical: "docs.vastdata.com/vast-sts-ldap-keycloak-field-cheat-sheet" --- > ## 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. # VAST STS + LDAP + Keycloak — Field Cheat Sheet (cheatsheet.md) **CHEATSHEET.md** Reproduce the working demo on a fresh host in ~15 minutes. This is the TL;DR companion to `KB.md`. Read [KB.md](/kb/docs/vast-s3-with-ldap-driven-group-access-via-keycloak-sts) if you want to understand *why* each piece is there; this file just gets you running. --- ## [](/docs/vast-sts-ldap-keycloak-field-cheat-sheet#architecture-structural)Architecture (structural) ![](https://cdn.us.document360.io/89b91113-5dc6-49db-86ec-57b16dc8ce56/Images/Documentation/image(205).png?sv=2026-02-06&spr=https&st=2026-11-08T09%3A42%3A30Z&se=2026-11-08T10%3A42%3A30Z&sr=c&sp=r&sig=RqFJ5O%2FAUwdjjft8tghnmGAXA2OxIIhJNvQj7%2B682o0%3D) **One-line summary of every arrow:** LDAP group membership decides which realm role you have, the realm role decides which Keycloak client will issue you a token, the client decides what `aud` ends up in your JWT, and the `aud` decides which VAST IAM role you can assume — and that role decides what S3 actions you can perform. --- ## [](/docs/vast-sts-ldap-keycloak-field-cheat-sheet#runtime-flow-one-happy-path-request)Runtime flow (one happy-path request) ![Runtime Flow](https://cdn.us.document360.io/89b91113-5dc6-49db-86ec-57b16dc8ce56/Images/Documentation/image(209).png?sv=2026-02-06&spr=https&st=2026-11-08T09%3A42%3A30Z&se=2026-11-08T10%3A42%3A30Z&sr=c&sp=r&sig=RqFJ5O%2FAUwdjjft8tghnmGAXA2OxIIhJNvQj7%2B682o0%3D) For the negative case (e.g. `rouser1` requesting from `vast-s3-rw`), flow stops at step **6**: `conditional-user-role(vast-s3-rw, negate=true)` returns true (user lacks the role), the sub-flow runs, `deny-access-authenticator` fires, the token endpoint returns an error, and no JWT ever gets minted. The client never even reaches VAST. --- ## [](/docs/vast-sts-ldap-keycloak-field-cheat-sheet#prereqs-host-running-the-demo)Prereqs (host running the demo) - Linux + `bash`, `curl`, `jq`, `python3` - AWS CLI v2 - `podman` or `docker` (the demo uses podman) - Reachable from this host on the network: - VAST cluster VMS REST endpoint (e.g. `https://var201.example.com`) - VAST cluster S3 endpoint VIPs (e.g. `https://172.200.201.7-10`) - VMS admin credentials - A tenant on VAST with an OIDC provider already pointing at this Keycloak's issuer URL, plus an S3-enabled view exposing a bucket named `demo-bucket` (or set `BUCKET=` to whatever you have) --- ## [](/docs/vast-sts-ldap-keycloak-field-cheat-sheet#set-up-from-zero)Set up from zero ### [](/docs/vast-sts-ldap-keycloak-field-cheat-sheet#step-0--set-environment-variables)Step 0 — Set environment variables These are read by every script in this walkthrough. Set them once, and the rest of the steps just work. ```bash # Where you cloned this repo cd /path/to/sts-keycloak-examples/ldap-users-ldap-groups # VMS — used by create-iam.sh (TENANT_NAME also used by tests) export VMS_HOST=your_host.example.com           # <-- your VMS host export VMS_USER=admin export VMS_PASS='your-vms-admin-password'    # single-quote to dodge shell expansion export TENANT_NAME=your-tenant-name          # <-- your tenant name # Keycloak + LDAP — used by configure-keycloak-*.sh (KC_URL also used by tests) export KC_URL=http://localhost:8080 export KC_USER=admin export KC_PASS='your-keycloak-admin-password' export KC_REALM=vast export LDAP_PASS='your-openldap-admin-password' # Tests — used by test-*.sh (also reuse KC_URL and TENANT_NAME above) export VAST_VIP=172.200.201.8                # <-- a working S3 data VIP export BUCKET=demo-bucket export PASSWORD='your-demo-user-password'    # the LDAP demo-user password (set in Step 2) # Optional — max STS credential lifetime baked into each IAM role, in # seconds. Default 900 (15 min, the VAST floor). Raise for workflows # that can't re-auth mid-run (e.g. long batch jobs). export MAX_SESSION_DURATION=900 ``` Optional overrides for less-common setups: ```bash # If Keycloak runs in a container without --network=host, point it at # your host's reachable address so it can bind to LDAP: export LDAP_URL='ldap://192.168.1.10:389' # If VAST sees Keycloak at a different URL than the script does # (reverse proxy, NAT, separate network), set the issuer URL VAST will # see — this is what gets baked into the JWT 'iss' claim: export KC_FRONTEND_URL='http://keycloak.lab.example.com:8080' ``` ### [](/docs/vast-sts-ldap-keycloak-field-cheat-sheet#step-1--run-openldap-and-keycloak)Step 1 — Run OpenLDAP and Keycloak The container env-var names below (`LDAP_ADMIN_PASSWORD`, `KEYCLOAK_ADMIN_PASSWORD`) are what the upstream images expect; we just feed our `${LDAP_PASS}` and `${KC_PASS}` into them. ```bash sudo podman run -d --name openldap \  -p 389:389 -p 636:636 \  -e LDAP_ORGANISATION="VAST Demo" \  -e LDAP_DOMAIN="vast.local" \  -e LDAP_ADMIN_PASSWORD="${LDAP_PASS}" \  docker.io/osixia/openldap:1.5.0 sudo podman run -d --name keycloak \  -p 8080:8080 -p 8443:8443 -p 9000:9000 \  -e KEYCLOAK_ADMIN="${KC_USER}" \  -e KEYCLOAK_ADMIN_PASSWORD="${KC_PASS}" \  quay.io/keycloak/keycloak:26.5.6 \  start-dev --health-enabled=true # Wait until both show "Up X seconds" sudo podman ps ``` ### [](/docs/vast-sts-ldap-keycloak-field-cheat-sheet#step-2--edit-usersldif-then-load-ldap)Step 2 — Edit [users.ldif](/kb/docs/sts-keycloak-files#usersldif1), then load LDAP `users.ldif` ships with each user's password as the literal string `CHANGEME_BEFORE_LOAD`. **You must substitute it before loading**, and the value must equal whatever you exported as `${PASSWORD}` in Step 0 (otherwise the test scripts won't be able to log in as the demo users): ```bash sed -i "s/CHANGEME_BEFORE_LOAD/${PASSWORD}/g" ldap-files/users.ldif ``` - [ous.ldif](/kb/docs/sts-keycloak-files#ousldif1) - [users.ldif](/kb/docs/sts-keycloak-files#usersldif1) - [groups.ldif](/kb/docs/sts-keycloak-files#groupsldif) Then load the three LDIFs in order — OUs first (the parents), then users, then groups: ```bash ldapadd -x -H ldap://localhost:389 \  -D "cn=admin,dc=vast,dc=local" -w "${LDAP_PASS}" \  -f ldap-files/ous.ldif ldapadd -x -H ldap://localhost:389 \  -D "cn=admin,dc=vast,dc=local" -w "${LDAP_PASS}" \  -f ldap-files/users.ldif ldapadd -x -H ldap://localhost:389 \  -D "cn=admin,dc=vast,dc=local" -w "${LDAP_PASS}" \  -f ldap-files/groups.ldif ``` **Sanity check:** ```bash ldapsearch -x -H ldap://localhost:389 \  -D "cn=admin,dc=vast,dc=local" -w "${LDAP_PASS}" \  -b "ou=users,dc=vast,dc=local" "(uid=rwuser1)" dn uid mail # → should return rwuser1 with mail=rwuser1@example.com ``` ### [](/docs/vast-sts-ldap-keycloak-field-cheat-sheet#step-3--phase-1-keycloak-realm-federation-group-sync)Step 3 — Phase 1 Keycloak (realm, federation, group sync) ```bash ./configure-keycloak-ldap.sh ``` [configure-keycloak-ldap.sh](/kb/docs/sts-keycloak-files#configurekeycloakldapsh) **Sanity check** — fetch a token from the base `vast-s3` client and verify the `groups` claim: ```bash curl -s -X POST "${KC_URL}/realms/${KC_REALM}/protocol/openid-connect/token" \  -d "grant_type=password" -d "client_id=vast-s3" \  -d "username=rwuser1" -d "password=${PASSWORD}" -d "scope=openid" \  | jq -r '.id_token' | cut -d. -f2 | base64 -d 2>/dev/null \  | jq '{aud, email, groups}' # → groups: ["rw-group"] ``` ### [](/docs/vast-sts-ldap-keycloak-field-cheat-sheet#step-4--phase-2-keycloak-per-group-clients-with-conditional-flow)Step 4 — Phase 2 Keycloak (per-group clients with conditional flow) ```plaintext ./configure-keycloak-clients.sh ``` [configure-keycloak-clients.sh](/kb/docs/sts-keycloak-files#configurekeycloakclientssh2) The script is idempotent. It creates 4 realm roles, 4 group→role mappings, 4 clients, 4 flow copies, 4 conditional sub-flows, and binds each new flow to its client. ### [](/docs/vast-sts-ldap-keycloak-field-cheat-sheet#step-5--vast-iam-4-identity-policies--4-roles)Step 5 — VAST IAM (4 identity policies + 4 roles) ```plaintext ./create-iam.sh ``` [create-iam.sh](/kb/docs/sts-keycloak-files#createiamsh2) Idempotent. Reads `iam-roles/{rw,ro,wo,na}-identity-policy.json` for the identity policies and **generates the trust policies in-script** from `KC_FRONTEND_URL` (or `KC_URL`) + `KC_REALM` — the first line of output shows the OIDC issuer it ended up using. POSTs/PATCHes everything to your tenant via the VMS REST API. ### [](/docs/vast-sts-ldap-keycloak-field-cheat-sheet#step-6--verify)Step 6 — Verify ```plaintext unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN ./test-ldap-sts.sh ``` [test-ldap-sts.sh](/kb/docs/sts-keycloak-files#testldapstssh) **Expected:** ```plaintext Summary: 17 passed, 0 failed ``` If you hit `0 passed, 9 failed` with "Unknown role" errors, re-run [create-iam.sh](/kb/docs/sts-keycloak-files#createiamsh2). If you encounter token issuance failures, rerun [configure-keycloak-clients.sh](/kb/docs/sts-keycloak-files#configurekeycloakclientssh2). --- ## [](/docs/vast-sts-ldap-keycloak-field-cheat-sheet#optional-bonus-hidden-prefix)Optional bonus: hidden prefix A prefix inside `demo-bucket` that only one specific user (`rwuser1`) can read or write, even though other rw users have full access to the rest of the bucket. ```plaintext # Apply as the bucket OWNER (static keys, not STS) export AWS_ACCESS_KEY_ID='' export AWS_SECRET_ACCESS_KEY='' unset AWS_SESSION_TOKEN aws s3api put-bucket-policy \  --bucket "${BUCKET}" \  --policy file://bucket-policies/hidden-prefix-policy.json \  --endpoint-url "https://${VAST_VIP}" \  --no-verify-ssl # Verify aws s3api get-bucket-policy \  --bucket "${BUCKET}" \  --endpoint-url "https://${VAST_VIP}" \  --no-verify-ssl | jq -r '.Policy' | jq . ``` Then: ```plaintext unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN ./test-hidden-prefix.sh # → Summary: 10 passed, 0 failed # Also rerun the base test to confirm the bucket policy didn't break anything ./test-ldap-sts.sh # → Summary: 17 passed, 0 failed (still) ``` To remove the bucket policy: ```plaintext export AWS_ACCESS_KEY_ID='' export AWS_SECRET_ACCESS_KEY='' unset AWS_SESSION_TOKEN aws s3api delete-bucket-policy \  --bucket "${BUCKET}" \  --endpoint-url "https://${VAST_VIP}" \  --no-verify-ssl ``` --- ## [](/docs/vast-sts-ldap-keycloak-field-cheat-sheet#troubleshooting-one-liners)Troubleshooting one-liners | Symptom | Most likely cause | Quick fix | | --- | --- | --- | | `InvalidArgument: Unknown role` from STS | [create-iam.sh](/kb/docs/sts-keycloak-files#createiamsh2) not run, or run against wrong tenant | Re-run [create-iam.sh](/kb/docs/sts-keycloak-files#createiamsh2) after checking `TENANT_NAME` | | Token request returns `unauthorized_client` / `invalid_grant` | User isn't in the LDAP group whose realm role gates that client's flow, OR LDAP→KC sync is stale | Check group membership in LDAP. Trigger Keycloak sync: `curl -X POST -H "Authorization: Bearer $TOKEN" "$KC_URL/admin/realms/vast/user-storage/<ldap-component-id>/sync?action=triggerFullSync"` | | `Couldn't parse condition_key=<iss>:<x>` from VMS | Trust policy uses a JWT claim VAST doesn't accept | Only `aud`, `sub`, `email`, `azp` work in trust-policy conditions. Project group identity onto `aud` (the per-client architecture). | | `put-bucket-policy` hangs forever | The specific VIP you're hitting is unresponsive, or you attached LDAP directly to the VAST tenant | Retry against a different VIP in the same pool. **Never attach OpenLDAP directly to a VAST tenant** — it breaks bucket-policy operations for OIDC STS sessions on some VAST builds. This demo deliberately uses OIDC only. | | `ros user → vast-s3-rw` actually gets a token | Conditional flow not bound, or `negate=true` not set on the condition | Re-run [configure-keycloak-clients.sh](/kb/docs/sts-keycloak-files#configurekeycloakclientssh2) (idempotent), then in Keycloak admin UI verify `vast-s3-rw direct grant` flow has `vast-s3-rw group check` sub-flow with `conditional-user-role(condUserRole=vast-s3-rw, negate=true)` | | Test `parse error: Invalid numeric literal` line | Cosmetic — jq trying to parse Keycloak's error response | Ignore. The test recognizes an empty token as a refusal and records PASS | | S3 starts returning 403 after ~15 min of use | STS session hit its `max_session_duration` cap (15 min by default) | Re-run from the `assume-role-with-web-identity` step with a fresh JWT. If you need longer, `export MAX_SESSION_DURATION=3600` and re-run [create-iam.sh](/kb/docs/sts-keycloak-files#createiamsh2) to PATCH the roles, then re-auth. | | `aws sts assume-role-with-web-identity --duration-seconds N` returns an error | N exceeds the role's `max_session_duration` | Either lower N, or raise the role cap via `MAX_SESSION_DURATION` + re-run `./create-iam.sh`. Floor is 900s. | --- ## [](/docs/vast-sts-ldap-keycloak-field-cheat-sheet#one-page-sanity-checklist)One-page sanity checklist Run these after a fresh setup to confirm every layer is healthy. Each should produce the expected output. ```bash # 1. Containers up sudo podman ps | grep -E 'openldap|keycloak' # 2. LDAP responding, has the test users ldapsearch -x -H ldap://localhost:389 \  -D "cn=admin,dc=vast,dc=local" -w "${LDAP_PASS}" \  -b "ou=users,dc=vast,dc=local" "(uid=rwuser1)" dn # 3. Keycloak healthy curl -sf http://localhost:9000/health/ready # 4. Base client issues a JWT with a groups claim curl -s -X POST "${KC_URL}/realms/${KC_REALM}/protocol/openid-connect/token" \  -d "grant_type=password" -d "client_id=vast-s3" \  -d "username=rwuser1" -d "password=${PASSWORD}" -d "scope=openid" \  | jq -r '.id_token' | cut -d. -f2 | base64 -d 2>/dev/null | jq '.groups' # 5. Per-group client refuses cross-group curl -s -o /dev/null -w "%{http_code}\n" -X POST \  "${KC_URL}/realms/${KC_REALM}/protocol/openid-connect/token" \  -d "grant_type=password" -d "client_id=vast-s3-rw" \  -d "username=rouser1" -d "password=${PASSWORD}" -d "scope=openid" # → 401 (rouser1 not in rw-group) # 6. Per-group client accepts in-group curl -s -o /dev/null -w "%{http_code}\n" -X POST \  "${KC_URL}/realms/${KC_REALM}/protocol/openid-connect/token" \  -d "grant_type=password" -d "client_id=vast-s3-rw" \  -d "username=rwuser1" -d "password=${PASSWORD}" -d "scope=openid" # → 200 # 7. VAST roles exist in the right tenant TOKEN=$(curl -sk -X POST "https://${VMS_HOST}/api/token/" \  -H "Content-Type: application/json" \  -d "$(jq -n --arg u "$VMS_USER" --arg p "$VMS_PASS" '{username:$u,password:$p}')" \  | jq -r '.access') TENANT_ID=$(curl -sk "https://${VMS_HOST}/api/tenants/?name=${TENANT_NAME}" \  -H "Authorization: Bearer ${TOKEN}" \  | jq --arg n "$TENANT_NAME" '.[] | select(.name==$n) | .id') curl -sk "https://${VMS_HOST}/api/iamroles/" \  -H "Authorization: Bearer ${TOKEN}" \  | jq --argjson t "$TENANT_ID" 'map(select(.tenant_id==$t and (.name | test("^(rw|ro|wo|na)-role$")))) | map(.name)' # → ["rw-role","ro-role","wo-role","na-role"] # 8. Full end-to-end matrix unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN ./test-ldap-sts.sh 2>&1 | tail -5 # → Summary: 17 passed, 0 failed ``` If all 8 checks pass, the demo is fully wired, and you can hand it to a customer. --- ### [Files and Scripts](/kb/docs/sts-keycloak-files) index | File | What it is | | --- | --- | | [KB.md](/kb/docs/vast-s3-with-ldap-driven-group-access-via-keycloak-sts) | The full article | | CHEETSHEET.md | This file | | [ldap-files/ous.ldif](/kb/docs/sts-keycloak-files#ousldif1) | ou=users, ou=groups parents | | [ldap-files/users.ldif](/kb/docs/sts-keycloak-files#usersldif1) | 8 inetOrgPerson users | | [ldap-files/groups.ldif](/kb/docs/sts-keycloak-files#groupsldif) | 4 groupOfNames groups | | [configure-keycloak-ldap.sh](/kb/docs/sts-keycloak-files#configurekeycloakldapsh) | KC Phase 1: realm + federation + sync | | [configure-keycloak-clients.sh](/kb/docs/sts-keycloak-files#configurekeycloakclientssh2) | KC Phase 2: per-group clients + flows | | [iam-roles/rw-identity-policy.json](/kb/docs/sts-keycloak-files#rwidentitypolicyjson1) | Only identity policies live as files | | [iam-roles/ro-identity-policy.json](/kb/docs/sts-keycloak-files#roidentitypolicyjson2) | | | [iam-roles/wo-identity-policy.json](/kb/docs/sts-keycloak-files#woidentitypolicyjson) | | | [iam-roles/na-identity-policy.json](/kb/docs/sts-keycloak-files#naidentitypolicyjson2) | | | [create-iam.sh](/kb/docs/sts-keycloak-files#createiamsh2) | VMS REST: 4 policies + 4 roles | | [bucket-policies/hidden-prefix-policy.json](/kb/docs/sts-keycloak-files#hiddenprefixpolicyjson) | Optional Deny overlay carving out `hidden/` for rwuser1 | | [test-ldap-sts.sh](/kb/docs/sts-keycloak-files#testldapstssh) | The 17-cell access matrix verification | | [test-hidden-prefix.sh](/kb/docs/sts-keycloak-files#testhiddenprefixsh) | The 10-cell hidden-prefix verification |