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 if you want to understand why each piece is there; this file just gets you running.
Architecture (structural)
.png?sv=2026-02-06&spr=https&st=2026-09-18T15%3A56%3A15Z&se=2026-09-18T16%3A13%3A15Z&sr=c&sp=r&sig=Pp%2Bg%2FyUXZIjHUFfqzRK6Jol%2FkzZpEBP%2F05c%2Fmb1%2F3Qo%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.
Runtime flow (one happy-path request)
.png?sv=2026-02-06&spr=https&st=2026-09-18T15%3A56%3A15Z&se=2026-09-18T16%3A13%3A15Z&sr=c&sp=r&sig=Pp%2Bg%2FyUXZIjHUFfqzRK6Jol%2FkzZpEBP%2F05c%2Fmb1%2F3Qo%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.
Prereqs (host running the demo)
Linux +
bash,curl,jq,python3AWS CLI v2
podmanordocker(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 setBUCKET=to whatever you have)
Set up from zero
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.
# 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=900Optional overrides for less-common setups:
# 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'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.
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 psStep 2 — Edit users.ldif, 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):
sed -i "s/CHANGEME_BEFORE_LOAD/${PASSWORD}/g" ldap-files/users.ldifThen load the three LDIFs in order — OUs first (the parents), then users, then groups:
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.ldifSanity check:
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.comStep 3 — Phase 1 Keycloak (realm, federation, group sync)
./configure-keycloak-ldap.shSanity check — fetch a token from the base vast-s3 client and verify the 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 '{aud, email, groups}'
# → groups: ["rw-group"]Step 4 — Phase 2 Keycloak (per-group clients with conditional flow)
./configure-keycloak-clients.shThe 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.
Step 5 — VAST IAM (4 identity policies + 4 roles)
./create-iam.shIdempotent. 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.
Step 6 — Verify
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
./test-ldap-sts.shExpected:
Summary: 17 passed, 0 failedIf you hit 0 passed, 9 failed with "Unknown role" errors, re-run create-iam.sh. If you encounter token issuance failures, rerun configure-keycloak-clients.sh.
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.
# Apply as the bucket OWNER (static keys, not STS)
export AWS_ACCESS_KEY_ID='<bucket-owner-access-key>'
export AWS_SECRET_ACCESS_KEY='<bucket-owner-secret-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:
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:
export AWS_ACCESS_KEY_ID='<bucket-owner-access-key>'
export AWS_SECRET_ACCESS_KEY='<bucket-owner-secret-key>'
unset AWS_SESSION_TOKEN
aws s3api delete-bucket-policy \
--bucket "${BUCKET}" \
--endpoint-url "https://${VAST_VIP}" \
--no-verify-sslTroubleshooting one-liners
Symptom | Most likely cause | Quick fix |
|---|---|---|
| create-iam.sh not run, or run against wrong tenant | Re-run create-iam.sh after checking |
Token request returns | 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: |
| Trust policy uses a JWT claim VAST doesn't accept | Only |
| 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. |
| Conditional flow not bound, or | Re-run configure-keycloak-clients.sh (idempotent), then in Keycloak admin UI verify |
Test | 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 | Re-run from the |
| N exceeds the role's | Either lower N, or raise the role cap via |
One-page sanity checklist
Run these after a fresh setup to confirm every layer is healthy. Each should produce the expected output.
# 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 failedIf all 8 checks pass, the demo is fully wired, and you can hand it to a customer.
Files and Scripts index
File | What it is |
|---|---|
The full article | |
CHEETSHEET.md | This file |
ou=users, ou=groups parents | |
8 inetOrgPerson users | |
4 groupOfNames groups | |
KC Phase 1: realm + federation + sync | |
KC Phase 2: per-group clients + flows | |
Only identity policies live as files | |
VMS REST: 4 policies + 4 roles | |
Optional Deny overlay carving out | |
The 17-cell access matrix verification | |
The 10-cell hidden-prefix verification |