Skip to content

Setting Up Dex for OIDC

Dex is the simplest OIDC provider I’ve found, and it’s perfect for a lab — it can run with an in-memory store and a literal YAML list of users while still speaking the protocol every cluster app expects.

I’m deploying Dex via Argo CD using a slightly unusual pattern: the Application has three sources. One is the upstream Helm chart, one is this repo’s directory for sibling manifests (Ingress, RBAC), and one is the same repo registered as a $values reference so Helm can pull values.yaml out of git.

That third source is the bit that surprised me when I first wrote it — without it you can’t tell Helm “the values file is in another repo entirely”. It’s a multi-source App, which is a relatively new Argo CD feature.

Dex stores nothing on disk. The static client list and a single demo password live in values.yaml, the Ingress lives next door, the cluster-admin RBAC binding sits in the same folder, and Argo CD reconciles the whole lot.

src/manifests/ainur/dex.yml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: dex
namespace: ainur
annotations:
argocd.argoproj.io/sync-wave: "-1"
spec:
project: ainur
sources:
- repoURL: https://charts.dexidp.io
targetRevision: 0.19.0
chart: dex
helm:
valueFiles:
- $values/src/manifests/ainur/dex/values.yaml
- repoURL: https://github.com/andrewiankidd/project-iluvatar.git
targetRevision: main
path: src/manifests/ainur/dex
- repoURL: https://github.com/andrewiankidd/project-iluvatar.git
targetRevision: main
ref: values
destination:
server: https://kubernetes.default.svc
namespace: ainur-dex
syncPolicy:
managedNamespaceMetadata:
labels:
ainur.kidd.network/ca: inject
syncOptions:
- CreateNamespace=true
automated:
prune: true
selfHeal: true

Things worth pointing out:

  • sync-wave: "-1" — Dex syncs before other apps that depend on it. The cluster-admin RBAC binding (rbac-admin-user.yml) is annotated with sync-wave: "2" so it lands once the namespace and Dex are settled.
  • managedNamespaceMetadata.labels — Argo CD writes ainur.kidd.network/ca: inject onto the ainur-dex namespace on every sync. That’s what causes trust-manager to drop a lab-ca ConfigMap into the namespace.
  • The third source (ref: values) — exists purely so the Helm source can read $values/src/manifests/ainur/dex/values.yaml. Without it Argo CD doesn’t know where $values points.
src/manifests/ainur/dex/values.yaml
config:
issuer: https://dex.kidd.network
storage:
type: memory
enablePasswordDB: true
staticPasswords:
- email: admin@kidd.network
# bcrypt of a demo password: change for your environment
# generate with: htpasswd -nbBC 10 "" 'YourStrongPass' | tr -d ':\n'
# Password: ubuntu
hash: $2y$10$66R20C4j0E4d0tjg7YewX.b7UvI3XoA6CcxAFO7Pj8J6izWLY18S6
username: admin
userID: "0001"
staticClients:
- id: headlamp
name: headlamp
secret: dex-oidc-secret
redirectURIs:
- https://headlamp.kidd.network/oidc-callback
- id: argocd
name: argocd
secret: dex-oidc-secret
redirectURIs:
- https://argocd.kidd.network/auth/callback
service:
type: ClusterIP

A few notes from things I learned the hard way:

src/manifests/ainur/dex/ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dex-ingress
namespace: ainur-dex
annotations:
kubernetes.io/ingress.class: "traefik"
cert-manager.io/cluster-issuer: "local-ca"
spec:
ingressClassName: traefik
tls:
- hosts:
- dex.kidd.network
secretName: dex-tls
rules:
- host: dex.kidd.network
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: dex
port:
number: 5556

The cert-manager.io/cluster-issuer: local-ca annotation is what tells cert-manager “issue a cert for dex.kidd.network from the local-ca ClusterIssuer and stash it in the dex-tls Secret”. The Ingress then serves that on the way through.

src/manifests/ainur/dex/rbac-admin-user.yml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: oidc-admin-admin-at-kidd-network
annotations:
argocd.argoproj.io/sync-wave: "2"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: User
apiGroup: rbac.authorization.k8s.io
name: admin@kidd.network

When k3s’ API server is started with --oidc-username-claim=email (which gen-lab-certs.sh configures — see user-data:292-301), the username string presented by Dex is literally the email. So this ClusterRoleBinding says “the OIDC user admin@kidd.network is cluster-admin”. sync-wave: "2" makes sure the ClusterRoleBinding doesn’t get applied before the namespace exists.

$ kubectl -n ainur-dex get pods
NAME READY STATUS RESTARTS AGE
dex-7f4b9d8c4f-x6sk2 1/1 Running 0 1d
$ kubectl -n ainur-dex get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
dex-ingress traefik dex.kidd.network 192.168.1.x 80, 443 1d
$ curl -s https://dex.kidd.network/.well-known/openid-configuration | jq .issuer
"https://dex.kidd.network"

If that curl returns a cert error, your CA isn’t trusted on the machine you’re running it from. See Verifying OIDC Authentication.

Dex is up. Next we point Headlamp and Argo CD at it: Integrating Headlamp and Argo CD.