2636 words
13 minutes
Zero Trust Kubernetes: Securing Cluster Access with Keycloak and Pinniped SSO

This guide details a battle-tested setup for implementing Zero Trust Kubernetes authentication in production — including common runtime pitfalls, configuration edge cases, and step-by-step troubleshooting solutions.


Background & Motivation#

While many engineering teams run workloads on Kubernetes in production, cluster access is frequently managed using legacy authentication anti-patterns:

  • Shared, static kubeconfig files distributed via Slack, email, or internal repositories.
  • Static ServiceAccount tokens that are never rotated.
  • Over-privileged cluster admin roles with zero individual user audit logging.

In this tutorial, we will set up Keycloak as a centralized Identity Provider (IdP) and Pinniped as the authentication bridge between Keycloak and your Kubernetes cluster.

Key Outcomes#

  • Self-service user login: Developers authenticate directly using their single sign-on (SSO) credentials via a browser window.
  • Short-lived sessions: User access automatically expires after 8 hours, eliminating static long-lived credentials.
  • Role-Based Access Control (RBAC): Permissions are dynamically mapped based on Keycloak user groups.
  • Stateless kubeconfig: Distribute a single, static kubeconfig template that contains zero credentials or secrets.

Prerequisites#

Before starting, ensure you have the following prerequisites in place:

  1. Kubernetes Cluster: A running Kubernetes cluster (e.g., set up via kubeadm or a managed provider).
  2. Keycloak Instance: Keycloak running and accessible over HTTPS. If you don’t have one set up yet, follow our guide on Deploying Keycloak 26 in Production on AlmaLinux 9.
  3. Ingress Controller: Traefik installed as an Ingress Controller on your cluster. For installation details, see Installing ArgoCD & Traefik HTTPS Ingress — you only need to complete the Install Traefik via Helm section.
  4. DNS Domain: A domain pointing to your cluster Ingress IP for exposing the Pinniped Supervisor (e.g., pinniped.example.com).

Configuration Placeholders#

The table below lists the placeholder variables used throughout this guide. Replace them with your environment’s specific values:

PlaceholderDescriptionExample
<KEYCLOAK_URL>Keycloak domain FQDNsso.example.com
<REALM_NAME>Keycloak realm nameinternal
<PINNIPED_SUPERVISOR_URL>FQDN assigned to Pinniped Supervisorpinniped.example.com
<K8S_API_URL>Kubernetes API Server endpoint and porthttps://192.168.1.100:6443
<YOUR_EMAIL>ACME registration email for cert-manageradmin@example.com
<USER_EMAIL>Test developer email addressdev-user@example.com

Understanding Pinniped#

Pinniped is a CNCF Sandbox project designed specifically to make Kubernetes authentication secure, seamless, and infrastructure-agnostic.

Without Pinniped, every Kubernetes cluster must be individually configured with API server OIDC flags (--oidc-issuer-url, --oidc-client-id, etc.) — requiring control plane restarts and tricky multi-tenant maintenance. Pinniped solves this by acting as an intermediary authentication broker.

[ Keycloak ] (Identity Provider)
│ OIDC Authorization Code Flow
[ Pinniped Supervisor ] (Central Broker / OIDC IdP)
│ Cluster-scoped Token Exchange
[ Pinniped Concierge ] (Per-cluster Agent)
│ Impersonate User & Groups
[ kube-apiserver ] (Cluster Control Plane)

Pinniped consists of two core components:

  1. Supervisor: Installed once (either on a dedicated management cluster or your main cluster). It acts as an OIDC identity broker, authenticating users against Keycloak and issuing cluster-scoped identity tokens.
  2. Concierge: Installed on every cluster you want to secure. It validates incoming tokens issued by the Supervisor and impersonates the identity when sending requests to kube-apiserver.

How Pinniped Kubeconfig Works#

Traditional kubeconfig files contain embedded client certificates or static tokens. Pinniped changes this paradigm entirely:

  • Credential-less configuration: The generated kubeconfig file contains no user credentials, certificates, or tokens.
  • Safe distribution: The exact same kubeconfig file can be safely stored in public or team internal Git repositories.
  • Browser-based flow: When a user executes kubectl, the Pinniped CLI plugin opens a local browser window to authenticate with Keycloak. Upon successful login, short-lived credentials are cached locally.

