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.
Explanation
Section titled “Explanation”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.
Implementation
Section titled “Implementation”The Argo CD Application
Section titled “The Argo CD Application”apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: 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: trueThings 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 withsync-wave: "2"so it lands once the namespace and Dex are settled.managedNamespaceMetadata.labels— Argo CD writesainur.kidd.network/ca: injectonto theainur-dexnamespace on every sync. That’s what causes trust-manager to drop alab-caConfigMap 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$valuespoints.
Dex values
Section titled “Dex values”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: ClusterIPA few notes from things I learned the hard way:
Ingress
Section titled “Ingress”apiVersion: networking.k8s.io/v1kind: Ingressmetadata: 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: 5556The 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.
Admin RBAC
Section titled “Admin RBAC”apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: oidc-admin-admin-at-kidd-network annotations: argocd.argoproj.io/sync-wave: "2"roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-adminsubjects: - kind: User apiGroup: rbac.authorization.k8s.io name: admin@kidd.networkWhen 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.
What you should see after this lands
Section titled “What you should see after this lands”$ kubectl -n ainur-dex get podsNAME READY STATUS RESTARTS AGEdex-7f4b9d8c4f-x6sk2 1/1 Running 0 1d
$ kubectl -n ainur-dex get ingressNAME CLASS HOSTS ADDRESS PORTS AGEdex-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.
Next Steps
Section titled “Next Steps”Dex is up. Next we point Headlamp and Argo CD at it: Integrating Headlamp and Argo CD.