BuildNot yet confirmed elsewhere1 publisher3 min readPublished
ALB validates the JWT, Keycloak decides what is in it: the Terraform for the matching realm
A dev.to walkthrough publishes the Keycloak stack behind an EKS gateway auth setup: one enabled grant, four redirect URLs, and a group membership Terraform does not fully own.
The Engineer · Build desk

What happened
- A follow-up to an ALB Gateway API JWT-offload experiment publishes the Terraform/OpenTofu stack that configures Keycloak as the matching OIDC provider for EKS.
- The cluster-facing client is confidential with a client secret, and its authorization code flow is the only grant left switched on.
- A kube-api client scope holds the protocol mappers, and the one shown copies the username attribute into both the access token and the ID token.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint There is no scripted way to mint a cluster token here: a human has to finish a browser redirect to a loopback port or a Postman callback, which is why automation needs a separate federated client...
- exposure Declaring group membership non-exhaustively means anyone dropped into kube-admin through the console keeps that grant through every future apply, and no plan output will surface it.
- decision Because claim names are decided in the realm's client scope, the team writing cluster RBAC has to settle the claim contract with the realm owner before either side can ship independently.
- contradiction The prose says the client role reaches the token's role claim, but the mappers actually published stop at username, so copying this stack verbatim yields an accepted token with nothing to...
The gateway checks a signature and looks for claims. It cannot add one. So the useful part of this stack is not the ALB policy but the client scope, which the author describes as the shared set of protocol mappers attached to the client [12]: whatever is not mapped there does not exist as far as the cluster is concerned.
The narrowing starts with the client flags. On the `kube-api` client, one flow toggle is on, `standard_flow_enabled`, which the author identifies as the authorization code grant [5]. Six are off: implicit, direct access grants, service accounts, standard token exchange, the OAuth2 JWT authorization grant and device authorization [6][18]. `full_scope_allowed = false` limits the token to roles scoped to this client [7]. With the direct access grant disabled, there is no password-grant shortcut for a test token; anything the gateway sees has to come back through one of the four registered redirect URLs [8][19], two loopback ports for kubelogin [9] and two Postman callbacks [10]. That constraint is exactly why runners need their own path, and the author's JWT-federated GitHub Actions client is deferred to a future article [3].
The membership side is looser than the client side. `keycloak_user_groups` is declared with `exhaustive = false` [13], and the `kube-admin` group carries the client role named `admin`, described in the code as the cluster-admin role for kube-api [14][11]. Non-exhaustive means Terraform owns the rows it declares and ignores the rest, so a hand-added member of that group is not drift anyone will be shown.
Two other lines are worth reading as policy rather than plumbing. `email_verified = true` is set from config for every user the stack creates [15], which is the realm asserting a fact about a person rather than the person proving it. And `lifecycle { ignore_changes = [first_name, last_name] }` [16] hands display names back to the directory while leaving `username` under Terraform control, which matters because `username` is the one attribute the article shows being copied into both the access token and the ID token [17][20]. Rename it in config and you rename the subject the cluster authorizes.
Which is where the walkthrough stops short of its own promise. The role resource is said to appear in the token's role claim once assigned to a user [11], but the mapper set published here contains only the username mapper, and the text breaks off mid-explanation of `add_to_id_token` [22][23]. Follow it literally and you get a token the gateway will accept carrying an identity and no authorization data.
One last asymmetry. The provider itself authenticates with a service-account client using the client credentials grant, driven by five environment variables and an otherwise empty `provider "keycloak" {}` block [2]. Service accounts are precisely what the `kube-api` client forbids [6][21]. The realm-management credential and the human credential are different clients with opposite capabilities, which is correct, and also means the secret that can rewrite every role mapping in this stack lives wherever the plan runs.
What to watch
- The promised write-up of the GitHub Actions JWT-federated client, currently the only identity path in this stack with no configuration shown.
- Whether group membership moves to exhaustive ownership, which decides the fate of any kube-admin member added by hand in the console.
- Which mapper is published alongside username in the kube-api client scope, and whether the gateway policy reads the access token or the ID token.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence66
- Adoption12
- Hype gap−8
- Incentives28
- Confidence58
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
The author previously tested using ALB CRDs to offload JWT validation for OIDC-based authentication to the Amazon EKS Kubernetes API, and this article supplies the Terraform/OpenTofu stack that sets up Keycloak as the OIDC provider.
- [2]
The official Keycloak Terraform provider is configured with a service-account client (OAuth 2.0 client credentials grant) and five environment variables (KEYCLOAK_URL, KEYCLOAK_REALM, KEYCLOAK_BASE_PATH, KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET), so an empty provider "keycloak" {} block is enough.
- [3]
For GitHub Actions runners the author used a JWT-federated client and says it deserves a dedicated article.
- [4]
The kube-api client sets access_type = CONFIDENTIAL and client_authenticator_type = client-secret, which the author explains creates a non-public client with a client secret.
- [5]
standard_flow_enabled = true, which the author explains enables the Authorization Code Grant flow.
- [6]
The kube-api client sets implicit_flow_enabled, direct_access_grants_enabled, service_accounts_enabled, standard_token_exchange_enabled, oauth2_jwt_authorization_grant_enabled and oauth2_device_authorization_grant_enabled all to false.
- [7]
full_scope_allowed = false, which the author explains means the token contains only the roles scoped for this client.
- [8]
valid_redirect_uris lists four URLs: http://localhost:8000, http://localhost:18000, https://oauth.pstmn.io/v1/callback and https://oauth.pstmn.io/v1/browser-callback, described as the redirect URLs allowed for the standard flow.
- [9]
The two localhost redirect URIs (ports 8000 and 18000) are for kubelogin.
- [11]
A keycloak_role named "admin" is created against the kube_api client with the description "cluster-admin role for kube-api", and the author states this client-specific role will be included in the token's role claim if it is assigned to a user.
- [12]
The author describes shared client configuration as a client scope, in this example simply a set of protocol mappers, created as keycloak_openid_client_scope named kube-api.
- [13]
keycloak_user_groups assigns users to the kube-admin group with exhaustive = false, iterating over the users in local config whose groups list contains the group name.
- [14]
A keycloak_group named kube-admin is created and keycloak_group_roles maps the client-specific kube_api admin role to that group.
- [15]
The keycloak_user resource creates users from local.config.keycloak.users with email_verified = true.
- [16]
The keycloak_user resource carries lifecycle { ignore_changes = [first_name, last_name] }.
- [17]
A keycloak_openid_user_attribute_protocol_mapper on the kube-api client scope maps the username user attribute to a claim named username with add_to_access_token = true and add_to_id_token = true.
- [18]
Of the seven grant and flow toggles set on the kube-api client, exactly one is enabled (the authorization code flow) and six are disabled.
- [19]
With the direct access (password) grant disabled and the authorization code flow the only one enabled, any token for the cluster must be obtained through a browser redirect to one of the four registered redirect URLs.
- [20]
Terraform retains control of the username value that becomes the token claim, while explicitly relinquishing control of first and last name.
- [21]
The client credentials grant the Terraform provider depends on is the same capability disabled on the kube-api client, so realm management and cluster login use separate clients with opposite grant sets.
- [22]
The supplied text of the article ends mid-sentence while explaining add_to_id_token, with the username mapper the only protocol mapper shown in full.
- [23]
The published configuration promises a role claim but contains no role or group mapper, so a token built from exactly what is shown carries only a username.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toOIDC: Keycloak setup for ALB Gateway API
1 article · August 22, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.