Group-scoped, short-lived S3 credentials on a VAST cluster, backed by local Keycloak users and local Keycloak groups — no LDAP, no external directory. Keycloak is the OIDC identity provider; VAST issues temporary S3 keys. End-to-end working demo with reproducible setup scripts and verification tests.
This is the local-users sibling of ldap-users-ldap-groups. If you have an existing LDAP / Active Directory that you want Keycloak to federate from, use that repo instead. The architecture from the Keycloak client layer onward is identical; the only difference is where users and group membership live.
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.
What this proves
A user authenticates against Keycloak's local user database, receives a JWT, trades it for short-lived VAST STS credentials, and operates on S3 — with their effective S3 access determined entirely by which Keycloak group they belong to. No long-lived keys and no per-user policy bookkeeping in VAST. Group membership is the live control knob: an admin removing a user from a group cuts that user's S3 access on the next token, without touching credentials or VAST. (The demo itself keeps each user pinned to one group — this is a property claim about the architecture, not something the scripts exercise.)
The demo wires up four worked Keycloak groups against one bucket:
Keycloak group | Effective S3 access on |
|---|---|
| full read/write |
| read-only |
| write-only |
| explicit deny — proves the gate is real |
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 the user database — there is no external directory.
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. Must be an address the VAST cluster can actually reach —localhostwill not work.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.
Architecture in one diagram
.png?sv=2026-02-06&spr=https&st=2026-09-18T16%3A44%3A49Z&se=2026-09-18T17%3A00%3A49Z&sr=c&sp=r&sig=w36yCpPgMRG0I9I1rUwbfYC3tJuYFyri%2BigVTvLWOI0%3D)
KC group membership → 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.
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 five steps
Detailed walkthrough is in CHEATSHEET.md. The TL;DR:
Run Keycloak as a container on a host that can reach your VAST cluster's VMS REST endpoint and S3 VIPs. No LDAP container needed.
Run
configure-keycloak-local.shto create thevastrealm, eight local users (with profile + password + group membership), four local groups, the basevast-s3client, and thegroupsprotocol mapper. SetKC_PASSandPASSWORDin your environment first.Run
configure-keycloak-clients.shto add the four per-group clients with conditional flows that gate token issuance on group membership. Identical to the script in../ldap-users-ldap-groups/— it doesn't care whether the groups are LDAP-synced or native.Run
create-iam.shto create the four identity policies and four IAM roles in your VAST tenant via the VMS REST API.Run
test-local-sts.shto 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 — 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
# Password applied to every local demo user — used by
# configure-keycloak-local.sh to set the password, and by the test
# scripts to log in as each user.
export PASSWORD='your-demo-user-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
# 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=900One optional override covers non-default networking. 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-local.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 KC_FRONTEND_URL='http://keycloak.lab.example.com:8080'Critical: Whatever URL ends up as the iss claim must be reachable from the VAST cluster's nodes — VAST fetches the OIDC discovery document and JWKS directly. localhost will not work. If you're running Keycloak on a dev host that VAST can't reach, you need to put it somewhere VAST can, or use port-forwarding.
What's in the repo
File | What it is |
|---|---|
KC Phase 1: realm + federation + sync | |
KC Phase 2: per-group clients + flows | |
VMS REST: 4 policies + 4 roles | |
The 17-cell access matrix verification | |
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 copies of the versions in ../ldap-users-ldap-groups/. Nothing in them cares where your Keycloak users come from — they work unchanged on top of the local-user Phase 1.
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 or read from the environment. Before you run anything, export the listed environment variables — the scripts refuse to run with placeholder values.
There is no LDIF file in this repo because there is no LDAP. All eight demo users are created by configure-keycloak-local.sh via the Keycloak admin REST API, with their password set from $PASSWORD.
Do not commit a real credential to this repo, even temporarily. Use the environment variable mechanism that the scripts already provide.