MastertheMesh
OIDC · Keycloak · how-to · shared building block
How-to

Keycloak setup — the shared OIDC issuer the identity labs point at

TO
Tom O'Rourke
EMEA Field CTO · Solo.io

One bitnami Keycloak install with realm solo, client kagent and three pre-grouped users (alice, bob, carol). Every lab on this site that needs a real OIDC token mints it from here — the kagent AccessPolicy lab, the claims-to-headers lab, the OIDC OBO lab, and RFC 8693 token exchange. Get this up once, link to it from each lab.

Keycloak OIDC JWT bitnami chart Realm import

What you'll get

FTE

alice

Group
field-fte
Password
alice
Use for
full-toolset, full-access demo paths.
Trial

bob

Group
field-trial
Password
bob
Use for
reduced-toolset / deny-by-default demo paths.
Admin

carol

Group
field-admin
Password
carol
Use for
cluster-wide / privileged demo paths.

Install

1. Apply the realm-import ConfigMap

The realm export below pre-seeds the realm, client, groups and users. We mount it as a ConfigMap and tell Keycloak to --import-realm on first boot.

json yaml/realm-solo.json
{
  "realm": "solo",
  "enabled": true,
  "groups": [
    { "name": "field-fte" },
    { "name": "field-trial" },
    { "name": "field-admin" }
  ],
  "users": [
    { "username": "alice", "enabled": true,
      "credentials": [ { "type": "password", "value": "alice", "temporary": false } ],
      "groups": ["field-fte"] },
    { "username": "bob",   "enabled": true,
      "credentials": [ { "type": "password", "value": "bob",   "temporary": false } ],
      "groups": ["field-trial"] },
    { "username": "carol", "enabled": true,
      "credentials": [ { "type": "password", "value": "carol", "temporary": false } ],
      "groups": ["field-admin"] }
  ],
  "clients": [
    {
      "clientId": "kagent",
      "publicClient": true,
      "directAccessGrantsEnabled": true,
      "protocolMappers": [
        { "name": "audience-kagent",
          "protocolMapper": "oidc-audience-mapper",
          "config": { "included.client.audience": "kagent",
                      "access.token.claim": "true" } },
        { "name": "groups",
          "protocolMapper": "oidc-group-membership-mapper",
          "config": { "claim.name": "groups", "full.path": "false",
                      "access.token.claim": "true",
                      "id.token.claim": "true",
                      "userinfo.token.claim": "true" } }
      ]
    }
  ]
}
kubectl create namespace kagent --dry-run=client -o yaml | kubectl apply -f -

kubectl -n kagent create configmap keycloak-realm-import \
  --from-file=realm-solo.json=yaml/realm-solo.json \
  --dry-run=client -o yaml | kubectl apply -f -

2. Install the bitnami chart

yaml yaml/values.yaml
auth:
  adminUser: admin
  adminPassword: admin

production: false
proxy: edge

resources:
  requests: { cpu: 200m, memory: 512Mi }
  limits:   { cpu: 1,    memory: 1Gi   }

extraStartupArgs: "--import-realm"

extraVolumes:
  - name: realm-import
    configMap:
      name: keycloak-realm-import

extraVolumeMounts:
  - name: realm-import
    mountPath: /opt/bitnami/keycloak/data/import
    readOnly: true

replicaCount: 1
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

helm upgrade --install keycloak bitnami/keycloak \
  --namespace kagent \
  -f yaml/values.yaml

kubectl -n kagent rollout status statefulset/keycloak --timeout=180s

3. Reach Keycloak from your laptop

Local port 18080 is used instead of 8080 because tools like OrbStack, Docker Desktop, and many Java app servers default to 8080 and quietly win the bind race against kubectl port-forward. Pick any free local port if 18080 is also taken on your laptop.

kubectl -n kagent port-forward svc/keycloak 18080:80 &
export KC_HOST="http://127.0.0.1:18080"
export KC_REALM="solo"
export KC_CLIENT="kagent"

Mint a token

The kagent client has the password grant enabled, so minting an access token is one curl call. Swap username/password for whichever user you want to act as.

# alice — groups=["field-fte"]
ALICE_JWT=$(curl -s -X POST \
  "${KC_HOST}/realms/${KC_REALM}/protocol/openid-connect/token" \
  -d "grant_type=password" \
  -d "client_id=${KC_CLIENT}" \
  -d "username=alice" \
  -d "password=alice" \
  -d "scope=openid" \
  | jq -r .access_token)

# bob — groups=["field-trial"]
BOB_JWT=$(curl -s -X POST \
  "${KC_HOST}/realms/${KC_REALM}/protocol/openid-connect/token" \
  -d "grant_type=password" \
  -d "client_id=${KC_CLIENT}" \
  -d "username=bob" \
  -d "password=bob" \
  -d "scope=openid" \
  | jq -r .access_token)

# carol — groups=["field-admin"]
CAROL_JWT=$(curl -s -X POST \
  "${KC_HOST}/realms/${KC_REALM}/protocol/openid-connect/token" \
  -d "grant_type=password" \
  -d "client_id=${KC_CLIENT}" \
  -d "username=carol" \
  -d "password=carol" \
  -d "scope=openid" \
  | jq -r .access_token)

Decode and verify the token

Decode the JWT payload to confirm the issuer, audience and groups claims arrived on the token. Every lab that consumes this token validates these three fields.

echo "${ALICE_JWT}" | cut -d. -f2 | base64 -d 2>/dev/null | jq '.iss, .aud, .groups, .preferred_username'

You should see:

"http://keycloak.kagent.svc.cluster.local:8080/realms/solo"
"kagent"
[ "field-fte" ]
"alice"

Note that the issuer claim uses the in-cluster service DNS — that's because Keycloak's frontend URL is whatever the realm was first imported with. If a downstream verifier needs a different issuer (e.g. https://keycloak.example.com/realms/solo from outside the cluster), set the realm's frontendUrl attribute in the import JSON or via the admin console.

Fetch the JWKS

Some policies need the realm's public keys pasted inline — for example, kagent's AccessPolicy.jwksKey.inline only supports inline JWKS, not a remote URL. Fetch it:

curl -sS "${KC_HOST}/realms/${KC_REALM}/protocol/openid-connect/certs" \
  | tee /tmp/keycloak-jwks.json | jq

Teardown

helm -n kagent uninstall keycloak
kubectl -n kagent delete configmap keycloak-realm-import
kubectl -n kagent delete pvc -l app.kubernetes.io/instance=keycloak
# Optionally also drop the namespace if nothing else lives there:
#   kubectl delete namespace kagent

Labs that use this