Architecture Overview#

┌──────────────────────────────────────────────┐
│ Keycloak (Identity Provider) │
└──────────────────────┬───────────────────────┘
│ OIDC Auth Code Flow
┌──────────────────────────────────────────────┐
│ Pinniped Supervisor │ ← TLS via cert-manager
│ - FederationDomain │
│ - OIDCIdentityProvider │
└──────────────────────┬───────────────────────┘
│ Cluster-Scoped Token Exchange
┌──────────────────────────────────────────────┐
│ Pinniped Concierge │ ← Deployed per downstream cluster
│ - JWTAuthenticator │
└──────────────────────┬───────────────────────┘
│ User Impersonation Header
┌──────────────────────────────────────────────┐
│ kube-apiserver │
└──────────────────────┬───────────────────────┘
│ Native RBAC Evaluation
│ (Group Claim: keycloak:<group>)
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ ns: dev │ │ ns: prod │ │ ns: staging │
│ dev-team │ │ ops-team │ │ qa-team │
│ (Role: edit)│ │(Role: admin)│ │(Role: view) │
└─────────────┘ └─────────────┘ └─────────────┘

CNCF Projects Involved#

ProjectCNCF StatusRole
KeycloakIncubatingIdentity Provider (SSO & User Directory)
PinnipedSandboxAuthentication Broker & Credential Distribution
cert-managerGraduatedAutomated TLS Certificate Management (Let’s Encrypt)
KubernetesGraduatedContainer Orchestration Engine

Part 1: Keycloak Configuration#

1.1 Create the OIDC Client#

  1. Log in to your Keycloak Admin Console and select your realm (<REALM_NAME>).
  2. Navigate to Clients → click Create client.
  3. Configure the client parameters:
FieldValue
Client typeOpenID Connect
Client IDkubernetes
Client authenticationOff (Public client)
Standard flowOn
Valid redirect URIshttp://localhost:8000, http://localhost:18000

Note: In Part 3, once the Pinniped Supervisor endpoint is configured, you must return here and add https://<PINNIPED_SUPERVISOR_URL>/* to Valid redirect URIs. Failing to do so will result in an Invalid parameter: redirect_uri error during browser authentication.


1.2 Add the Group Mapper#

To enforce Kubernetes RBAC based on Keycloak user groups, the JWT token issued by Keycloak must contain a groups claim.

  1. Under the kubernetes client, open the Client scopes tab.
  2. Select the dedicated scope (e.g., kubernetes-dedicated).
  3. Click Add mapperBy configurationGroup Membership.
  4. Set the following fields:
FieldValue
Namegroups
Token Claim Namegroups
Full group pathOff
Add to ID tokenOn
Add to access tokenOn
Add to userinfoOn

Important: groups here is a Token Claim Name, not an OIDC scope name. If you attempt a manual OAuth request with -d "scope=openid groups", Keycloak will reject it with invalid_scope. Always request scope=openid — the groups claim will automatically be included in the token payload once this mapper is enabled.

You can verify the supported scopes on your realm endpoint:

Terminal window
curl -s https://<KEYCLOAK_URL>/realms/<REALM_NAME>/.well-known/openid-configuration | jq '.scopes_supported'

1.3 Create Groups and Assign Users#

  1. In Keycloak sidebar, go to Groups and create:
    • dev-team
    • ops-team
    • qa-team
  2. Go to Users, select a user, open the Groups tab, and click Join Group to assign them to their respective team.

1.4 Test User Setup Checklist#

When creating test users in Keycloak, ensure all required user details are configured. Incomplete profiles will cause authentication failures:

RequirementConsequence if omitted
First Name & Last Name setError: Account is not fully set up
Email verified = On (Details tab)Error: email_verified claim in upstream ID token has false value
Password Temporary = Off (Credentials tab)Error: Account is not fully set up
Required user actions clearedError: Account is not fully set up

Tip: If Keycloak returns Account is not fully set up despite meeting all criteria above, reset the user password manually under CredentialsSet password.


1.5 Optional: Test Token Generation via Direct Access Grants#

