VAST STS + LDAP + Keycloak — Field Cheat Sheet (cheatsheet.md)

Prev Next

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)

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)

Runtime Flow

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, 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)


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=900

Optional 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 ps

Step 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.ldif

Then 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.ldif

Sanity 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.com

Step 3 — Phase 1 Keycloak (realm, federation, group sync)

./configure-keycloak-ldap.sh

configure-keycloak-ldap.sh

Sanity 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.sh

configure-keycloak-clients.sh

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.

Step 5 — VAST IAM (4 identity policies + 4 roles)

./create-iam.sh

create-iam.sh

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.

Step 6 — Verify

unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
./test-ldap-sts.sh

test-ldap-sts.sh

Expected:

Summary: 17 passed, 0 failed

If 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-ssl

Troubleshooting one-liners

Symptom

Most likely cause

Quick fix

InvalidArgument: Unknown role from STS

create-iam.sh not run, or run against wrong tenant

Re-run create-iam.sh 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 (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 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.


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 failed

If 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

KB.md

The full article

CHEETSHEET.md

This file

ldap-files/ous.ldif

ou=users, ou=groups parents

ldap-files/users.ldif

8 inetOrgPerson users

ldap-files/groups.ldif

4 groupOfNames groups

configure-keycloak-ldap.sh

KC Phase 1: realm + federation + sync

configure-keycloak-clients.sh

KC Phase 2: per-group clients + flows

iam-roles/rw-identity-policy.json

Only identity policies live as files

iam-roles/ro-identity-policy.json

iam-roles/wo-identity-policy.json

iam-roles/na-identity-policy.json

create-iam.sh

VMS REST: 4 policies + 4 roles

bucket-policies/hidden-prefix-policy.json

Optional Deny overlay carving out hidden/ for rwuser1

test-ldap-sts.sh

The 17-cell access matrix verification

test-hidden-prefix.sh

The 10-cell hidden-prefix verification