How I Stopped Using Client Secrets in Kubernetes (And Why I'm Never Going Back)
Every Kubernetes workload that calls a protected service needs a credential.
The default answer is a client secret. You create one in Keycloak, copy it into a Kubernetes Secret, wire it up as an environment variable, and move on.
It works.
But it also means you’ve just created a shared secret that needs to live somewhere, get rotated eventually, stay in sync across environments, and not leak.
In practice, client secrets rarely get rotated. They end up in version control, CI pipelines, and Slack messages. When they do leak, you don’t always know immediately. And when you’re managing dozens of services, the operational surface adds up.
What if the workload didn’t need a secret at all because it already has an identity?
The identity that was already there
Every Kubernetes pod runs as a service account. That service account is an actual identity within the cluster, backed by a cryptographic key that Kubernetes controls.
Since Kubernetes 1.20, you can request projected service account tokens.
Short-lived JWTs that Kubernetes mints specifically for your workload, signed with the cluster’s private key, and mounted into the pod at a path you define.
The Kubelet manages rotation: before the token expires, it quietly replaces it. The application just reads from a file path.
These tokens carry structured claims:
iss - the cluster’s OIDC issuer URL
sub - the full service account identity (system:serviceaccount:<namespace>:<name>)
aud - a scoped audience you control
exp - an expiry you configure
The audience field is the critical detail. A token with aud: <http://keycloak.example.com/realms/kubernetes> is scoped to Keycloak specifically. Even if it's captured in transit, it can't be replayed against any other system.
volumes:
- name: kctoken
projected:
sources:
- serviceAccountToken:
path: kctoken
expirationSeconds: 600
audience: http://keycloak.keycloak.svc.cluster.local:8080/realms/kubernetesThe credential is already there.
Kubernetes as an OIDC identity provider
The other half of this is that Kubernetes exposes a JWKS endpoint.
At https://kubernetes.default.svc.cluster.local/openid/v1/jwks, the cluster publishes the public keys used to sign its service account tokens. Any system that can reach this endpoint can verify that a given JWT was genuinely minted by that specific cluster.
Try it out. A cluster exposes two standard OIDC endpoints:
/.well-known/openid-configuration ⇒ the standard OIDC Discovery Document tells external systems how to interact with your Kubernetes cluster's identity provider.
kubectl get --raw /.well-known/openid-configuration | jq
{
"issuer": "https://kubernetes.default.svc.cluster.local",
"jwks_uri": "https://172.18.0.2:6443/openid/v1/jwks",
"response_types_supported": [
"id_token"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
]
}/openid/v1/jwks ⇒ the endpoint serves the JWKS, which contains the public keys used by the API server (or the Service Account issuer) to sign Service Account OIDC tokens.
kubectl get --raw /openid/v1/jwks | jq
{
"keys": [
{
"use": "sig",
"kty": "RSA",
"kid": "KpuJlKenJ_Vu3flp-xJ-p4jMzrYGwbpxiQnEDTC-DQw",
"alg": "RS256",
"n": "4ixGviGT0e4qEleqt7G2tyVNIl-cBk3_4xLQ9675UrzL5gzKtlfP4AixVYlBi5xgalnWgl8wy_0xHOtXEu8PylDmTRhuOc6n6PUdt2Pw5nXdpxu6yYja4092sHPgIVSM_Hb60pYGL83oG8XAHfePN4OF95oW9PLi9iuOgXrsxSqP9GxIXxwMuhyb4BiTNSHacSngR18JrER3aKGUbqkfdKzYedQX-hbcseH35s8t_d4W1EVjRbBcuFBOSm9pg-aBJQjc0veJ9cfPUyH831nWhpqz0eBJkHCG_WFr1dhXgBInhRo1qsjAAbZWmAVRo3uofYiMw7W9rKg3qYvpXw9NiQ",
"e": "AQAB"
}
]
}This is standard OIDC. Keycloak already knows how to consume it.
When you configure a Kubernetes identity provider in Keycloak, you point it at this issuer URL. Keycloak fetches the JWKS, caches the keys, and can now validate any projected SA token from that cluster.
No shared secret needs to be configured between Kubernetes and Keycloak.
Trust is established purely through public-key cryptography. Keycloak trusts what Kubernetes signed.
The JWT Bearer Token grant ties it together
The exchange mechanism is defined in RFC 7523 (the JWT Bearer Token grant). It’s an extension of OAuth 2.0’s client_credentials flow, but instead of a client_secret, the client sends a signed JWT as client_assertion.
The request to Keycloak’s token endpoint looks like this:
POST /realms/kubernetes/protocol/openid-connect/token
grant_type=client_credentials
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertion=<the projected SA token>
client_id=myclientKeycloak receives the assertion, validates its signature against the Kubernetes JWKS, and checks:
iss - does this match the configured identity provider alias?
sub - does this service account identity match the client’s configuration?
aud - is the token addressed to this Keycloak realm?
exp - has it expired?
If all checks pass, Keycloak issues a signed access token.
No shared secret ever changes hands between the workload and Keycloak.
Keycloak's federated client authentication
This is the piece that Keycloak marked in its 26.x builds as supported: clientAuthenticatorType=federated-jwt.
When you configure a Keycloak client with this authenticator type, you declare which external identity provider to trust and which subject maps to this client.
The configuration is purely claim-based:
clientAuthenticatorType = federated-jwt
jwt.credential.issuer = kubernetes
jwt.credential.sub = system:serviceaccount:service-a:my-serviceaccount
jwt.credential.audience = http://keycloak.keycloak.svc.cluster.local:8080/realms/kubernetesNotice what’s absent: there is no clientSecret field.
There’s nothing to create, store, or rotate.
The trust relationship is expressed entirely through expected JWT claims, backed by cryptographic validation at runtime.
Keycloak looks at the incoming client_assertion, verifies it came from the trusted Kubernetes cluster, checks that the subject matches the configured service account, and confirms the audience is correct.
If all three match, the client is authenticated without a password, without a shared secret, without anything stored in etcd.
Security gains
Short-lived by design. The SA token expires in 600 seconds. The Keycloak access token in approximately 300 seconds. A captured token has a narrow window before it’s worthless.
No rotation burden. The Kubelet replaces the SA token file automatically before expiry. The application reads it fresh when needed. There’s no rotation job to write, no secret-sync operator to run, no quarterly reminder to rotate credentials.
Audience-scoped tokens at every hop. The SA token is only good for Keycloak any other system will reject it because the audience doesn’t match. The access token is only good for a specific service, for the same reason. A token leaked from one layer cannot be replayed at another layer in the chain.
Nothing sensitive in etcd. A Kubernetes Secret containing a client secret is a credential at rest, encrypted or not. Here, nothing of that nature exists. The projected SA token lives only in memory inside the pod and in the mounted file path.
Cryptographic provenance. Every token in the chain is verifiable against a public key. There is no password database, no shared state, no “who knows this secret” question. Either the signature is valid and the claims match, or it isn’t.
The bottom line
Static client secrets are a habit, not a requirement. The default answer, create a secret, store it in a Secret, rotate it eventually, exists because it’s the lowest common denominator, not because it’s the right model.
Kubernetes already mints and rotates workload credentials. Every pod has a service account. Every service account can produce a short-lived, audience-scoped JWT that any OIDC-aware system can validate. That is a complete identity.
Keycloak’s federated client authentication turns that existing identity into a trust anchor. The mechanism is standard, the pieces are already in place, and the operational overhead of client secrets doesn’t have to be the cost of doing business.
Over to you
Are you still managing client secrets for your Kubernetes workloads? I’d love to hear how you’re handling workload auth in your environment.
Drop a comment or hit reply. I read every response and I’d love to hear your thoughts.
That’s all for today! Thank you and see you soon!
If you found this helpful subscribe to Opinionated Ops!