Direct Access Grants (password grant type) are disabled by default in modern Keycloak versions for security reasons. To temporarily test token issuance via curl:

  1. Open Client kubernetesSettingsCapability config → enable Direct access grants → Save.
  2. Execute the token request:
Terminal window
curl -s -X POST \
https://<KEYCLOAK_URL>/realms/<REALM_NAME>/protocol/openid-connect/token \
-d "client_id=kubernetes" \
-d "username=<USER_EMAIL>" \
-d "password=<PASSWORD>" \
-d "grant_type=password" \
-d "scope=openid" \
| jq -r '.id_token' \
| cut -d. -f2 \
| base64 -d 2>/dev/null \
| jq .

Verify that the groups array is present in the decoded token payload:

{
"email": "dev-user@example.com",
"email_verified": true,
"groups": ["dev-team"],
"iss": "https://sso.example.com/realms/internal"
}

Security Note: Disable Direct access grants after testing. Production authorization always uses the standard Authorization Code Flow via browser.


1.6 Configure 8-Hour Session Lifespan#

In Keycloak Admin Console, open Realm SettingsSessions tab:

FieldLifespan
SSO Session Idle8 hours
SSO Session Max8 hours
Client Session Idle8 hours
Client Session Max8 hours

Next, select the Tokens tab:

FieldLifespan
Access Token Lifespan5 minutes
Refresh Token Max Reuse0

When Keycloak’s refresh token expires after 8 hours, Pinniped automatically prompts the user to re-authenticate via their browser on the next kubectl command execution.


Part 2: Installing cert-manager for Pinniped TLS#

Pinniped Supervisor requires a trusted TLS certificate for its endpoint. By default, Pinniped generates a self-signed bootstrap certificate that external CLI clients will reject with: tls: failed to verify certificate: x509: certificate is valid for pinniped-supervisor-bootstrap-cert.

We install cert-manager to automatically obtain and renew Let’s Encrypt TLS certificates as Kubernetes secrets.

2.1 Install cert-manager#

Terminal window
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml
# Monitor deployment status until all pods are Ready
kubectl get pods -n cert-manager --watch

2.2 Define ClusterIssuer and Certificate#

Create cert-manager-pinniped.yaml:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: <YOUR_EMAIL>
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
ingressClassName: traefik
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: pinniped-tls
namespace: pinniped-supervisor
spec:
secretName: pinniped-tls-cert
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- <PINNIPED_SUPERVISOR_URL>

Apply the configuration:

Terminal window
kubectl apply -f cert-manager-pinniped.yaml
# Verify Certificate status (Wait for READY: True)
kubectl get certificate pinniped-tls -n pinniped-supervisor --watch

Part 3: Deploying Pinniped Supervisor#

3.1 Install Pinniped Supervisor#

Terminal window
kubectl apply -f https://get.pinniped.dev/latest/install-pinniped-supervisor.yaml
# Verify pods
kubectl get pods -n pinniped-supervisor

3.2 Expose Supervisor via Traefik (TLS Passthrough)#

Because Pinniped Supervisor directly handles sensitive credential exchanges, it requires TLS Passthrough to perform end-to-end TLS termination directly inside the pod rather than at the Ingress controller layer.

  1. Create a ClusterIP Service for the Supervisor:
pinniped-supervisor-service.yaml
apiVersion: v1
kind: Service
metadata:
name: pinniped-supervisor
namespace: pinniped-supervisor
spec:
type: ClusterIP
selector:
app: pinniped-supervisor
ports:
- protocol: TCP
port: 443
targetPort: 8443
  1. Create a Traefik IngressRouteTCP with passthrough: true:
pinniped-supervisor-ingressroute.yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: pinniped-supervisor
namespace: pinniped-supervisor
spec:
entryPoints:
- websecure
routes:
- match: HostSNI(`<PINNIPED_SUPERVISOR_URL>`)
services:
- name: pinniped-supervisor
port: 443
tls:
passthrough: true

Apply both manifests:

Terminal window
kubectl apply -f pinniped-supervisor-service.yaml
kubectl apply -f pinniped-supervisor-ingressroute.yaml

3.3 Configure OIDCIdentityProvider#

Create the custom OIDCIdentityProvider resource to link Pinniped Supervisor to Keycloak:

