Skip to content

Overview & Goals

By this point I had an ephemeral K3s cluster booting itself from scratch, Argo CD syncing apps from this repo, and Headlamp staring at me asking for a login. The obvious next move was a sane login story: one identity, end-to-end TLS, no plaintext anywhere, and no fragile copy-pasting of passwords from ~/.bashrc.

I had three things I wanted to fix at once:

  1. One login for every UI. Headlamp, Argo CD, and (eventually) other things should all defer to the same identity store. I didn’t want a per-app password file.
  2. HTTPS everywhere, including internal hosts. Browser-trusted certs without paying for or proving out a public domain. The cluster is private, so a private Certificate Authority is fine.
  3. Stay GitOps-pure. No kubectl create secret … at a terminal. Everything reconcilable from this repo, including the bits that bootstrap the rest.

The shape that fell out of this:

  • Dex as the OIDC provider. Tiny, declarative, perfect for a lab.
  • A self-signed Root CA that lives on the node and gets handed to cert-manager.
  • cert-manager with a ClusterIssuer backed by that CA, issuing TLS for every internal hostname.
  • trust-manager to publish the CA into a lab-ca ConfigMap that any app can mount to trust outbound TLS calls.
  • Traefik doing the Ingress part; cert-manager auto-issues the TLS secret it serves.

It’s surprising how few moving parts that ends up being once you let Argo CD do the wiring.

flowchart LR
    A[gen-lab-certs.sh<br/>cloud-init] --> B[cluster-root-ca<br/>Secret]
    B --> C[ClusterIssuer<br/>local-ca]
    B --> D[Bundle: lab-ca]
    D --> E[ConfigMap lab-ca<br/>in labelled namespaces]
    C --> F[TLS Secrets<br/>per Ingress]
    E --> G[Apps mount<br/>for outbound TLS]
    F --> H[Traefik HTTPS<br/>dex.kidd.network<br/>headlamp.kidd.network<br/>argocd.kidd.network]
PieceWhere it livesWhat it does
Dexsrc/manifests/ainur/dex.yml, dex/values.yamlOIDC provider; static clients for Headlamp and Argo CD
cert-manager + trust-managerInstalled via user-data:589-627 HelmChartsIssue TLS, distribute CA to namespaces
Root CAGenerated by gen-lab-certs.sh (user-data:232-302)Lives at /etc/rancher/k3s/oidc/lab-ca.crt; imported as cluster-root-ca Secret
lab-ca Bundleuser-data:642-663trust-manager Bundle that targets namespaces labelled ainur.kidd.network/ca: inject
Headlamp OIDC clientsrc/manifests/ainur/headlamp/values.yamlMounts lab-ca so it trusts Dex’s cert
Argo CD OIDC configEmbedded in user-data at the Argo CD HelmChart blockWires Argo CD to Dex; lives in user-data because Argo bootstraps the rest of GitOps
CA download portalsrc/manifests/ainur/cert/Friendly page at cert.kidd.network with install instructions per OS
  1. The node boots and gen-lab-certs.sh generates the Root CA (if it doesn’t already exist) and stores it on disk.
  2. cert-manager and trust-manager are installed via k3s HelmChart manifests staged from cloud-init.
  3. cluster-root-ca Secret is created in the cert-manager namespace from the on-disk CA.
  4. ClusterIssuer local-ca and the lab-ca Bundle are applied.
  5. Argo CD syncs the dex, headlamp, and cert-download applications.
  6. Each Argo CD Application uses syncPolicy.managedNamespaceMetadata to drop the ainur.kidd.network/ca: inject label onto its destination namespace.
  7. trust-manager sees the label and publishes the lab-ca ConfigMap into the namespace.
  8. cert-manager issues TLS for each Ingress; Traefik serves the apps over HTTPS.
  9. You trust the CA on your laptop once (via cert.kidd.network) and you’re done.

Walk through the prerequisites to make sure the cluster you have lines up with the cluster I’m assuming, then start at Configuring cert-manager for a Self-Signed CA and work down.