LDAP-driven, group-scoped temporary S3 credentials on a VAST cluster. Keycloak handles the login flow as the OIDC identity provider; VAST issues short-lived S3 keys. End-to-end working demo with reproducible setup scripts and verification tests.
New to OIDC, JWTs, or IAM trust policies? Skim the Glossary below before reading the architecture diagram — every term in this README is defined there in plain English.
Status: 17 / 17 base access-matrix assertions passing, 10 / 10 hidden-prefix assertions passing.
What this proves
A user authenticates against an existing OpenLDAP directory via Keycloak, receives a JWT, trades it for short-lived VAST STS credentials, and operates on S3 — with their effective S3 access entirely determined by which LDAP group they belong to. No long-lived keys, no per-user policy bookkeeping in VAST, revocation by group membership change.
The demo wires up four worked LDAP groups against one bucket:
LDAP group | Effective S3 access on |
|---|---|
| full read/write |
| read-only |
| write-only |
| explicit deny — proves the gate is real |
It also includes a bonus pattern: a per-user hidden prefix inside the bucket, applied as a bucket-policy overlay, so a single user (e.g. finance@example.com) can read or write inside s3://demo-bucket/finance/ while everyone else — including other rw-group members — is denied.
Glossary
Plain-English definitions of every acronym used below. Skip this if these are all familiar.
OIDC (OpenID Connect) — an internet standard for "log in with…" flows. Sits on top of OAuth 2.0. Keycloak speaks OIDC.
IdP (identity provider) — the system that proves who you are. In this demo, Keycloak is the IdP and OpenLDAP is its user database.
JWT (JSON Web Token) — a small signed JSON document an IdP hands back after a successful login. Anyone holding a valid JWT can present it as proof of identity until it expires. The signature lets the receiver verify the token came from the IdP without phoning home.
issclaim — one field inside a JWT. The URL of the IdP that minted the token. VAST uses it to look up the IdP's public signing key and verify the signature.audclaim — another field inside a JWT. Says which audience the token was minted for — typically theclient_idof the OIDC client that asked for it. VAST gates role assumption on this field in this demo.STS (Security Token Service) — VAST's API for trading a long-term credential (here: a JWT) for short-lived S3 keys (
AccessKeyId,SecretAccessKey,SessionToken). Mirrors the AWS STS API exactly, including theAssumeRoleWithWebIdentitycall this demo uses.Trust policy — a JSON document attached to an IAM role that says who is allowed to assume the role. Its conditions are evaluated against the incoming JWT's claims. We gate ours on
aud.Identity policy — a JSON document attached to an IAM role that says what S3 actions the resulting session can perform. Trust policy says who; identity policy says what.
Bucket policy — a JSON document attached directly to an S3 bucket. Applies to every caller regardless of which role they assumed; the right place for cross-cutting rules. The hidden-prefix bonus uses one.
Denyin any policy beatsAllowin any other.
Architecture in one diagram
.png?sv=2026-02-06&spr=https&st=2026-09-18T16%3A46%3A54Z&se=2026-09-18T17%3A01%3A54Z&sr=c&sp=r&sig=x2XB5xqM5e3B%2Fq01stJA1iYizboMhXHCAEQIhYviAnE%3D)
LDAP group → Keycloak realm role → Keycloak client (with conditional auth flow) → JWT aud claim → VAST trust policy → VAST IAM role → identity policy → S3 permissions.
Read the docs
Two documents in the repo cover everything:
KB.md - The long-form article. Read this if you want to understand the architecture, why each piece exists, what each component is doing, and the full request flow end-to-end. Also has a reference section listing every condition key and API endpoint used.
CHEATSHEET.md — The field reproducer. Read this if you want to spin the whole thing up on a fresh host in 15 minutes. Has the diagrams from KB.md plus a one-page checklist of commands.
Reproduce in six steps
Detailed walkthrough is in CHEATSHEET.md. The TL;DR:
Run OpenLDAP and Keycloak as containers on a host that can reach your VAST cluster's VMS REST endpoint and S3 VIPs.
Load the LDAP test users and groups from
ldap-files/. Editusers.ldiffirst to replaceCHANGEME_BEFORE_LOADwith the demo user password you want to use.Run configure-keycloak-ldap.sh to create the
vastrealm, federate from LDAP, sync groups, and create the basevast-s3client. SetKC_PASSandLDAP_PASSin your environment first.Run configure-keycloak-clients.sh to add the four per-group clients with conditional flows that gate token issuance on group membership.
Run create-iam.sh to create the four identity policies and four IAM roles in your VAST tenant via the VMS REST API.
Run test-ldap-sts.sh to verify the 17-cell access matrix end-to-end.
Required environment variables (every script reads from the env; the same names are used everywhere):
# VMS — used by create-iam.sh (TENANT_NAME also used by tests)
export VMS_HOST=vms.example.com
export VMS_USER=admin
export VMS_PASS='your-vms-admin-password'
export TENANT_NAME=your-tenant-name
# Keycloak + LDAP — used by configure-keycloak-*.sh AND create-iam.sh
# (KC_URL also used by tests; KC_URL + KC_REALM are how create-iam.sh
# derives the OIDC issuer it bakes into the trust policies)
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.x.x.x
export BUCKET=demo-bucket
export PASSWORD='your-demo-user-password' # the LDAP demo-user password
# Optional — max STS credential lifetime baked into each IAM role by
# create-iam.sh, in seconds. Default 900 (15 min, the VAST floor).
# Raise for long-running jobs that can't re-auth mid-run.
export MAX_SESSION_DURATION=900Two optional overrides cover non-default networking. LDAP_URL is the URL Keycloak uses to bind to LDAP — override it if Keycloak runs in a container without --network=host. KC_FRONTEND_URL is what gets baked into the JWT iss claim and into the trust policies that create-iam.sh generates — override it if VAST sees Keycloak at a different URL than this script does. Both configure-keycloak-ldap.sh and create-iam.sh read this same variable, so setting it once keeps the issuer in the JWT and the issuer in the trust policy in lockstep.
export LDAP_URL='ldap://192.168.1.10:389'
export KC_FRONTEND_URL='http://keycloak.lab.example.com:8080'What's in the repo
File | What it is |
|---|---|
KC Phase 1: realm + LDAP federation + sync | |
KC Phase 2: per-group clients with conditional flows | |
VMS REST: 4 policies + 4 roles | |
17-cell base access-matrix verification | |
10-cell hidden-prefix verification | |
ou=users, ou=groups parents | |
8 inetOrgPerson users | |
4 groupOfNames groups | |
full s3:* on demo-bucket | |
read-only on demo-bucket | |
write-only on demo-bucket | |
explicit Deny * | |
Trust policies are generated in-script by create-iam.sh from the resolved OIDC issuer URL and the matching aud value — see build_trust_policy() in that file. Every trust policy in this demo is structurally identical except for the issuer + aud, so stamping them out from one template is more honest than checking in four near-identical files with a baked-in lab IP.
All scripts are idempotent — re-running them is safe and only creates the objects that don't already exist.
A note about credentials in this repo
Every credential in the source is a placeholder (CHANGEME or CHANGEME_BEFORE_LOAD). Before you run anything, you must either:
For scripts: export the listed environment variables (the scripts refuse to run with placeholder values).
For
ldap-files/users.ldif: substitute the eightCHANGEME_BEFORE_LOADvalues with the demo user password you want to use, e.g.:sed -i 's/CHANGEME_BEFORE_LOAD/your-demo-user-password/g' ldap-files/users.ldifThen
ldapaddthe file. (After that, alsoexport PASSWORD=...so the test scripts use the matching value.)
Do not commit a real credential to this repo, even temporarily. Use the environment variable mechanism that the scripts already provide.