apiVersion: idp.supervisor.pinniped.dev/v1alpha1
kind: OIDCIdentityProvider
metadata:
name: keycloak-idp
namespace: pinniped-supervisor
spec:
issuer: https://<KEYCLOAK_URL>/realms/<REALM_NAME>
authorizationConfig:
additionalScopes:
- email
allowPasswordGrant: false
claims:
username: email
groups: groups
client:
secretName: keycloak-client-secret

Note: Do not add groups to additionalScopes. Requesting groups as an OIDC scope will trigger an invalid_scope error from Keycloak.

Now, create the corresponding client secret. Pinniped requires a custom secret type secrets.pinniped.dev/oidc-client:

apiVersion: v1
kind: Secret
metadata:
name: keycloak-client-secret
namespace: pinniped-supervisor
type: secrets.pinniped.dev/oidc-client
stringData:
clientID: kubernetes
clientSecret: ""

Caution: Do not use kubectl create secret generic. Generic secrets have type Opaque, which will cause Pinniped to throw the error referenced Secret has wrong type "Opaque". Always apply via YAML with type: secrets.pinniped.dev/oidc-client.

Verify IdP health:

Terminal window
kubectl describe OIDCIdentityProvider keycloak-idp -n pinniped-supervisor
# Look for: Phase: Ready

If status remains in Phase: Error, restart the Supervisor deployment to reload secrets:

Terminal window
kubectl rollout restart deployment/pinniped-supervisor -n pinniped-supervisor

3.4 Configure FederationDomain#

The FederationDomain exposes the public OIDC issuer endpoint of the Supervisor, using the TLS certificate generated by cert-manager:

apiVersion: config.supervisor.pinniped.dev/v1alpha1
kind: FederationDomain
metadata:
name: kubernetes-federation
namespace: pinniped-supervisor
spec:
issuer: https://<PINNIPED_SUPERVISOR_URL>/kubernetes-federation
tls:
secretName: pinniped-tls-cert
identityProviders:
- displayName: "Keycloak SSO"
objectRef:
apiGroup: idp.supervisor.pinniped.dev
kind: OIDCIdentityProvider
name: keycloak-idp
transforms:
expressions:
- type: username/v1
expression: '"keycloak:" + username'
- type: groups/v1
expression: 'groups.map(g, "keycloak:" + g)'

Verify FederationDomain status:

Terminal window
kubectl describe FederationDomain kubernetes-federation -n pinniped-supervisor | grep "Phase\|Message"
# Expected output: Phase: Ready

Remember to update Keycloak client settings (Part 1.1) by adding https://<PINNIPED_SUPERVISOR_URL>/* to the client’s Valid redirect URIs.


Part 4: Deploying Pinniped Concierge#

4.1 Install Concierge#

Deploy the Pinniped Concierge agent on the target Kubernetes cluster:

Terminal window
kubectl apply -f https://get.pinniped.dev/latest/install-pinniped-concierge.yaml

4.2 Configure JWTAuthenticator#

The Concierge uses a JWTAuthenticator to validate authentication tokens issued by the Supervisor.

Extract the active TLS Certificate Authority bundle from pinniped-tls-cert:

Terminal window
LE_CA=$(kubectl get secret pinniped-tls-cert \
-n pinniped-supervisor \
-o jsonpath='{.data.tls\.crt}')

Apply the JWTAuthenticator resource:

Terminal window
kubectl apply -f - <<EOF
apiVersion: authentication.concierge.pinniped.dev/v1alpha1
kind: JWTAuthenticator
metadata:
name: supervisor-jwt-authenticator
namespace: pinniped-concierge
spec:
issuer: https://<PINNIPED_SUPERVISOR_URL>/kubernetes-federation
audience: https://<K8S_API_URL>
tls:
certificateAuthorityData: ${LE_CA}
EOF

Verify Authenticator status:

Terminal window
kubectl describe JWTAuthenticator supervisor-jwt-authenticator -n pinniped-concierge | grep "Phase\|Message"
# Expected output: Phase: Ready

Part 5: Kubeconfig Generation & Developer Workflow#

5.1 Install Pinniped CLI#

Install the pinniped CLI on your workstation:

