Group-scoped, short-lived S3 credentials on a VAST cluster, where user identity comes from LDAP, but group membership is managed natively in Keycloak. The hybrid middle path between the pure-LDAP variant and the pure-local variant in this repo.
Sibling variants: see ldap-users-ldap-groups for LDAP users + LDAP groups, and local-users-local-groups for everything local. Pick this one when you want LDAP to own user lifecycle but your app admin — not your directory admin — to own group membership.
New to OIDC, JWTs, or IAM trust policies? See the glossary — every term used in this LDAP Users Local Groups - Hybrid STS Demo is defined there.
What this proves
An organization has an existing LDAP (or AD) that owns user accounts — hires and terminations flow through the directory, passwords are governed by directory policy, the IT team doesn't want the S3 app creating shadow accounts. But the S3 access-control groups themselves — "who's allowed to read hot-bucket, who's allowed to write archive-bucket" — change faster than the directory admins want to service, or they touch data the directory admins shouldn't be modeling in the enterprise LDAP schema.
This variant federates Keycloak against LDAP for users only, and manages the four access-control groups (rw-group, ro-group, wo-group, no-access-group) as native Keycloak objects. Group membership is assigned by the Phase 1 script after the initial LDAP sync, entirely inside Keycloak.
From the user's perspective, login works exactly like the pure-LDAP variant: they type their LDAP username and LDAP password, Keycloak binds to LDAP to verify, and they get a JWT. From the VAST cluster's perspective, it's exactly the same JWT → STS → S3 chain as the other two variants — the trust policies gate on aud, not on user source.
End-to-end test result: 17 of 17 access assertions pass, plus a negative test that proves Keycloak refuses to issue a token to a user who is not a member of the matching Keycloak group.
The three variants, side by side
Concern |
|
|
|
|---|---|---|---|
User identity | LDAP (federated into KC) | LDAP (federated into KC) | Keycloak local users |
Group membership | LDAP | Keycloak native groups | Keycloak native groups |
Who owns user lifecycle | directory admin | directory admin | Keycloak admin |
Who owns group definitions | directory admin | Keycloak admin | Keycloak admin |
Phase 1 script | |||
Phase 2 script | same (shared) | ||
VMS IAM setup | same (shared) | ||
Test script |
Architecture in one diagram
.png?sv=2026-02-06&spr=https&st=2026-09-18T16%3A45%3A57Z&se=2026-09-18T16%3A59%3A57Z&sr=c&sp=r&sig=OwGx5Hpoqlt9rZA7%2B5EH8S1oPbpnkkFAYMJ00wO9pos%3D)
LDAP is the source of truth for users. Group assignments are made by the Phase 1 script after sync, and groups themselves are native Keycloak objects. Everything from the Keycloak client layer onward is identical to the other two variants.
Read the docs
KB.md — long-form article covering the architecture and the request flow end-to-end, with reference material for the API endpoints used.
CHEATSHEET.md — 15-minute field reproducer.
If you're sharing this with a customer, hand them KB.md. If you're sitting at the keyboard rebuilding the demo, follow CHEATSHEET.md.
Reproduce in six steps
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 from
ldap-files/(onlyous.ldifandusers.ldif— there is nogroups.ldifin this variant'sldap-files/, since group membership is managed inside Keycloak, not LDAP). Editusers.ldiffirst to replaceCHANGEME_BEFORE_LOADwith the demo-user password you want to use.Run configure-keycloak-ldap-users-local-groups.sh to create the
vastrealm, federate LDAP for user auth, trigger the initial sync, create four native Keycloak groups, assign each synced user to their matching group, 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 Keycloak group membership. (Same script as the other two variants — group lookup is by name and doesn't care about origin.)
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-users-local-groups-sts.sh to verify the 17-cell access matrix end-to-end.
Required environment variables:
# 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
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-users-local-groups.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 (unique to this variant) | |
KC Phase 2 (shared across variants) | |
VMS REST: 4 policies + 4 roles | |
The 17-cell access matrix 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 * |
configure-keycloak-clients.sh, create-iam.sh, and the four identity policies under iam-roles/ are byte-for-byte identical to their counterparts in the other two variants. Trust policies are generated in-script by create-iam.sh.
All scripts are idempotent — re-running them is safe and only creates the objects that don't already exist.
Why this variant exists
The group-LDAP-mapper is the single switch that separates the pure-LDAP variant from this hybrid. Both configure-keycloak-ldap.sh (in ldap-users-ldap-groups) and configure-keycloak-ldap-users-local-groups.sh (this variant) create the same LDAP user federation with identical config. The pure-LDAP variant then adds a group-ldap-mapper component under the federation; this variant does not. The result: LDAP users sync in both cases, but only one case imports LDAP groups. Everything downstream in Keycloak treats the resulting Keycloak groups the same way.
If your directory admin is willing to maintain the access-control groups in LDAP, use ldap-users-ldap-groups. If the app team needs to own group membership without having to file a ticket with the directory team, use this variant.