⚠️ AI-generated & untested — read before using. This repository was generated by Claude (Anthropic's Claude Code, Opus 4.8) while designing a portable, multi-distribution Helm layout for Traefik + Keycloak. It has not been deployed or tested on a real cluster — neither OpenShift, RKE2, k3s, kubeadm nor Tanzu. Validation so far is limited tohelm template,helm lintand offline YAML/schema checks — no live install, no end-to-end auth flow has been exercised. Treat it as a reviewed starting point: read every manifest and values file, replace the dummy values (domains, realms, client secrets, LB pools, image registries), and validate in a non-production environment before any real use.
A single umbrella Helm chart that deploys the official Traefik chart
(vendored) behind a LoadBalancer, with the Traefik dashboard authenticated
against Keycloak (oauth2-proxy + ForwardAuth) and restricted by role
(traefik-admin). It is portable across on-prem / air-gapped Kubernetes
distributions — OpenShift, Rancher RKE2, k3s, kubeadm, VMware Tanzu/TKG —
driven by a small set of feature flags, with fail-fast validation that
rejects impossible combinations at helm template time instead of shipping a
broken deployment.
Rather than a per-distribution fork, one core composes along a few independent axes:
- Distribution —
platform: OpenShift (SCC-aware UID handling) · RKE2 · k3s · kubeadm · generic. (§2, §4) - Load balancer —
loadBalancer.backend: MetalLB · NSX ALB (Avi) · kube-vip · Cilium LB-IPAM · generic. (§4) - Secrets —
secret.mode: pre-created (Sealed Secrets) · inline (dev) · External Secrets Operator + HashiCorp Vault. (§10) - Keycloak TLS trust —
caTrust.mode: none · OpenShift CA injector · custom CA ConfigMap · insecure (test). (§9) - Network — optional NetworkPolicies for default-deny CNIs (Calico / Cilium); storage (CSI) is not applicable — the workload is stateless. (§11)
- Air-gap — vendored Traefik chart (no
traefik.github.iopull) + internal registry-mirror overrides for every image. (§12) - Delivery — imperative (
install.sh) or GitOps (single multi-source ArgoCD Application). (§8, §16)
This is the multi-distribution sibling of
traefik-keycloak-openshift-gitops(OpenShift-only). If you deploy on OpenShift exclusively, either works; this repo generalises the same design to any distribution.
This repository provides an enterprise, production-ready umbrella Helm chart that deploys the official Traefik Ingress Controller (v3.7+) with a Keycloak-protected dashboard (oauth2-proxy + ForwardAuth), portable across Red Hat OpenShift, Rancher RKE2, k3s, kubeadm, and VMware Tanzu. Use this map to navigate the repository layout, architectural documentation, and multimedia series:
traefik-keycloak-portable/ # 🌐 Multi-Distribution Ingress & Keycloak SSO Platform
├── 📁 argocd/ # Declarative GitOps Multi-Source Delivery
│ ├── 📁 apps/ # ArgoCD Applications (traefik-keycloak)
│ ├── 📄 project.yaml # AppProject scoping repos, namespaces, and cluster resources
│ └── 📄 README.md # Multi-source $values GitOps pattern & runbooks
├── 📁 docs/ # Architectural Deep Dives & Distribution Guides
│ ├── 📄 air-gapped.md # Disconnected deployment, vendoring & image mirroring
│ ├── 📄 external-secrets.md # HashiCorp Vault ESO SecretStore & token sync
│ ├── 📄 network-policies.md # Calico & Cilium default-deny NetworkPolicies
│ └── 📄 tls-secret.md # CA trust chains, cert-manager & TLS secret injection
├── 📁 helm/ # Umbrella Helm Chart & Templates
│ └── 📁 traefik-keycloak/ # Portable umbrella chart (Chart v41.0.2 / Traefik v3.7.6)
│ ├── 📁 charts/ # Vendored official Traefik subchart (offline-ready)
│ ├── 📁 templates/ # Core oauth2-proxy, IngressRoute & _validate.tpl
│ └── 📄 values.yaml # Base values & orthogonal feature flag defaults
├── 📁 keycloak/ # Identity Provider Configuration & Setup
│ └── 📄 keycloak-client-setup.md # OIDC client, mapper & traefik-admin role guide
├── 📁 metallb/ # Bare-Metal Layer 2 / BGP Load Balancing
│ └── 📄 ipaddresspool.example.yaml # On-prem IPAddressPool & L2Advertisement definition
├── 📁 secrets/ # Secret Synchronization Templates
│ └── 📄 oauth2-proxy-secret.example.yaml # Sealed/Vault secret reference for cookie & client secrets
├── 📁 sites/ # Distribution-Specific Values Overlays
│ ├── 📄 values-openshift.yaml # Red Hat OpenShift (restricted-v2 SCC, dynamic UIDs)
│ ├── 📄 values-rke2.yaml # Rancher RKE2 (Cilium LB-IPAM, non-root)
│ ├── 📄 values-k3s.yaml # Lightweight edge k3s (MetalLB, Traefik disabled)
│ ├── 📄 values-kubeadm.yaml # Bare-metal kubeadm (MetalLB, local CA ConfigMap)
│ ├── 📄 values-tanzu-nsx.yaml # VMware Tanzu / TKG with NSX ALB (Avi Vantage)
│ └── 📄 values-tanzu-kubevip.yaml # VMware Tanzu / TKG with kube-vip virtual IP
├── 📄 CHANGELOG.md # Release history & version notes
├── 📄 install.sh # Imperative deployment script with pre-flight linting
└── 📄 PORTABILITY.md # Portability matrix across distros & load balancers
This repository is accompanied by an educational video masterclass series and technical shorts synthesized with Gemini NotebookLM based directly on the multi-distribution architecture, ForwardAuth security flows, and air-gapped runbooks from this project. All videos are freely accessible on YouTube on the @nubenetes channel.
Note
Multilingual Learning Experience:
Content features sessions with original spoken audio in English 🇺🇸, with automated YouTube closed captions (CC) translated into 20+ languages for global platform engineering and SRE teams.
| # | Video Guide Title | Engineering Domain & Core Architecture | Audio | Duration | Direct Link |
|---|---|---|---|---|---|
| 01 | Portable Traefik & Keycloak Helm Chart | Multi-Distro Architecture & Feature Flags Orthogonal axes, fail-fast validation in _validate.tpl, vendored offline charts |
🇺🇸 EN | 9:26 |
|
| 02 | Multi-Distro Kubernetes Portability | Platform Engineering & Distro Quirks OpenShift restricted-v2 SCC vs vanilla K8s, dynamic UIDs & CNI NetworkPolicies |
🇺🇸 EN | 9:26 |
|
| 03 | Traefik & Keycloak OIDC Authentication | Zero-Trust Ingress & ForwardAuth oauth2-proxy token exchange, statusRewrites 401 to 302, role RBAC ( traefik-admin) |
🇺🇸 EN | 7:04 |
|
| 04 | Portable Kubernetes Networking | On-Premises LoadBalancer Backends MetalLB (L2/BGP), VMware NSX ALB (Avi), kube-vip virtual IP & Cilium LB-IPAM |
🇺🇸 EN | 7:52 |
|
| 05 | End-to-End Portable Traefik & Keycloak | Air-Gapped GitOps Masterclass Full Day 0 to Day 2 lifecycle, HashiCorp Vault ESO sync, multi-source ArgoCD |
🇺🇸 EN | 9:46 |
| # | Short Title | Architectural Domain & Focus | Audio | Duration | Action |
|---|---|---|---|---|---|
| 01 | The Ultimate Portable Traefik & Keycloak Helm Chart | Unified Ingress & OIDC Zero forks, ForwardAuth middleware, status 401 to 302 rewrite & role-gating |
🇺🇸 EN | 1:30 |
|
| 02 | How Air-Gapped Kubernetes Clusters Install Ingress | Air-Gapped Architecture Vendored Helm dependencies & internal OCI mirrors for disconnected datacenters |
🇺🇸 EN | 1:03 |
|
| 03 | How External Secrets Operator Connects Vault | Zero-Trust Secret Sync Bridging HashiCorp Vault into Kubernetes secrets without committing passwords |
🇺🇸 EN | 1:02 |
|
| 04 | Why OpenShift SCC Breaks Standard Pods | OpenShift Hardening restricted-v2 dynamic UID ranges vs hardcoded non-root UIDs (65532) |
🇺🇸 EN | 1:20 |
For complete technical summaries, topic breakdowns, and direct studio links, see Section 18: Video Walkthroughs & Architecture References.
- Architecture
- How portability works (orthogonal axes)
- Deployment topology
- Supported platforms & presets
- Repository layout
- Prerequisites
- Configuration
- Step-by-step install
- Feature-flag reference
- Secrets management
- CNI & CSI
- Air-gapped / disconnected
- Upgrade
- Decommission
- Operations & troubleshooting
- GitOps with ArgoCD
- Contributing & license
- Video Walkthroughs & Architecture References (YouTube)
The authentication flow is the same on every platform — only the load balancer and the pod-security profile differ.
- Traefik OSS has no native OIDC.
oauth2-proxyperforms the OIDC exchange with Keycloak; Traefik only asks "is this request authenticated?" through theForwardAuthmiddleware against/oauth2/auth. - The
errorsmiddleware withstatusRewrites: "401": 302turns the 401 into a real redirect to Keycloak. Requires Traefik ≥ v3.4. Without it you would see a blank 401 instead of the login page. - Role gate:
oauth2Proxy.allowedRoles=traefik-dashboard:traefik-adminonly lets users carrying that role through; everyone else gets 403 after login.
Diagram — authentication flow (click to expand)
flowchart TD
B["Browser"] -->|"HTTPS :443 — TLS terminates in Traefik"| T["Traefik (websecure entrypoint)"]
T -->|"IngressRoute /dashboard, /api<br/>middlewares: [oauth-errors, oauth-auth]"| AUTH["oauth-auth (ForwardAuth)<br/>GET /oauth2/auth"]
AUTH --> OP["oauth2-proxy"]
OP -->|"202 authenticated"| DASH["api@internal<br/>(dashboard loads)"]
OP -->|"401 not authenticated"| ERR["oauth-errors<br/>rewrites 401 → 302 /oauth2/sign_in"]
ERR -->|"redirect"| T
T -->|"IngressRoute /oauth2/*"| OP
OP -->|"OIDC (skip provider button)"| KC["Keycloak (realm)"]
KC -->|"login OK + role traefik-admin → session cookie"| OP
There is one core (Traefik + oauth2-proxy + the dashboard routes, identical
everywhere). Portability comes from a few independent axes you pick from; the
chart renders the same core and _validate.tpl
rejects the combinations that cannot work. This is why there is no per-platform
fork of the chart — you compose one, you don't copy it.
Diagram — orthogonal axes compose one core (click to expand)
flowchart LR
subgraph AXES["Pick one value per axis"]
direction TB
P["platform<br/>openshift · rke2 · k3s · kubeadm · generic"]
LB["loadBalancer.backend<br/>metallb · nsx-alb · kube-vip · generic"]
CA["caTrust.mode<br/>none · openshift-injector · configmap · insecure"]
SEC["secret.mode<br/>external · inline"]
REG["image.registry<br/>public · internal mirror (air-gap)"]
end
AXES --> CORE["traefik-keycloak umbrella chart<br/>(same core: Traefik + oauth2-proxy + routes)"]
CORE --> VAL{"_validate.tpl<br/>rejects impossible combos"}
VAL -->|"valid"| OUT["Rendered manifests"]
VAL -->|"invalid"| ERRP["helm aborts with an actionable message"]
classDef axis fill:#DDE8FF,stroke:#3B6FD4,color:#111;
classDef gate fill:#FFF3CD,stroke:#E0A800,color:#111;
class P,LB,CA,SEC,REG axis;
class VAL gate;
The axes are genuinely independent — e.g. OpenShift can use MetalLB or another LB, and Tanzu can use NSX ALB or kube-vip. See §4 for the combinations that matter; the full Cartesian product is intentionally not enumerated (hundreds of rows describing the same core).
Full picture — delivery (GitOps) + runtime, all components and the optional pieces (External Secrets/Vault, air-gap mirror, the four LB backends). Node colours map to the legend below.
Diagram — full architecture (delivery + runtime, all components) (click to expand)
flowchart TB
subgraph EXT["⬜ Client · on-prem (may be air-gapped)"]
direction TB
BROWSER["Browser"]
DNS["DNS · A record → LB IP"]
end
subgraph GIT["🔁 GitOps · ArgoCD (optional delivery)"]
direction TB
REPO[("Git repo")]
APP["Application: traefik-keycloak<br>vendored chart + $values preset"]
PROJ["AppProject: traefik"]
REPO --> APP
PROJ -. scopes .-> APP
end
subgraph LBG["🟦 LoadBalancer · pick one (loadBalancer.backend)"]
direction TB
METAL["MetalLB"]
NSX["NSX ALB (Avi)"]
KVIP["kube-vip"]
CIL["Cilium LB-IPAM"]
end
subgraph NS["Namespace: traefik (any distribution)"]
direction TB
TRAEFIK["Traefik<br>web :80→443 · websecure :443 TLS"]
subgraph RT["🟪 Traefik CRDs · routing"]
direction TB
IRD["IngressRoute · /dashboard · /api"]
MERR["Middleware · oauth-errors"]
MAUTH["Middleware · oauth-auth"]
IRO["IngressRoute · /oauth2/*"]
end
API["api@internal · Dashboard"]
O2P["oauth2-proxy · :4180"]
subgraph ESOG["🟩 External Secrets Operator · optional"]
direction TB
SS["SecretStore → Vault"]
ES["ExternalSecret"]
end
subgraph SEC["🩷 Secrets · in-cluster"]
direction TB
SECRET["oauth2-proxy-secret"]
TLS["traefik-dashboard-tls"]
end
TRAEFIK --> IRD --> MERR --> MAUTH --> API
TRAEFIK --> IRO --> O2P
MAUTH -. forwardAuth .-> O2P
O2P -. reads .-> SECRET
TRAEFIK -. TLS .-> TLS
SS --> ES
ES -->|creates| SECRET
end
KC["Keycloak · OIDC IdP"]
VAULT[("HashiCorp Vault · KV v2")]
MIRROR["Internal registry mirror<br>images + vendored chart"]
BROWSER --> DNS --> LBG
LBG --> TRAEFIK
O2P -->|OIDC| KC
VAULT -->|"Kubernetes auth"| ES
MIRROR -. "images + chart (no internet)" .-> TRAEFIK
MIRROR -. image .-> O2P
APP -. helm .-> TRAEFIK
EXT ~~~ GIT
classDef ext fill:#ECEFF1,stroke:#607D8B,color:#111;
classDef idp fill:#F8D7DA,stroke:#C0392B,color:#111;
classDef vault fill:#FFF3CD,stroke:#E0A800,color:#111;
classDef mirror fill:#E0F2F1,stroke:#00695C,color:#111;
classDef gitops fill:#DDE8FF,stroke:#3B6FD4,color:#111;
classDef lb fill:#B2DFDB,stroke:#00897B,color:#111;
classDef app fill:#FFE0B2,stroke:#EF7B4D,color:#111;
classDef crd fill:#E8E0FF,stroke:#7C4DFF,color:#111;
classDef eso fill:#D7F5DD,stroke:#2E9E5B,color:#111;
classDef secret fill:#FCE1F0,stroke:#C2185B,color:#111;
class BROWSER,DNS ext;
class KC idp;
class VAULT vault;
class MIRROR mirror;
class REPO,APP,PROJ gitops;
class METAL,NSX,KVIP,CIL,TRAEFIK lb;
class IRD,MERR,MAUTH,IRO crd;
class API,O2P app;
class SS,ES eso;
class SECRET,TLS secret;
Legend
| Group | What it covers | |
|---|---|---|
| 🟦 | Ingress / LB | MetalLB · NSX ALB · kube-vip · Cilium · Traefik |
| 🟪 | Traefik CRDs | IngressRoutes · middlewares (oauth-auth / oauth-errors) |
| 🟧 | Apps | oauth2-proxy · api@internal dashboard |
| 🟩 | External Secrets Operator | SecretStore · ExternalSecret (secret.mode=external-secrets) |
| 🩷 | Secrets | oauth2-proxy-secret · traefik-dashboard-tls |
| 🔁 | GitOps | git repo · ArgoCD Application · AppProject |
| 🟨 | Vault | HashiCorp Vault · KV v2 (backend for ESO) |
| 🟥 | Keycloak | OIDC IdP |
| 🟢 | Internal mirror | image registry + vendored chart (air-gap) |
| ⬜ | Client / DNS | browser · DNS record |
What is typical, merely possible, or best avoided per platform:
| Platform ↓ / LB → | metallb |
nsx-alb |
kube-vip |
cilium |
generic |
|---|---|---|---|---|---|
| openshift | ✅ typical (MetalLB Operator) | ⚪ if Avi/AKO present | ⚪ possible | ⚪ if Cilium CNI | ⚪ external/physical LB |
| rke2 | ✅ typical | ⚪ possible | ⚪ possible | ⚪ if Cilium CNI | ⚪ possible |
| k3s | ✅ typical | ⚪ possible | ⚪ possible | ⚪ if Cilium CNI | ⚪ built-in ServiceLB (klipper) |
| kubeadm / generic | ✅ typical | ⚪ possible | ⚪ possible | ⚪ if Cilium CNI | ⚪ possible |
| Tanzu / TKG (vSphere) | ✅ typical (NSX ALB + AKO) | ✅ typical (bare-metal/edge) | ⚪ if Cilium CNI | ⚪ possible |
✅ typical · ⚪ possible (supported, less common) ·
cilium fits when Cilium is already the CNI and does the LoadBalancer (LB-IPAM /
L2 announcements / BGP), so no MetalLB is needed — request an IP with the
lbipam.cilium.io/ips annotation on the Service.
vSphere caveat applies to any L2 mode (MetalLB L2, kube-vip ARP) regardless of platform — including OpenShift-on-vSphere: the port group must allow Forged Transmits, or the VIP is assigned but no traffic arrives. MetalLB BGP mode sidesteps it. This is separate from the cluster's own API/Ingress VIP (keepalived on IPI), which serves the cluster, not this Service.
Each sites/values-<platform>.yaml is a small delta over the chart defaults:
| Preset | platform |
LB backend | Traefik pod UID | caTrust.mode |
Cluster prerequisite |
|---|---|---|---|---|---|
values-openshift |
openshift |
metallb |
null (SCC injects a UID) | openshift-injector |
MetalLB Operator + pool |
values-rke2 |
rke2 |
metallb |
65532 |
none |
Disable bundled ingress-nginx; MetalLB |
values-k3s |
k3s |
metallb |
65532 |
none |
Install k3s --disable traefik; MetalLB |
values-kubeadm |
kubeadm |
metallb |
65532 |
none |
PodSecurity restricted; MetalLB |
values-tanzu-nsx |
generic |
nsx-alb |
65532 |
none |
NSX ALB (Avi) + AKO |
values-tanzu-kubevip |
generic |
kube-vip |
65532 |
none |
kube-vip in service mode |
OpenShift's restricted-v2 SCC injects a UID from the namespace range, so the
Traefik pod UID must be left null (pinning one is rejected). Every other
distribution has no such injector, so the preset sets an explicit non-root UID
(65532). The chart deletes the Traefik chart's built-in 65532 with an explicit
null and the non-OpenShift presets add it back — see the note in
values.yaml. Consequence: install with a
preset; a bare helm install with no preset fails validation on purpose.
helm/traefik-keycloak/ Umbrella chart
Chart.yaml Depends on the vendored Traefik chart
values.yaml Full catalogue of feature flags (documented)
charts/traefik-41.0.2.tgz Vendored official Traefik chart (air-gap)
templates/
oauth2-proxy-deployment.yaml oauth2-proxy (keycloak-oidc provider)
oauth2-proxy-service.yaml
middlewares.yaml oauth-auth (ForwardAuth) + oauth-errors
ingressroutes.yaml /oauth2/* and /dashboard routes
trusted-ca-configmap.yaml CA trust (openshift-injector / configmap)
oauth2-proxy-secret.yaml rendered only when secret.mode=inline
external-secrets.yaml ESO SecretStore + ExternalSecret (secret.mode=external-secrets)
networkpolicy.yaml NetworkPolicies (networkPolicy.enabled)
validation.yaml + _validate.tpl fail-fast combo validation
_helpers.tpl
README.md Chart-level reference (every flag)
sites/values-<platform>.yaml Per-platform presets (the delta)
argocd/ GitOps: AppProject + single Application
apps/traefik-keycloak.yaml multi-source (vendored chart + $values preset)
project.yaml
README.md
docs/ tls-secret.md, air-gapped.md,
external-secrets.md, network-policies.md
keycloak/keycloak-client-setup.md
metallb/ipaddresspool.example.yaml
secrets/oauth2-proxy-secret.example.yaml
scripts/validate-mermaid.mjs CI: parse every mermaid diagram
install.sh <platform> Imperative install (kubectl)
PORTABILITY.md Migration notes / what changed vs OpenShift-only
| Requirement | Check |
|---|---|
| Target cluster + kubectl/oc session | kubectl get nodes |
| Helm v3 CLI | helm version |
| A LoadBalancer provider with an address pool | MetalLB / NSX ALB / kube-vip |
| Keycloak reachable over HTTPS | open its console |
| Permissions to create namespace + CRDs | cluster-admin or equivalent |
| Manageable DNS for the dashboard host | points to the LoadBalancer IP |
Platform-specific prerequisites (bundled-component conflicts, etc.) are listed at
the top of each sites/values-<platform>.yaml.
Set the handful of real values — inline with --set, or by editing a copy of the
preset:
| Value | What it is | Example |
|---|---|---|
dashboard.host |
Public FQDN of the dashboard | traefik.apps.mycluster.com |
dashboard.cookieDomain |
Parent domain shared by dashboard + Keycloak | .apps.mycluster.com |
dashboard.tlsSecretName |
Pre-created TLS Secret (Traefik terminates TLS) | traefik-dashboard-tls |
keycloak.issuerUrl |
OIDC issuer (Keycloak 17+: /realms/<realm>) |
https://keycloak…/realms/myrealm |
oauth2Proxy.allowedRoles |
Role gate (client:role or realm-role) |
traefik-dashboard:traefik-admin |
Out-of-band (see the linked docs):
- TLS Secret —
docs/tls-secret.md. - oauth2-proxy Secret —
secrets/(orsecret.mode=inlinefor dev). - Keycloak client +
traefik-adminrole —keycloak/keycloak-client-setup.md. - DNS A record for
dashboard.host→ LoadBalancer IP.
Pick one path.
# 1) Create the namespace + PodSecurity label, then install with your preset:
./install.sh rke2 \
--set dashboard.host=traefik.apps.mycluster.com \
--set dashboard.cookieDomain=.apps.mycluster.com \
--set keycloak.issuerUrl=https://keycloak.apps.mycluster.com/realms/myrealm
# (install.sh wraps: namespace + PSA label, then `helm upgrade --install` with
# sites/values-<platform>.yaml. The Traefik chart is vendored — no internet pull.)
# 2) Read the LoadBalancer IP and create the DNS A record:
kubectl get svc -n traefik -l app.kubernetes.io/name=traefik -o widePlatforms: openshift, rke2, k3s, kubeadm, tanzu-nsx, tanzu-kubevip.
See argocd/README.md. In short: pick your preset in
argocd/apps/traefik-keycloak.yaml, then:
kubectl apply -n openshift-gitops -f argocd/project.yaml # or -n argocd
kubectl apply -n openshift-gitops -f argocd/apps/traefik-keycloak.yamlSecrets are created out-of-band first (§7) — ArgoCD does not manage them unless
you use Sealed/External Secrets or secret.mode=inline.
Every flag lives in helm/traefik-keycloak/values.yaml
(fully commented). Summary:
| Flag | Values | What it does |
|---|---|---|
platform |
generic openshift rke2 k3s kubeadm |
Distribution profile; drives pod-security expectations. Validated. |
loadBalancer.backend |
metallb nsx-alb kube-vip cilium generic |
Documents/validates the LB. Annotations go in traefik.service.annotations. |
image.registry |
"" or host |
Air-gap registry prefix for the oauth2-proxy image (traefik.image.registry for Traefik). |
caTrust.mode |
none openshift-injector configmap insecure |
How oauth2-proxy trusts the Keycloak TLS cert. |
secret.mode |
external inline external-secrets |
Reference a pre-created Secret (default), render one from values (dev), or pull it from a backend via ESO (§10). |
networkPolicy.enabled |
false true |
Render minimal NetworkPolicies for default-deny clusters (§11). |
dashboard.* keycloak.* oauth2Proxy.* |
— | Hostnames, OIDC issuer, role gate, replicas, resources. |
helm install/helm template runs the validation first and aborts on an
impossible combination:
- Enum checks on
platform,loadBalancer.backend,caTrust.mode,secret.mode. caTrust.mode=openshift-injector⇒ requiresplatform=openshift.platform=openshift⇒runAsUsermust be null;platform≠openshift⇒ a non-rootrunAsUsermust be set.secret.mode=inline⇒clientSecretandcookieSecretmust be set.secret.mode=external-secrets⇒ aSecretStoreref (and, when the chart creates the store, a Vaultserver) must be set.dashboard.hostandkeycloak.issuerUrlmust be non-empty.
oauth2-proxy needs three values — OAUTH2_PROXY_CLIENT_ID, _CLIENT_SECRET,
_COOKIE_SECRET — in a Secret named by secret.name. How that Secret comes to
exist is the secret.mode axis:
secret.mode |
Who creates the Secret | Use when |
|---|---|---|
external (default) |
You, out-of-band (manually, or Sealed Secrets) | GitOps and you seal secrets, or you apply them by hand |
inline |
The chart, from secret.clientSecret / secret.cookieSecret |
Dev / demo only — keep the values out of public git |
external-secrets |
The External Secrets Operator, pulled from a backend (e.g. HashiCorp Vault) | GitOps with a real secrets backend — nothing sensitive in git |
external-secrets mode renders a SecretStore (→ Vault, Kubernetes auth) and an
ExternalSecret that fills oauth2-proxy-secret; the oauth2-proxy Deployment
consumes it unchanged. Full setup (Vault KV, policy, role, self-signed CA) is in
docs/external-secrets.md.
Diagram — secret modes and the ESO/Vault flow (click to expand)
flowchart LR
subgraph MODES["secret.mode"]
direction TB
EXT["external<br/>you create it (Sealed Secrets / manual)"]
INL["inline<br/>chart renders from values (dev)"]
ES["external-secrets<br/>ESO pulls from a backend"]
end
VAULT[("HashiCorp Vault<br/>KV v2")]
ESO["External Secrets Operator<br/>SecretStore + ExternalSecret"]
SECRET["Secret: oauth2-proxy-secret<br/>CLIENT_ID · CLIENT_SECRET · COOKIE_SECRET"]
O2P["oauth2-proxy"]
EXT --> SECRET
INL --> SECRET
ES --> ESO
VAULT -->|"Kubernetes auth"| ESO
ESO -->|"creates / refreshes"| SECRET
SECRET --> O2P
classDef mode fill:#DDE8FF,stroke:#3B6FD4,color:#111;
classDef vault fill:#FFF3CD,stroke:#E0A800,color:#111;
classDef eso fill:#D7F5DD,stroke:#2E9E5B,color:#111;
class EXT,INL,ES mode;
class VAULT vault;
class ESO eso;
Two questions come up for on-prem clusters: does the CNI (Calico, Cilium, OVN-Kubernetes, Antrea, flannel…) or the CSI (storage) choice affect this deployment? Short answer: CSI not at all; CNI only through NetworkPolicy.
The workload is stateless: no PersistentVolumeClaims, no StorageClass
dependency. Traefik runs with readOnlyRootFilesystem and an emptyDir; TLS
comes from a pre-created Secret (no ACME/acme.json to persist); oauth2-proxy
sessions are cookie-based. So the CSI driver and storage classes are irrelevant
here. (Storage only re-enters if you self-host Keycloak or Vault in-cluster —
those are external dependencies, out of scope for this chart.)
Pods are standard and run on any CNI. The one place the CNI matters is
NetworkPolicy enforcement: on a default-deny cluster (common with Calico
or Cilium in secure / air-gapped environments) the required flows would be
blocked — Traefik→oauth2-proxy (ForwardAuth), oauth2-proxy→Keycloak (OIDC), and
Traefik→API server (the kubernetesCRD provider watch). Set
networkPolicy.enabled=true to render a minimal, working policy set. Details and
tightening options in docs/network-policies.md.
Cilium can also be the load balancer (LB-IPAM) — that is the
loadBalancer.backend=ciliumvalue (§4), a different axis from NetworkPolicy.
Diagram — allowed flows with networkPolicy.enabled (click to expand)
flowchart TD
LB["LoadBalancer"] -->|":80 / :443 (8000/8443)"| TR["Traefik"]
TR -->|":4180 ForwardAuth"| OP["oauth2-proxy"]
TR -->|"API-server port (CRD watch)"| API["kube-apiserver"]
OP -->|":443 OIDC"| KC["Keycloak"]
TR -.->|"DNS :53"| DNS["kube-dns"]
OP -.->|"DNS :53"| DNS
classDef inpol fill:#E8E0FF,stroke:#7C4DFF,color:#111;
classDef ext fill:#ECEFF1,stroke:#607D8B,color:#111;
class TR,OP inpol;
class LB,API,KC,DNS ext;
Two things must resolve without internet; both are handled — details in
docs/air-gapped.md:
- The Traefik chart is vendored in
charts/traefik-41.0.2.tgz, so neitherhelm installnor an ArgoCD sync pulls fromtraefik.github.io. - Image registries are parameterised. Point them at your internal mirror:
Mirror
--set image.registry=harbor.internal.example.com \ --set traefik.image.registry=harbor.internal.example.com
oauth2-proxyandtraefikinto that registry beforehand.
# Bump the vendored Traefik chart:
helm pull traefik/traefik --version <X.Y.Z> -d helm/traefik-keycloak/charts/
# update dependencies.version in helm/traefik-keycloak/Chart.yaml, remove the old .tgz
# Re-render/lint before shipping:
helm lint ./helm/traefik-keycloak -f sites/values-<platform>.yaml
helm template t ./helm/traefik-keycloak -f sites/values-<platform>.yaml >/dev/null
# Imperative apply:
helm upgrade traefik ./helm/traefik-keycloak -n traefik -f sites/values-<platform>.yamlKeep Traefik ≥ v3.4 (the errors middleware statusRewrites requirement).
Under GitOps, commit the change and let ArgoCD reconcile — do not helm/kubectl
by hand.
# Imperative:
helm uninstall traefik -n traefik
kubectl delete namespace traefik
# GitOps: delete the Application (finalizer cascades to its resources):
kubectl delete -n openshift-gitops application/traefik-keycloakExternal cleanup: DNS record, Keycloak client + traefik-admin role, MetalLB pool
(if dedicated), and the Traefik CRDs if you want them gone
(kubectl get crd | grep traefik.io).
kubectl get pods,svc -n traefik
kubectl logs -n traefik deploy/oauth2-proxy -f # OIDC, issuer, CA, roles
kubectl logs -n traefik deploy/traefik | grep -i error| Symptom | Likely cause | Fix |
|---|---|---|
| Blank 401 (no redirect) | Traefik < v3.4, or wrong middleware order | Traefik ≥ v3.4; order [oauth-errors, oauth-auth] |
| 403 after login | User lacks the traefik-admin role |
Assign the role in Keycloak |
| Redirect loop | dashboard.cookieDomain doesn't cover the host |
Set it to the shared parent domain |
Invalid redirect_uri |
Keycloak redirect URI mismatch | Must be https://<dashboard.host>/oauth2/callback |
x509: unknown authority |
oauth2-proxy doesn't trust the Keycloak cert | Set caTrust.mode (openshift-injector / configmap), or insecure for tests |
invalid issuer |
issuer with/without /auth wrong |
Check <issuer>/.well-known/openid-configuration |
| Service has no EXTERNAL-IP | No LB provider/pool, or blocked L2 on vSphere | Install MetalLB/NSX ALB/kube-vip; allow Forged Transmits, or use BGP |
Traefik pod CreateContainerConfigError (OpenShift) |
A pinned UID conflicts with the SCC | Use platform=openshift (leaves UID unset) — the chart validates this |
helm aborts: "requires an explicit non-root runAsUser" |
Installed with no preset | Install with a sites/ preset |
| Login times out / ForwardAuth 502 after enabling policies | networkPolicy.enabled=true but the cluster/CNI needs different ports (e.g. API server on 443) |
Set networkPolicy.apiServerPort; check keycloakEgressCIDR; see docs/network-policies.md |
oauth2-proxy-secret never appears (ESO) |
Wrong Vault role/path, or ESO can't auth | Check the ExternalSecret status and ESO logs; verify the Vault role binds the SA + namespace (docs/external-secrets.md) |
Diagram — GitOps reconciliation (click to expand)
flowchart LR
DEV["Edit sites/values-*.yaml<br/>commit + push"] --> REPO[("Git repo")]
REPO --> APP["ArgoCD Application<br/>traefik-keycloak (multi-source)"]
APP -->|"$values preset + vendored chart"| RENDER["helm template<br/>(chart 41.0.2)"]
RENDER --> SYNC["ArgoCD sync<br/>auto-sync · self-heal · prune"]
SYNC --> CLUSTER["namespace: traefik"]
PROJ["AppProject: traefik<br/>bounds repos / dest / resources"] -. scopes .-> APP
classDef git fill:#DDE8FF,stroke:#3B6FD4,color:#111;
classDef argo fill:#FFE0B2,stroke:#EF7B4D,color:#111;
class REPO,DEV git;
class APP,SYNC,PROJ argo;
Design points (full detail in argocd/README.md):
- Single Application, multi-source: the vendored chart is fed the platform
preset via the
$valuesref (a-fpath outside the chart dir is rejected by ArgoCD, so the ref pattern is used). - Namespace created and PSA-labelled via
managedNamespaceMetadata. ServerSideApply=trueavoids the "metadata.annotations: Too long" error on Traefik's large CRDs.- Dedicated AppProject bounds repos (just this one — the chart is vendored),
destination (
traefiknamespace) and cluster-scoped resources. - Secrets stay out of git (Sealed/External Secrets), or use
secret.mode=inline.
CI (.github/workflows/ci.yml) runs helm lint + helm template/kubeconform for
every preset, the validation-rule checks, yamllint, shellcheck, and parses
every mermaid diagram (scripts/validate-mermaid.mjs). Please keep it green.
Licensed under the MIT License.
Status: untested on a live cluster — all validation is
helm lint/helm template/ kubeconform / mermaid parsing, not runtime.
Architectural deep dives, video walkthroughs, and technical shorts for traefik-keycloak-portable, multi-distribution Kubernetes deployments, Traefik Ingress, and Keycloak SSO are hosted on the Nubenetes YouTube Channel (@nubenetes).
📂 Full-Length Technical Deep Dives (Architecture Masterclasses)
- 🔗 Direct Link: https://www.youtube.com/watch?v=3OQhS25KbIk
- 🌐 Origin Language: English 🇺🇸 (Subtitles in 20+ languages)
- ⏱️ Duration: 9:26
- 🏷️ Engineering Domain: Multi-Distribution Architecture, Orthogonal Feature Flags & Helm Design
- 📝 Technical Overview:
Detailed exploration of a single, highly portable umbrella Helm chart deploying Traefik v3 and a Keycloak-protected dashboard across OpenShift, RKE2, k3s, kubeadm, and VMware Tanzu. Explains why a single core with orthogonal feature flags outperforms per-distribution forks, how
_validate.tplcatches invalid configurations duringhelm templaterendering, and how vendored subcharts enable true air-gapped deployments. - 🛠️ Direct Links: Watch on YouTube | Edit in YouTube Studio
- 🔗 Direct Link: https://www.youtube.com/watch?v=8w0-SJw0jLo
- 🌐 Origin Language: English 🇺🇸 (Subtitles in 20+ languages)
- ⏱️ Duration: 9:26
- 🏷️ Engineering Domain: Platform Engineering, Distribution Quirks & Security Hardening
- 📝 Technical Overview:
Architectural analysis of multi-distribution Kubernetes portability. Explores how platform-specific security models—specifically Red Hat OpenShift's
restricted-v2SecurityContextConstraints rejecting hardcoded UIDs like 65532—differ from standard Kubernetes PodSecurityStandards. Demonstrates how the portable chart adapts UID allocation dynamically, configures CNI NetworkPolicies for Calico and Cilium, and eliminates environment drift. - 🛠️ Direct Links: Watch on YouTube | Edit in YouTube Studio
- 🔗 Direct Link: https://www.youtube.com/watch?v=X4GdtkJrbZo
- 🌐 Origin Language: English 🇺🇸 (Subtitles in 20+ languages)
- ⏱️ Duration: 7:04
- 🏷️ Engineering Domain: Zero-Trust Ingress, Keycloak OIDC, oauth2-proxy & ForwardAuth Middleware
- 📝 Technical Overview:
Comprehensive guide to securing the internal Traefik dashboard with OpenID Connect when Traefik OSS lacks native OIDC. Details the exact mechanics of
oauth2-proxy, Traefik'sForwardAuthmiddleware (/oauth2/auth), the Traefik v3.4+errorsmiddleware status rewrite (statusRewrites: "401": 302), and enforcing strict role-based access control withtraefik-dashboard:traefik-admin. - 🛠️ Direct Links: Watch on YouTube | Edit in YouTube Studio
- 🔗 Direct Link: https://www.youtube.com/watch?v=Hb4K81jiGrA
- 🌐 Origin Language: English 🇺🇸 (Subtitles in 20+ languages)
- ⏱️ Duration: 7:52
- 🏷️ Engineering Domain: On-Premises Load Balancing, Ingress Networking & CNI Isolation
- 📝 Technical Overview: Deep dive into decoupling Kubernetes ingress networking from specific cloud providers. Compares on-premises LoadBalancer backends supported by the chart: MetalLB (Layer 2 ARP and BGP pools), VMware NSX Advanced Load Balancer (Avi Vantage with AKO), kube-vip virtual IP failover, and modern eBPF-native Cilium LB-IPAM. Also reviews ingress NetworkPolicy hardening for zero-trust datacenters.
- 🛠️ Direct Links: Watch on YouTube | Edit in YouTube Studio
- 🔗 Direct Link: https://www.youtube.com/watch?v=1AfsoppwT_w
- 🌐 Origin Language: English 🇺🇸 (Subtitles in 20+ languages)
- ⏱️ Duration: 9:46
- 🏷️ Engineering Domain: End-to-End Production Lifecycle, Air-Gapped GitOps & Secrets Management
- 📝 Technical Overview: The complete production walkthrough covering Day 0 setup through Day 2 operations. Explains disconnected air-gapped image mirroring and chart vendoring, synchronizing secrets and private CA certificates from HashiCorp Vault via External Secrets Operator, declarative multi-source GitOps delivery with ArgoCD, and zero-downtime Helm upgrades.
- 🛠️ Direct Links: Watch on YouTube | Edit in YouTube Studio
📂 Technical Shorts Matrix & Architecture Breakdowns
| # | Short Title | Architectural Domain & Focus | Audio | Duration | Action |
|---|---|---|---|---|---|
| 01 | The Ultimate Portable Traefik & Keycloak Helm Chart | Unified Ingress & OIDC Zero forks, ForwardAuth middleware, status 401 to 302 rewrite & role-gating |
🇺🇸 EN | 1:30 |
|
| 02 | How Air-Gapped Kubernetes Clusters Install Ingress | Air-Gapped Architecture Vendored Helm dependencies & internal OCI mirrors for disconnected datacenters |
🇺🇸 EN | 1:03 |
|
| 03 | How External Secrets Operator Connects Vault | Zero-Trust Secret Sync Bridging HashiCorp Vault into Kubernetes secrets without committing passwords |
🇺🇸 EN | 1:02 |
|
| 04 | Why OpenShift SCC Breaks Standard Pods | OpenShift Hardening restricted-v2 dynamic UID ranges vs hardcoded non-root UIDs (65532) |
🇺🇸 EN | 1:20 |
- 🔗 Direct Link: https://www.youtube.com/shorts/murHHpH8WHE
- 🌐 Origin Language: English 🇺🇸 (Subtitles in 20+ languages)
- ⏱️ Duration: 1:30
- 🏷️ Engineering Domain: Unified Ingress, OIDC Authentication & Helm Architecture
- 📝 Technical Overview: Why managing separate Helm chart forks per distribution is an anti-pattern. Demonstrates how a single portable chart handles Traefik ingress and Keycloak dashboard protection across any Kubernetes cluster, using oauth2-proxy, ForwardAuth, status 401 to 302 redirect rewriting, and role-based gating.
- 🛠️ Direct Links: Watch Short | Edit in YouTube Studio
- 🔗 Direct Link: https://www.youtube.com/shorts/LGdVhOyaVaI
- 🌐 Origin Language: English 🇺🇸 (Subtitles in 20+ languages)
- ⏱️ Duration: 1:03
- 🏷️ Engineering Domain: Air-Gapped Kubernetes, Disconnected Mirroring & Offline Charts
- 📝 Technical Overview: How to deploy ingress controllers and complex microservices in strictly isolated datacenters with zero internet egress. Explains storing vendored Helm charts in Git and redirecting image repositories to internal OCI registries.
- 🛠️ Direct Links: Watch Short | Edit in YouTube Studio
- 🔗 Direct Link: https://www.youtube.com/shorts/BUwUb6vpbqs
- 🌐 Origin Language: English 🇺🇸 (Subtitles in 20+ languages)
- ⏱️ Duration: 1:02
- 🏷️ Engineering Domain: Zero-Trust Secret Management, External Secrets Operator & Vault
- 📝 Technical Overview: Eliminating plaintext secrets from Git repositories. Shows how External Secrets Operator continuously polls HashiCorp Vault to synchronize client secrets, cookie encryption keys, and internal TLS certs into standard Kubernetes Secrets.
- 🛠️ Direct Links: Watch Short | Edit in YouTube Studio
- 🔗 Direct Link: https://www.youtube.com/shorts/Cv5jakkIseE
- 🌐 Origin Language: English 🇺🇸 (Subtitles in 20+ languages)
- ⏱️ Duration: 1:20
- 🏷️ Engineering Domain: OpenShift Security, restricted-v2 SCC & Dynamic UID Ranges
- 📝 Technical Overview:
Why pods that run fine on k3s or kubeadm get blocked on Red Hat OpenShift. Explains how OpenShift's
restricted-v2SCC allocates dynamic UIDs and why hardcodingrunAsUser: 65532causes admission failures, plus how the portable chart adapts seamlessly. - 🛠️ Direct Links: Watch Short | Edit in YouTube Studio