Terminal window
# macOS via Homebrew
brew install pinniped-cli
# Linux Binary
curl -Lo pinniped https://get.pinniped.dev/latest/pinniped-cli-linux-amd64
chmod +x pinniped
sudo mv pinniped /usr/local/bin/pinniped

5.2 Generate Kubeconfig File#

As an administrator, generate the static kubeconfig template:

Terminal window
pinniped get kubeconfig \
--kubeconfig ~/.kube/config \
> kubeconfig-production.yaml

Note: In Pinniped CLI v0.47.0+, the --pinniped-namespace flag was deprecated in favor of automatic discovery.

This kubeconfig-production.yaml file contains no user secrets. Upload it to your team’s internal documentation site or repository.


5.3 Workstation Setup for Developers#

Developers set up cluster access on their machines in two steps:

Terminal window
# 1. Download generic kubeconfig from internal docs
curl -O https://internal-docs.example.com/kubernetes/kubeconfig-production.yaml
# 2. Install Pinniped CLI
brew install pinniped-cli

Path Troubleshooting: The generated kubeconfig invokes /usr/local/bin/pinniped. On Apple Silicon Macs, Homebrew installs binaries under /opt/homebrew/bin/pinniped. Create a symbolic link to prevent exec: fork/exec /usr/local/bin/pinniped: no such file or directory:

Terminal window
sudo ln -s $(which pinniped) /usr/local/bin/pinniped

5.4 First-Time Login Experience#

When a developer runs kubectl using the Pinniped kubeconfig:

Terminal window
export KUBECONFIG=./kubeconfig-production.yaml
kubectl get pod -n dev
  1. Pinniped automatically launches the system web browser pointing to Keycloak SSO.
  2. The user enters their Keycloak credentials.
  3. The browser displays “Login Succeeded”.
  4. Control returns to the terminal, and kubectl executes seamlessly.

5.5 Daily Session Flow#

Morning (08:00):
kubectl get pods ──▶ Browser opens ──▶ User authenticates
└── Session active for 8 hours ✅
Afternoon (12:00):
kubectl get pods ──▶ Seamless background token refresh ✅
End of Shift / Next Day (After 8 Hours):
kubectl get pods ──▶ Refresh token expired
└── Browser opens automatically for standard re-login ✅

To manually clear session credentials (e.g., when testing different users or groups):

Terminal window
# Linux
rm -rf ~/.config/pinniped/sessions.yaml
# macOS
rm -rf ~/Library/Application\ Support/pinniped/sessions.yaml

Part 6: Multi-Tenant RBAC & Workload Setup#

Apply namespaces, RBAC bindings, and test pods across development, production, and staging environments:

06-rbac-and-pods.yaml
---
# NAMESPACE: dev — Granted to keycloak:dev-team (Role: edit)
apiVersion: v1
kind: Namespace
metadata:
name: dev
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: dev-team-edit
namespace: dev
subjects:
- kind: Group
name: "keycloak:dev-team"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: edit
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: Pod
metadata:
name: demo-frontend
namespace: dev
spec:
containers:
- name: frontend
image: nginx:alpine
---
apiVersion: v1
kind: Pod
metadata:
name: demo-backend
namespace: dev
spec:
containers:
- name: backend
image: hashicorp/http-echo:alpine
args: ["-text=Backend Service running in DEV"]
---
apiVersion: v1
kind: Pod
metadata:
name: demo-database
namespace: dev
spec:
containers:
- name: database
image: postgres:15-alpine
env:
- name: POSTGRES_PASSWORD
value: "devpassword"
---
# NAMESPACE: prod — Granted to keycloak:ops-team (Role: admin)
apiVersion: v1
kind: Namespace
metadata:
name: prod
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ops-team-admin
namespace: prod
subjects:
- kind: Group
name: "keycloak:ops-team"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: admin
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: Pod
metadata:
name: demo-payment
namespace: prod
spec:
containers:
- name: payment
image: hashicorp/http-echo:alpine
args: ["-text=Payment Service in PROD"]

Apply the resources:

Terminal window
kubectl apply -f 06-rbac-and-pods.yaml

Part 7: End-to-End Verification & CLI Testing#

7.1 Verify Active User Identity#

Run kubectl auth whoami to verify how the API server perceives your authenticated identity and groups:

Terminal window
$ kubectl auth whoami
ATTRIBUTE VALUE
Username keycloak:dev-user@example.com
Groups [keycloak:dev-team system:authenticated]
Extra: authentication.kubernetes.io/credential-id [X509SHA256=e5cfbbef0c518879defcaa995b2a4e578522c68d691d1ae9f910e759b5b7c5b7]

7.2 Access Control Verification#

Test resource listing under allowed versus restricted namespaces:

Allowed Access (dev namespace)#

Terminal window
$ kubectl get pod -n dev
NAME READY STATUS RESTARTS AGE
demo-backend 1/1 Running 0 9h
demo-database 1/1 Running 0 9h
demo-frontend 1/1 Running 0 9h

Forbidden Access (prod namespace for dev-team member)#

Terminal window
$ kubectl get pod -n prod
Error from server (Forbidden): pods is forbidden: User "keycloak:dev-user@example.com" cannot list resource "pods" in API group "" in the namespace "prod"

Pre-flight Permission Check#

Terminal window
$ kubectl auth can-i create deployments -n prod
no

Troubleshooting Guide#

Symptom / ErrorRoot CauseResolution
unauthorized_client: Client not allowed for direct access grantsDirect access grants disabled in Keycloak settingsEnable temporarily under Client Settings tab if testing via curl.
invalid_scope: Invalid scopes: openid groupsgroups passed as scope parameterOmit groups from scope argument; standard scope=openid includes claim.
Account is not fully set upMissing user profile dataEnsure First Name, Last Name, Email Verified=True, and clear temporary passwords.
Secret has wrong type "Opaque"Created via kubectl create secret genericApply via YAML setting type: secrets.pinniped.dev/oidc-client.
tls: certificate is valid for pinniped-supervisor-bootstrap-certUsing self-signed Supervisor certificateDeploy cert-manager Certificate and reference TLS secret in FederationDomain.
Invalid parameter: redirect_uriCallback URL missing in KeycloakAdd https://<PINNIPED_SUPERVISOR_URL>/* to Valid Redirect URIs in Keycloak.
email_verified claim in upstream ID token has false valueUnverified user email in KeycloakToggle Email Verified to On under Keycloak User details.
fork/exec /usr/local/bin/pinniped: no such file or directoryCLI installed outside standard pathSymlink binary: sudo ln -s $(which pinniped) /usr/local/bin/pinniped.
could not complete Concierge credential exchange: authentication failedOutdated TLS CA in ConciergeRe-extract tls.crt from pinniped-tls-cert and update JWTAuthenticator.
Error: unknown flag: --pinniped-namespaceDeprecated CLI flagRemove flag; Pinniped CLI auto-detects Concierge namespace in modern versions.

Production Lessons Learned#

  1. Group Mapper is Critical: Without adding the groups mapper under Client Scopes, authentication completes but RBAC bindings continuously fail.
  2. Treat groups as Claims, Not Scopes: Passing groups as an OIDC scope parameter breaks Keycloak protocol validation.
  3. Pinniped Supervisor Requires Valid Public TLS: Never expose Supervisor using self-signed bootstrap certs in production environments.
  4. Stateless Kubeconfig Security: Since Pinniped kubeconfig files contain no secrets, credential leaks via repository commits are completely mitigated. Access revocation is performed instantly in Keycloak.

Architecture Summary#

AspectLegacy KubeconfigPinniped + Keycloak Zero Trust
CredentialsStatic embedded tokens / certsShort-lived OIDC tokens (8h max)
User IdentificationGeneric / Shared credentialsPer-user identity and group claims
Audit TrailAnonymous / Shared admin logsDistinct per-user audit trails in API server
Access RevocationManual cluster cert rotationSingle-click disable in Keycloak
Developer OnboardingAdmin generates custom kubeconfigSelf-service download of static template

Official Documentation & References#

Zero Trust Kubernetes: Securing Cluster Access with Keycloak and Pinniped SSO
https://im-gatan.com/posts/zero-trust-kubernetes-keycloak-pinniped/
Author
Gatan
Published at
2026-07-24
License
CC BY-NC-SA 4.0