From a79a33eeed2dd04af755948954c06858f87f5fb9 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 09:01:11 -0800 Subject: [PATCH 01/23] Mikado: identify three leaf prerequisites for Authentik deploy Attempted deployment fails on three independent blockers: 1. Container image doesn't exist (build-authentik-container) 2. PostgreSQL database doesn't exist (provision-authentik-database) 3. 1Password secrets don't exist (create-authentik-secrets) Created cards for each and added requires to goal card. Co-Authored-By: Claude Opus 4.6 --- docs/how-to/how-to.md | 3 ++ .../how-to/plans/build-authentik-container.md | 36 +++++++++++++++++ docs/how-to/plans/create-authentik-secrets.md | 39 +++++++++++++++++++ docs/how-to/plans/deploy-authentik.md | 6 ++- docs/how-to/plans/plans.md | 3 ++ .../plans/provision-authentik-database.md | 34 ++++++++++++++++ 6 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 docs/how-to/plans/build-authentik-container.md create mode 100644 docs/how-to/plans/create-authentik-secrets.md create mode 100644 docs/how-to/plans/provision-authentik-database.md diff --git a/docs/how-to/how-to.md b/docs/how-to/how-to.md index 356c2b2..edd5b2f 100644 --- a/docs/how-to/how-to.md +++ b/docs/how-to/how-to.md @@ -64,4 +64,7 @@ Migration and transition plans for upcoming infrastructure changes. | [[forgejo-actions-dashboard]] | Grafana dashboard for Forgejo Actions CI metrics | | [[upgrade-grafana-helm-chart]] | Upgrade Grafana Helm chart from 8.8.2 to 11.x | | [[deploy-authentik]] | Deploy Authentik identity provider to replace Dex | +| [[build-authentik-container]] | Build Nix container image for Authentik | +| [[provision-authentik-database]] | Create PostgreSQL database for Authentik | +| [[create-authentik-secrets]] | Create 1Password secrets for Authentik | | [[operationalize-reolink-camera]] | Cloud-free NVR with Frigate and ring buffer recording | diff --git a/docs/how-to/plans/build-authentik-container.md b/docs/how-to/plans/build-authentik-container.md new file mode 100644 index 0000000..e1326f3 --- /dev/null +++ b/docs/how-to/plans/build-authentik-container.md @@ -0,0 +1,36 @@ +--- +title: Build Authentik Container Image +status: active +modified: 2026-02-20 +tags: + - how-to + - plans + - authentik +--- + +# Build Authentik Container Image + +Build and publish a Nix-based container image for Authentik to the local registry. + +## Context + +Discovered while attempting [[deploy-authentik]]: the deployment references `registry.ops.eblu.me/blumeops/authentik:v1.0.0-nix` which doesn't exist. Authentik's nixpkgs package (`pkgs.authentik`) provides the `ak` wrapper which orchestrates a Go server binary and Python Django worker. + +## What to Do + +1. Verify `containers/authentik/default.nix` builds on ringtail (the Nix builder runs there) +2. The `ak` entrypoint needs bash (included via `bashInteractive`) and orchestrates both `server` and `worker` subcommands +3. Tag and release: `mise run container-tag-and-release authentik v1.0.0` +4. Verify the `-nix` tagged image appears in the registry + +## What We Learned + +- The entrypoint is `ak` (bash wrapper), not `authentik` (Go binary) +- `ak server` runs the Go HTTP server, `ak worker` runs the Python Django worker +- `pkgs.authentik` bundles Go binary, Python environment, and static assets via `wrapProgram` +- nixpkgs has v2025.10.1, upstream latest is 2025.12.4 — acceptable for initial deployment +- Container needs `bashInteractive` since `ak` is a bash script + +## Related + +- [[deploy-authentik]] — Parent goal diff --git a/docs/how-to/plans/create-authentik-secrets.md b/docs/how-to/plans/create-authentik-secrets.md new file mode 100644 index 0000000..7cf57a4 --- /dev/null +++ b/docs/how-to/plans/create-authentik-secrets.md @@ -0,0 +1,39 @@ +--- +title: Create Authentik Secrets +status: active +modified: 2026-02-20 +tags: + - how-to + - plans + - authentik + - secrets +--- + +# Create Authentik Secrets + +Create the 1Password item that the ExternalSecret references for Authentik configuration. + +## Context + +Discovered while attempting [[deploy-authentik]]: the ExternalSecret references 1Password item "Authentik (blumeops)" which doesn't exist. Without it, the `authentik-config` Kubernetes secret won't be created and pods can't start. + +## What to Do + +1. Generate a random secret key for Authentik (`AUTHENTIK_SECRET_KEY`) +2. Create 1Password item "Authentik (blumeops)" in vault `blumeops` with fields: + - `secret-key`: random 50+ character string + - `postgresql-host`: Tailscale-accessible postgres hostname + - `postgresql-port`: `5432` + - `postgresql-name`: `authentik` + - `postgresql-user`: `authentik` + - `postgresql-password`: the password from [[provision-authentik-database]] +3. Verify the ExternalSecret can resolve on ringtail's cluster + +## Notes + +- This partially depends on [[provision-authentik-database]] for the postgres password, but the 1Password item structure and secret key can be created independently. + +## Related + +- [[deploy-authentik]] — Parent goal +- [[provision-authentik-database]] — Source of database credentials diff --git a/docs/how-to/plans/deploy-authentik.md b/docs/how-to/plans/deploy-authentik.md index f977ddd..338e15f 100644 --- a/docs/how-to/plans/deploy-authentik.md +++ b/docs/how-to/plans/deploy-authentik.md @@ -2,6 +2,10 @@ title: Deploy Authentik Identity Provider status: active modified: 2026-02-20 +requires: + - build-authentik-container + - provision-authentik-database + - create-authentik-secrets tags: - how-to - plans @@ -27,7 +31,7 @@ Replace [[dex]] with [Authentik](https://goauthentik.io/) as the SSO identity pr ## Open Questions -- **nixpkgs:** Verify `pkgs.authentik` exists. If not, packaging from source is a significant sub-task. +- ~~**nixpkgs:** Verify `pkgs.authentik` exists.~~ **Resolved:** exists at v2025.10.1, entrypoint is `ak` (bash wrapper). See [[build-authentik-container]]. - **Cross-cluster metrics:** Prometheus on indri scraping authentik on ringtail needs a new pattern (Dex has no metrics collection today). - **Dex decommission:** Separate effort after all OIDC clients migrate to Authentik. diff --git a/docs/how-to/plans/plans.md b/docs/how-to/plans/plans.md index bce7184..a4c3293 100644 --- a/docs/how-to/plans/plans.md +++ b/docs/how-to/plans/plans.md @@ -22,4 +22,7 @@ Plans differ from regular how-to guides in that they describe work that has been | [[forgejo-actions-dashboard]] | Planned | Grafana dashboard and custom Prometheus exporter for Forgejo Actions CI metrics | | [[upgrade-grafana-helm-chart]] | Planned | Upgrade Grafana Helm chart from 8.8.2 to 11.x (3 phases) | | [[deploy-authentik]] | Active (C2) | Deploy Authentik identity provider to replace Dex for full SSO and user management | +| [[build-authentik-container]] | Active (C2) | Build Nix container image for Authentik (prerequisite of deploy-authentik) | +| [[provision-authentik-database]] | Active (C2) | Create PostgreSQL database for Authentik (prerequisite of deploy-authentik) | +| [[create-authentik-secrets]] | Active (C2) | Create 1Password secrets for Authentik (prerequisite of deploy-authentik) | | [[operationalize-reolink-camera]] | Planned | Cloud-free NVR with Frigate, object detection, and ring buffer recording to sifaka | diff --git a/docs/how-to/plans/provision-authentik-database.md b/docs/how-to/plans/provision-authentik-database.md new file mode 100644 index 0000000..49381f2 --- /dev/null +++ b/docs/how-to/plans/provision-authentik-database.md @@ -0,0 +1,34 @@ +--- +title: Provision Authentik Database +status: active +modified: 2026-02-20 +tags: + - how-to + - plans + - authentik + - postgresql +--- + +# Provision Authentik Database + +Create a PostgreSQL database and user for Authentik on the existing CNPG cluster. + +## Context + +Discovered while attempting [[deploy-authentik]]: Authentik requires a PostgreSQL database, but no `authentik` database exists on `blumeops-pg`. The CNPG cluster runs on [[indri]] (minikube) and is reachable from [[ringtail]] via Tailscale at `blumeops-pg-rw.databases.svc:5432` or the Tailscale endpoint. + +## What to Do + +1. Create database `authentik` and user `authentik` on the CNPG cluster +2. Store credentials in 1Password (part of the "Authentik (blumeops)" item) +3. Verify cross-cluster connectivity: ringtail pod → indri postgres via Tailscale + +## Open Questions + +- What Tailscale hostname does the CNPG cluster expose? Need to check if there's a Tailscale Ingress for postgres or if we need to use the MagicDNS name directly. +- Should the database user have limited permissions or superuser access? + +## Related + +- [[deploy-authentik]] — Parent goal +- [[postgresql]] — CNPG cluster reference -- 2.50.1 (Apple Git-155) From 08ee70d6df67e93d1879f643c028c2dc3d132abc Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 09:43:30 -0800 Subject: [PATCH 02/23] Refine C2 Mikado method: failed attempt handling and early PRs Lessons learned from first C2 attempt (deploy-authentik): - When an attempt fails, reset code changes before committing cards - Cherry-pick doc commits onto clean base if code/docs got mixed - Open a PR early so the user can review the Mikado graph evolving Co-Authored-By: Claude Opus 4.6 --- docs/how-to/agent-change-process.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/how-to/agent-change-process.md b/docs/how-to/agent-change-process.md index b478f6d..7e862c7 100644 --- a/docs/how-to/agent-change-process.md +++ b/docs/how-to/agent-change-process.md @@ -49,16 +49,28 @@ Like C1 but designed to survive agent context loss across sessions: 1. **Goal card:** Create a how-to doc in `docs/how-to/` describing the desired end state - Add `status: active` to frontmatter -2. **Attempt the change**, amending the working commit. On failure, revert the broken change and: - - Create/update prerequisite cards as how-to docs with `status: active` +2. **Attempt the change** — GitOps may require pushing code to test (e.g., ArgoCD sync). When the attempt fails: + - **First**, reset the failed code changes (the branch should not carry broken code forward) + - **Then**, create/update prerequisite cards as how-to docs with `status: active` - Add `requires: [prerequisite-stem, ...]` to the goal card's frontmatter - - Commit the doc updates (the documentation IS the Mikado graph) + - Commit only the doc updates (the documentation IS the Mikado graph) 3. **Work leaf nodes first** — cards with `status: active` and no unmet `requires` -4. **New agent sessions** pick up state by running `mise run docs-mikado` -5. When a card's change succeeds, remove `status: active` (or the entire field) from its frontmatter +4. **Re-attempt the goal** after leaf nodes are resolved — code from the attempt comes back here +5. **New agent sessions** pick up state by running `mise run docs-mikado` +6. When a card's change succeeds, remove `status: active` (or the entire field) from its frontmatter Documentation IS the Mikado graph. Each card captures what was learned from failed attempts, so the next agent session doesn't repeat mistakes. +### Handling failed attempts + +When an attempt fails and you discover prerequisites, the branch must be cleaned up before documenting what you learned: + +1. Reset to before the code attempt (`git reset --hard`) +2. Commit the new prerequisite cards and frontmatter updates +3. If you already committed docs mixed with code, cherry-pick the doc commits onto a clean base rather than reverting (avoids noisy add/revert history) + +The branch between attempts should contain only documentation. Code returns when prerequisites are satisfied and the next attempt succeeds. + ## Card Conventions ### Frontmatter @@ -89,6 +101,7 @@ tags: ### Git Discipline - Single feature branch per C1/C2 change +- **Create a PR early** — open a draft PR after the first doc commit so the user can review the Mikado graph as it evolves between iterations. Push doc updates after each attempt cycle. - Amend a single working commit as you iterate; keep the branch history clean - GitOps requires pushing to test — if a pushed commit breaks, revert it promptly - Commit doc updates noting what was learned from failures -- 2.50.1 (Apple Git-155) From fbf230b414679ab044d87e3dfbdf12fae62c62ed Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 09:53:07 -0800 Subject: [PATCH 03/23] Move Mikado cards to topic subdirectory, not plans/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mikado cards are discovered through failed attempts, not designed upfront — they don't belong in plans/. Cards now live where they topically belong (how-to/authentik/ for this chain). Updated agent-change-process to document this convention. Co-Authored-By: Claude Opus 4.6 --- docs/how-to/agent-change-process.md | 3 ++- .../build-authentik-container.md | 1 - .../create-authentik-secrets.md | 1 - .../how-to/{plans => authentik}/deploy-authentik.md | 1 - .../provision-authentik-database.md | 1 - docs/how-to/how-to.md | 13 +++++++++---- docs/how-to/plans/plans.md | 5 +---- 7 files changed, 12 insertions(+), 13 deletions(-) rename docs/how-to/{plans => authentik}/build-authentik-container.md (99%) rename docs/how-to/{plans => authentik}/create-authentik-secrets.md (99%) rename docs/how-to/{plans => authentik}/deploy-authentik.md (99%) rename docs/how-to/{plans => authentik}/provision-authentik-database.md (99%) diff --git a/docs/how-to/agent-change-process.md b/docs/how-to/agent-change-process.md index 7e862c7..7c3bfa7 100644 --- a/docs/how-to/agent-change-process.md +++ b/docs/how-to/agent-change-process.md @@ -93,7 +93,8 @@ tags: ### Writing Cards -- Cards live in `docs/how-to/` — they're how-to docs with lifecycle metadata +- **Mikado cards are not plans.** Plans are designed upfront; Mikado cards are discovered through failed attempts. Don't put Mikado prerequisite cards in `docs/how-to/plans/`. +- Cards live in a topic subdirectory under `docs/how-to/` (e.g., `docs/how-to/authentik/` for the deploy-authentik chain). The goal card may live in `plans/` if it started as a plan. - Keep cards brief (<30 seconds to read) - Link to other cards rather than inlining their content - Document what was learned from failures, not just what to do diff --git a/docs/how-to/plans/build-authentik-container.md b/docs/how-to/authentik/build-authentik-container.md similarity index 99% rename from docs/how-to/plans/build-authentik-container.md rename to docs/how-to/authentik/build-authentik-container.md index e1326f3..71365d7 100644 --- a/docs/how-to/plans/build-authentik-container.md +++ b/docs/how-to/authentik/build-authentik-container.md @@ -4,7 +4,6 @@ status: active modified: 2026-02-20 tags: - how-to - - plans - authentik --- diff --git a/docs/how-to/plans/create-authentik-secrets.md b/docs/how-to/authentik/create-authentik-secrets.md similarity index 99% rename from docs/how-to/plans/create-authentik-secrets.md rename to docs/how-to/authentik/create-authentik-secrets.md index 7cf57a4..351805a 100644 --- a/docs/how-to/plans/create-authentik-secrets.md +++ b/docs/how-to/authentik/create-authentik-secrets.md @@ -4,7 +4,6 @@ status: active modified: 2026-02-20 tags: - how-to - - plans - authentik - secrets --- diff --git a/docs/how-to/plans/deploy-authentik.md b/docs/how-to/authentik/deploy-authentik.md similarity index 99% rename from docs/how-to/plans/deploy-authentik.md rename to docs/how-to/authentik/deploy-authentik.md index 338e15f..6c59291 100644 --- a/docs/how-to/plans/deploy-authentik.md +++ b/docs/how-to/authentik/deploy-authentik.md @@ -8,7 +8,6 @@ requires: - create-authentik-secrets tags: - how-to - - plans - authentik - security - oidc diff --git a/docs/how-to/plans/provision-authentik-database.md b/docs/how-to/authentik/provision-authentik-database.md similarity index 99% rename from docs/how-to/plans/provision-authentik-database.md rename to docs/how-to/authentik/provision-authentik-database.md index 49381f2..e99f76b 100644 --- a/docs/how-to/plans/provision-authentik-database.md +++ b/docs/how-to/authentik/provision-authentik-database.md @@ -4,7 +4,6 @@ status: active modified: 2026-02-20 tags: - how-to - - plans - authentik - postgresql --- diff --git a/docs/how-to/how-to.md b/docs/how-to/how-to.md index edd5b2f..e4b625e 100644 --- a/docs/how-to/how-to.md +++ b/docs/how-to/how-to.md @@ -63,8 +63,13 @@ Migration and transition plans for upcoming infrastructure changes. | [[harden-zot-registry]] | Add authentication and tag immutability to zot registry | | [[forgejo-actions-dashboard]] | Grafana dashboard for Forgejo Actions CI metrics | | [[upgrade-grafana-helm-chart]] | Upgrade Grafana Helm chart from 8.8.2 to 11.x | -| [[deploy-authentik]] | Deploy Authentik identity provider to replace Dex | -| [[build-authentik-container]] | Build Nix container image for Authentik | -| [[provision-authentik-database]] | Create PostgreSQL database for Authentik | -| [[create-authentik-secrets]] | Create 1Password secrets for Authentik | | [[operationalize-reolink-camera]] | Cloud-free NVR with Frigate and ring buffer recording | + +## Authentik + +Mikado chain for replacing Dex with Authentik. Track progress with `mise run docs-mikado deploy-authentik`. + +- [[deploy-authentik]] +- [[build-authentik-container]] +- [[provision-authentik-database]] +- [[create-authentik-secrets]] diff --git a/docs/how-to/plans/plans.md b/docs/how-to/plans/plans.md index a4c3293..c53cfdc 100644 --- a/docs/how-to/plans/plans.md +++ b/docs/how-to/plans/plans.md @@ -21,8 +21,5 @@ Plans differ from regular how-to guides in that they describe work that has been | [[harden-zot-registry]] | Planned | Add authentication and tag immutability to zot registry | | [[forgejo-actions-dashboard]] | Planned | Grafana dashboard and custom Prometheus exporter for Forgejo Actions CI metrics | | [[upgrade-grafana-helm-chart]] | Planned | Upgrade Grafana Helm chart from 8.8.2 to 11.x (3 phases) | -| [[deploy-authentik]] | Active (C2) | Deploy Authentik identity provider to replace Dex for full SSO and user management | -| [[build-authentik-container]] | Active (C2) | Build Nix container image for Authentik (prerequisite of deploy-authentik) | -| [[provision-authentik-database]] | Active (C2) | Create PostgreSQL database for Authentik (prerequisite of deploy-authentik) | -| [[create-authentik-secrets]] | Active (C2) | Create 1Password secrets for Authentik (prerequisite of deploy-authentik) | +| [[deploy-authentik]] | Active (C2) | Deploy Authentik IdP — Mikado chain tracked in `how-to/authentik/` | | [[operationalize-reolink-camera]] | Planned | Cloud-free NVR with Frigate, object detection, and ring buffer recording to sifaka | -- 2.50.1 (Apple Git-155) From fdcb4d2ae35e2028585bb3cf0fd3b01920fe3f91 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 10:08:52 -0800 Subject: [PATCH 04/23] Mikado: document build artifact and tag handling Build artifacts (container images, git tags) are independent of branch lifecycle and don't need to be deferred or reset during Mikado iterations. Co-Authored-By: Claude Opus 4.6 --- docs/how-to/agent-change-process.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/how-to/agent-change-process.md b/docs/how-to/agent-change-process.md index 7c3bfa7..2007abc 100644 --- a/docs/how-to/agent-change-process.md +++ b/docs/how-to/agent-change-process.md @@ -71,6 +71,17 @@ When an attempt fails and you discover prerequisites, the branch must be cleaned The branch between attempts should contain only documentation. Code returns when prerequisites are satisfied and the next attempt succeeds. +### Build artifacts and tags + +Mikado resets apply to branch code, not build artifacts. Container images in the registry and git tags created by `container-tag-and-release` are independent of branch lifecycle: + +- **Git tags** point to commit SHAs, not branches — they survive branch deletion and force-pushes. +- **Registry images** are build outputs cached in zot — a wrong image is overwritten by the next release. +- **If a build succeeds but deployment fails**, the image is fine; the problem is elsewhere. Document what you learned, bump the version, and try again. +- **If a build fails in CI**, no image is pushed. Delete the git tag (`git tag -d && git push --delete origin `) and fix the nix/dockerfile before re-releasing. + +Tag freely during leaf node work. The build IS the verification step — deferring it creates a chicken-and-egg where the card can't be marked complete without a built image. + ## Card Conventions ### Frontmatter -- 2.50.1 (Apple Git-155) From ac94cf6c5d679bdd40a1c965175b3cd5ce8eaa15 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 10:09:17 -0800 Subject: [PATCH 05/23] Add Authentik container definition (Nix) Nix-built container using pkgs.authentik with ak entrypoint. Includes bashInteractive (ak is a bash wrapper), cacert, tzdata. Co-Authored-By: Claude Opus 4.6 --- containers/authentik/default.nix | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 containers/authentik/default.nix diff --git a/containers/authentik/default.nix b/containers/authentik/default.nix new file mode 100644 index 0000000..dce6116 --- /dev/null +++ b/containers/authentik/default.nix @@ -0,0 +1,29 @@ +# Nix-built Authentik identity provider +# Uses nixpkgs authentik package (ak entrypoint wrapping Go server + Python worker) +# Built with dockerTools.buildLayeredImage for efficient layer caching +{ pkgs ? import { } }: + +pkgs.dockerTools.buildLayeredImage { + name = "blumeops/authentik"; + tag = "latest"; + + contents = [ + pkgs.authentik + pkgs.bashInteractive + pkgs.cacert + pkgs.tzdata + ]; + + config = { + Entrypoint = [ "${pkgs.authentik}/bin/ak" ]; + Env = [ + "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" + "TZDIR=${pkgs.tzdata}/share/zoneinfo" + ]; + ExposedPorts = { + "9000/tcp" = { }; + "9443/tcp" = { }; + }; + User = "65534"; + }; +} -- 2.50.1 (Apple Git-155) From 8116d6294a6509eed9b117c5427ad1dcfa284593 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 10:12:50 -0800 Subject: [PATCH 06/23] Complete build-authentik-container leaf node Image registry.ops.eblu.me/blumeops/authentik:v1.0.0-nix built via Nix on ringtail and verified in zot registry. Co-Authored-By: Claude Opus 4.6 --- docs/how-to/authentik/build-authentik-container.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/how-to/authentik/build-authentik-container.md b/docs/how-to/authentik/build-authentik-container.md index 71365d7..48a303e 100644 --- a/docs/how-to/authentik/build-authentik-container.md +++ b/docs/how-to/authentik/build-authentik-container.md @@ -1,6 +1,5 @@ --- title: Build Authentik Container Image -status: active modified: 2026-02-20 tags: - how-to -- 2.50.1 (Apple Git-155) From cc9ed2f2dec7d1f774ac34c89632c568108dc0af Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 10:14:44 -0800 Subject: [PATCH 07/23] Mikado: add push-after-every-iteration to git discipline Co-Authored-By: Claude Opus 4.6 --- docs/how-to/agent-change-process.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/how-to/agent-change-process.md b/docs/how-to/agent-change-process.md index 2007abc..d086fe9 100644 --- a/docs/how-to/agent-change-process.md +++ b/docs/how-to/agent-change-process.md @@ -113,7 +113,8 @@ tags: ### Git Discipline - Single feature branch per C1/C2 change -- **Create a PR early** — open a draft PR after the first doc commit so the user can review the Mikado graph as it evolves between iterations. Push doc updates after each attempt cycle. +- **Create a PR early** — open a draft PR after the first doc commit so the user can review the Mikado graph as it evolves between iterations. +- **Push after every iteration** — after completing a leaf node or documenting a failed attempt, push to origin. This is the save point for multi-session work. - Amend a single working commit as you iterate; keep the branch history clean - GitOps requires pushing to test — if a pushed commit breaks, revert it promptly - Commit doc updates noting what was learned from failures -- 2.50.1 (Apple Git-155) From bddce1a1591fd1072aa4f628e2d1319ccfb20cd8 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 10:20:23 -0800 Subject: [PATCH 08/23] Add authentik database user and ExternalSecret Add managed role for authentik user on blumeops-pg CNPG cluster, with ExternalSecret pulling password from 1Password item "Authentik (blumeops)". Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/databases/blumeops-pg.yaml | 9 ++++++ .../databases/external-secret-authentik.yaml | 28 +++++++++++++++++++ argocd/manifests/databases/kustomization.yaml | 1 + 3 files changed, 38 insertions(+) create mode 100644 argocd/manifests/databases/external-secret-authentik.yaml diff --git a/argocd/manifests/databases/blumeops-pg.yaml b/argocd/manifests/databases/blumeops-pg.yaml index 1c3c7de..73e2236 100644 --- a/argocd/manifests/databases/blumeops-pg.yaml +++ b/argocd/manifests/databases/blumeops-pg.yaml @@ -55,6 +55,15 @@ spec: createdb: true passwordSecret: name: blumeops-pg-teslamate + # authentik user for Authentik identity provider (runs on ringtail) + - name: authentik + login: true + connectionLimit: -1 + ensure: present + inherit: true + createdb: true + passwordSecret: + name: blumeops-pg-authentik # Resource limits for minikube environment resources: diff --git a/argocd/manifests/databases/external-secret-authentik.yaml b/argocd/manifests/databases/external-secret-authentik.yaml new file mode 100644 index 0000000..1486ed6 --- /dev/null +++ b/argocd/manifests/databases/external-secret-authentik.yaml @@ -0,0 +1,28 @@ +# ExternalSecret for Authentik database user password +# +# 1Password item: "Authentik (blumeops)" in blumeops vault +# Field: "postgresql-password" +# +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: blumeops-pg-authentik + namespace: databases +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-blumeops + target: + name: blumeops-pg-authentik + creationPolicy: Owner + template: + type: kubernetes.io/basic-auth + data: + username: authentik + password: "{{ .password }}" + data: + - secretKey: password + remoteRef: + key: Authentik (blumeops) + property: postgresql-password diff --git a/argocd/manifests/databases/kustomization.yaml b/argocd/manifests/databases/kustomization.yaml index 4e33a7c..8c4f506 100644 --- a/argocd/manifests/databases/kustomization.yaml +++ b/argocd/manifests/databases/kustomization.yaml @@ -11,3 +11,4 @@ resources: - external-secret-eblume.yaml - external-secret-borgmatic.yaml - external-secret-teslamate.yaml + - external-secret-authentik.yaml -- 2.50.1 (Apple Git-155) From cbf08a7bdef81c68339946ba0aa268525d8e69ea Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 10:23:48 -0800 Subject: [PATCH 09/23] Complete provision-authentik-database and create-authentik-secrets leaf nodes Both prerequisites for deploy-authentik are now satisfied: - CNPG managed role + ExternalSecret for authentik DB user - 1Password item "Authentik (blumeops)" with all required fields - Database created and cross-cluster connectivity verified Co-Authored-By: Claude Opus 4.6 --- .../authentik/create-authentik-secrets.md | 22 +++++++------------ .../authentik/provision-authentik-database.md | 21 ++++++++---------- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/docs/how-to/authentik/create-authentik-secrets.md b/docs/how-to/authentik/create-authentik-secrets.md index 351805a..be9ed34 100644 --- a/docs/how-to/authentik/create-authentik-secrets.md +++ b/docs/how-to/authentik/create-authentik-secrets.md @@ -1,6 +1,5 @@ --- title: Create Authentik Secrets -status: active modified: 2026-02-20 tags: - how-to @@ -12,27 +11,22 @@ tags: Create the 1Password item that the ExternalSecret references for Authentik configuration. -## Context +## What Was Done -Discovered while attempting [[deploy-authentik]]: the ExternalSecret references 1Password item "Authentik (blumeops)" which doesn't exist. Without it, the `authentik-config` Kubernetes secret won't be created and pods can't start. - -## What to Do - -1. Generate a random secret key for Authentik (`AUTHENTIK_SECRET_KEY`) -2. Create 1Password item "Authentik (blumeops)" in vault `blumeops` with fields: - - `secret-key`: random 50+ character string - - `postgresql-host`: Tailscale-accessible postgres hostname +1. Created 1Password item "Authentik (blumeops)" in vault `blumeops` (category: database) with fields: + - `secret-key`: random 68-character base64 string (for `AUTHENTIK_SECRET_KEY`) + - `postgresql-host`: `pg.ops.eblu.me` - `postgresql-port`: `5432` - `postgresql-name`: `authentik` - `postgresql-user`: `authentik` - - `postgresql-password`: the password from [[provision-authentik-database]] -3. Verify the ExternalSecret can resolve on ringtail's cluster + - `postgresql-password`: random 44-character base64 string +2. ExternalSecret `blumeops-pg-authentik` in databases namespace resolves successfully (verified during [[provision-authentik-database]]) ## Notes -- This partially depends on [[provision-authentik-database]] for the postgres password, but the 1Password item structure and secret key can be created independently. +- The database password in this 1Password item is the same one used by the CNPG managed role via `external-secret-authentik.yaml`. Both the database ExternalSecret and the future Authentik deployment ExternalSecret reference the same 1Password item but different fields. ## Related - [[deploy-authentik]] — Parent goal -- [[provision-authentik-database]] — Source of database credentials +- [[provision-authentik-database]] — Database provisioning (uses `postgresql-password` field) diff --git a/docs/how-to/authentik/provision-authentik-database.md b/docs/how-to/authentik/provision-authentik-database.md index e99f76b..71cf844 100644 --- a/docs/how-to/authentik/provision-authentik-database.md +++ b/docs/how-to/authentik/provision-authentik-database.md @@ -1,6 +1,5 @@ --- title: Provision Authentik Database -status: active modified: 2026-02-20 tags: - how-to @@ -12,20 +11,18 @@ tags: Create a PostgreSQL database and user for Authentik on the existing CNPG cluster. -## Context +## What Was Done -Discovered while attempting [[deploy-authentik]]: Authentik requires a PostgreSQL database, but no `authentik` database exists on `blumeops-pg`. The CNPG cluster runs on [[indri]] (minikube) and is reachable from [[ringtail]] via Tailscale at `blumeops-pg-rw.databases.svc:5432` or the Tailscale endpoint. +1. Added `authentik` managed role to `blumeops-pg` CNPG cluster (`argocd/manifests/databases/blumeops-pg.yaml`) — non-superuser with `createdb` and `login` +2. Created ExternalSecret `blumeops-pg-authentik` pulling password from 1Password item "Authentik (blumeops)" field `postgresql-password` +3. Synced CNPG cluster — role reconciled with password set +4. Created `authentik` database owned by `authentik` user +5. Verified cross-cluster connectivity: ringtail pod → `pg.ops.eblu.me:5432` (Caddy L4) -## What to Do +## Resolved Questions -1. Create database `authentik` and user `authentik` on the CNPG cluster -2. Store credentials in 1Password (part of the "Authentik (blumeops)" item) -3. Verify cross-cluster connectivity: ringtail pod → indri postgres via Tailscale - -## Open Questions - -- What Tailscale hostname does the CNPG cluster expose? Need to check if there's a Tailscale Ingress for postgres or if we need to use the MagicDNS name directly. -- Should the database user have limited permissions or superuser access? +- **Hostname:** `pg.ops.eblu.me` via Caddy L4 plugin (not MagicDNS) +- **Permissions:** Non-superuser with `createdb` — Authentik manages its own schema via migrations ## Related -- 2.50.1 (Apple Git-155) From 8016427a3cd1e739988253223bcc1531283c9bbb Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 10:51:38 -0800 Subject: [PATCH 10/23] Add Authentik deployment manifests and ArgoCD app Server, worker, Redis deployments targeting ringtail k3s cluster. ExternalSecret pulls config from 1Password "Authentik (blumeops)". Tailscale Ingress exposes at authentik.tail8d86e.ts.net. Co-Authored-By: Claude Opus 4.6 --- argocd/apps/authentik.yaml | 18 +++++ .../manifests/authentik/deployment-redis.yaml | 31 ++++++++ .../authentik/deployment-server.yaml | 79 +++++++++++++++++++ .../authentik/deployment-worker.yaml | 62 +++++++++++++++ .../manifests/authentik/external-secret.yaml | 39 +++++++++ .../authentik/ingress-tailscale.yaml | 26 ++++++ argocd/manifests/authentik/kustomization.yaml | 12 +++ argocd/manifests/authentik/service-redis.yaml | 14 ++++ argocd/manifests/authentik/service.yaml | 14 ++++ 9 files changed, 295 insertions(+) create mode 100644 argocd/apps/authentik.yaml create mode 100644 argocd/manifests/authentik/deployment-redis.yaml create mode 100644 argocd/manifests/authentik/deployment-server.yaml create mode 100644 argocd/manifests/authentik/deployment-worker.yaml create mode 100644 argocd/manifests/authentik/external-secret.yaml create mode 100644 argocd/manifests/authentik/ingress-tailscale.yaml create mode 100644 argocd/manifests/authentik/kustomization.yaml create mode 100644 argocd/manifests/authentik/service-redis.yaml create mode 100644 argocd/manifests/authentik/service.yaml diff --git a/argocd/apps/authentik.yaml b/argocd/apps/authentik.yaml new file mode 100644 index 0000000..38d6909 --- /dev/null +++ b/argocd/apps/authentik.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: authentik + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/authentik + destination: + server: https://ringtail.tail8d86e.ts.net:6443 + namespace: authentik + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/manifests/authentik/deployment-redis.yaml b/argocd/manifests/authentik/deployment-redis.yaml new file mode 100644 index 0000000..03c5873 --- /dev/null +++ b/argocd/manifests/authentik/deployment-redis.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: authentik-redis + namespace: authentik +spec: + replicas: 1 + selector: + matchLabels: + app: authentik + component: redis + template: + metadata: + labels: + app: authentik + component: redis + spec: + containers: + - name: redis + image: docker.io/library/redis:7-alpine + ports: + - name: redis + containerPort: 6379 + resources: + requests: + memory: "64Mi" + cpu: "25m" + limits: + memory: "128Mi" + cpu: "100m" diff --git a/argocd/manifests/authentik/deployment-server.yaml b/argocd/manifests/authentik/deployment-server.yaml new file mode 100644 index 0000000..e67392b --- /dev/null +++ b/argocd/manifests/authentik/deployment-server.yaml @@ -0,0 +1,79 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: authentik-server + namespace: authentik +spec: + replicas: 1 + selector: + matchLabels: + app: authentik + component: server + template: + metadata: + labels: + app: authentik + component: server + spec: + containers: + - name: server + image: registry.ops.eblu.me/blumeops/authentik:v1.0.0-nix + args: ["server"] + ports: + - name: http + containerPort: 9000 + - name: https + containerPort: 9443 + env: + - name: AUTHENTIK_SECRET_KEY + valueFrom: + secretKeyRef: + name: authentik-config + key: secret-key + - name: AUTHENTIK_POSTGRESQL__HOST + valueFrom: + secretKeyRef: + name: authentik-config + key: postgresql-host + - name: AUTHENTIK_POSTGRESQL__PORT + valueFrom: + secretKeyRef: + name: authentik-config + key: postgresql-port + - name: AUTHENTIK_POSTGRESQL__NAME + valueFrom: + secretKeyRef: + name: authentik-config + key: postgresql-name + - name: AUTHENTIK_POSTGRESQL__USER + valueFrom: + secretKeyRef: + name: authentik-config + key: postgresql-user + - name: AUTHENTIK_POSTGRESQL__PASSWORD + valueFrom: + secretKeyRef: + name: authentik-config + key: postgresql-password + - name: AUTHENTIK_REDIS__HOST + value: authentik-redis + livenessProbe: + httpGet: + path: /-/health/live/ + port: 9000 + initialDelaySeconds: 30 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /-/health/ready/ + port: 9000 + initialDelaySeconds: 15 + periodSeconds: 10 + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "1Gi" + cpu: "1000m" diff --git a/argocd/manifests/authentik/deployment-worker.yaml b/argocd/manifests/authentik/deployment-worker.yaml new file mode 100644 index 0000000..a1f3952 --- /dev/null +++ b/argocd/manifests/authentik/deployment-worker.yaml @@ -0,0 +1,62 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: authentik-worker + namespace: authentik +spec: + replicas: 1 + selector: + matchLabels: + app: authentik + component: worker + template: + metadata: + labels: + app: authentik + component: worker + spec: + containers: + - name: worker + image: registry.ops.eblu.me/blumeops/authentik:v1.0.0-nix + args: ["worker"] + env: + - name: AUTHENTIK_SECRET_KEY + valueFrom: + secretKeyRef: + name: authentik-config + key: secret-key + - name: AUTHENTIK_POSTGRESQL__HOST + valueFrom: + secretKeyRef: + name: authentik-config + key: postgresql-host + - name: AUTHENTIK_POSTGRESQL__PORT + valueFrom: + secretKeyRef: + name: authentik-config + key: postgresql-port + - name: AUTHENTIK_POSTGRESQL__NAME + valueFrom: + secretKeyRef: + name: authentik-config + key: postgresql-name + - name: AUTHENTIK_POSTGRESQL__USER + valueFrom: + secretKeyRef: + name: authentik-config + key: postgresql-user + - name: AUTHENTIK_POSTGRESQL__PASSWORD + valueFrom: + secretKeyRef: + name: authentik-config + key: postgresql-password + - name: AUTHENTIK_REDIS__HOST + value: authentik-redis + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "1Gi" + cpu: "1000m" diff --git a/argocd/manifests/authentik/external-secret.yaml b/argocd/manifests/authentik/external-secret.yaml new file mode 100644 index 0000000..2c17d91 --- /dev/null +++ b/argocd/manifests/authentik/external-secret.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: authentik-config + namespace: authentik +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-blumeops + target: + name: authentik-config + creationPolicy: Owner + data: + - secretKey: secret-key + remoteRef: + key: "Authentik (blumeops)" + property: secret-key + - secretKey: postgresql-host + remoteRef: + key: "Authentik (blumeops)" + property: postgresql-host + - secretKey: postgresql-port + remoteRef: + key: "Authentik (blumeops)" + property: postgresql-port + - secretKey: postgresql-name + remoteRef: + key: "Authentik (blumeops)" + property: postgresql-name + - secretKey: postgresql-user + remoteRef: + key: "Authentik (blumeops)" + property: postgresql-user + - secretKey: postgresql-password + remoteRef: + key: "Authentik (blumeops)" + property: postgresql-password diff --git a/argocd/manifests/authentik/ingress-tailscale.yaml b/argocd/manifests/authentik/ingress-tailscale.yaml new file mode 100644 index 0000000..6d112ba --- /dev/null +++ b/argocd/manifests/authentik/ingress-tailscale.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: authentik-tailscale + namespace: authentik + annotations: + tailscale.com/proxy-class: "default" + tailscale.com/proxy-group: "ingress" + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "Authentik" + gethomepage.dev/group: "Infrastructure" + gethomepage.dev/icon: "authentik" + gethomepage.dev/description: "Identity provider (SSO)" + gethomepage.dev/href: "https://authentik.ops.eblu.me" + gethomepage.dev/pod-selector: "app=authentik" +spec: + ingressClassName: tailscale + defaultBackend: + service: + name: authentik + port: + number: 9000 + tls: + - hosts: + - authentik diff --git a/argocd/manifests/authentik/kustomization.yaml b/argocd/manifests/authentik/kustomization.yaml new file mode 100644 index 0000000..385ae5b --- /dev/null +++ b/argocd/manifests/authentik/kustomization.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: authentik +resources: + - external-secret.yaml + - deployment-server.yaml + - deployment-worker.yaml + - deployment-redis.yaml + - service.yaml + - service-redis.yaml + - ingress-tailscale.yaml diff --git a/argocd/manifests/authentik/service-redis.yaml b/argocd/manifests/authentik/service-redis.yaml new file mode 100644 index 0000000..c278e9b --- /dev/null +++ b/argocd/manifests/authentik/service-redis.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: authentik-redis + namespace: authentik +spec: + selector: + app: authentik + component: redis + ports: + - name: redis + port: 6379 + targetPort: 6379 diff --git a/argocd/manifests/authentik/service.yaml b/argocd/manifests/authentik/service.yaml new file mode 100644 index 0000000..6c15f17 --- /dev/null +++ b/argocd/manifests/authentik/service.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: authentik + namespace: authentik +spec: + selector: + app: authentik + component: server + ports: + - name: http + port: 9000 + targetPort: 9000 -- 2.50.1 (Apple Git-155) From 41ee4161a94473b32ff7c392d761aacc76fb29b3 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 10:54:40 -0800 Subject: [PATCH 11/23] Add coreutils to authentik container The ak wrapper script requires mkdir (and likely other coreutils) to create runtime directories. Co-Authored-By: Claude Opus 4.6 --- containers/authentik/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/containers/authentik/default.nix b/containers/authentik/default.nix index dce6116..c950d08 100644 --- a/containers/authentik/default.nix +++ b/containers/authentik/default.nix @@ -10,6 +10,7 @@ pkgs.dockerTools.buildLayeredImage { contents = [ pkgs.authentik pkgs.bashInteractive + pkgs.coreutils pkgs.cacert pkgs.tzdata ]; -- 2.50.1 (Apple Git-155) From d90c993c6d1897b66f12b1a4a34dc7cf35b6fc52 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 10:56:35 -0800 Subject: [PATCH 12/23] Bump authentik image to v1.1.0-nix (adds coreutils) Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/authentik/deployment-server.yaml | 2 +- argocd/manifests/authentik/deployment-worker.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/argocd/manifests/authentik/deployment-server.yaml b/argocd/manifests/authentik/deployment-server.yaml index e67392b..5f23842 100644 --- a/argocd/manifests/authentik/deployment-server.yaml +++ b/argocd/manifests/authentik/deployment-server.yaml @@ -18,7 +18,7 @@ spec: spec: containers: - name: server - image: registry.ops.eblu.me/blumeops/authentik:v1.0.0-nix + image: registry.ops.eblu.me/blumeops/authentik:v1.1.0-nix args: ["server"] ports: - name: http diff --git a/argocd/manifests/authentik/deployment-worker.yaml b/argocd/manifests/authentik/deployment-worker.yaml index a1f3952..152b460 100644 --- a/argocd/manifests/authentik/deployment-worker.yaml +++ b/argocd/manifests/authentik/deployment-worker.yaml @@ -18,7 +18,7 @@ spec: spec: containers: - name: worker - image: registry.ops.eblu.me/blumeops/authentik:v1.0.0-nix + image: registry.ops.eblu.me/blumeops/authentik:v1.1.0-nix args: ["worker"] env: - name: AUTHENTIK_SECRET_KEY -- 2.50.1 (Apple Git-155) From 7300f72e18b94c2023dc6cc75f116498dce0538d Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 10:59:25 -0800 Subject: [PATCH 13/23] Add authentik.ops.eblu.me to Caddy reverse proxy Co-Authored-By: Claude Opus 4.6 --- ansible/roles/caddy/defaults/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ansible/roles/caddy/defaults/main.yml b/ansible/roles/caddy/defaults/main.yml index f7d44fe..411381e 100644 --- a/ansible/roles/caddy/defaults/main.yml +++ b/ansible/roles/caddy/defaults/main.yml @@ -82,6 +82,9 @@ caddy_services: - name: dex host: "dex.{{ caddy_domain }}" backend: "https://dex.tail8d86e.ts.net" + - name: authentik + host: "authentik.{{ caddy_domain }}" + backend: "https://authentik.tail8d86e.ts.net" - name: ntfy host: "ntfy.{{ caddy_domain }}" backend: "https://ntfy.tail8d86e.ts.net" -- 2.50.1 (Apple Git-155) From f144581ec2dd23ea356d6afbef19444784d96e1a Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 11:02:36 -0800 Subject: [PATCH 14/23] =?UTF-8?q?Complete=20deploy-authentik=20goal=20?= =?UTF-8?q?=E2=80=94=20Authentik=20running=20on=20ringtail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mikado chain complete: all three prerequisites resolved, Authentik server/worker/Redis healthy on k3s, accessible at authentik.ops.eblu.me. Co-Authored-By: Claude Opus 4.6 --- .../feature-deploy-authentik.feature.md | 1 + docs/how-to/authentik/deploy-authentik.md | 29 +++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 docs/changelog.d/feature-deploy-authentik.feature.md diff --git a/docs/changelog.d/feature-deploy-authentik.feature.md b/docs/changelog.d/feature-deploy-authentik.feature.md new file mode 100644 index 0000000..e2d2daf --- /dev/null +++ b/docs/changelog.d/feature-deploy-authentik.feature.md @@ -0,0 +1 @@ +Deploy Authentik identity provider on ringtail k3s cluster, replacing Dex as the SSO provider. Includes Nix-built container, CNPG database, Redis, and Caddy routing at `authentik.ops.eblu.me`. diff --git a/docs/how-to/authentik/deploy-authentik.md b/docs/how-to/authentik/deploy-authentik.md index 6c59291..ce4165d 100644 --- a/docs/how-to/authentik/deploy-authentik.md +++ b/docs/how-to/authentik/deploy-authentik.md @@ -1,6 +1,5 @@ --- title: Deploy Authentik Identity Provider -status: active modified: 2026-02-20 requires: - build-authentik-container @@ -22,17 +21,35 @@ Replace [[dex]] with [Authentik](https://goauthentik.io/) as the SSO identity pr | Decision | Choice | Rationale | |----------|--------|-----------| | **Cluster** | [[ringtail]] (k3s) | IdP independent of main services cluster, same as Dex | -| **Database** | CNPG `blumeops-pg` on [[indri]] | Cross-cluster via Tailscale, no new operator needed | +| **Database** | CNPG `blumeops-pg` on [[indri]] | Cross-cluster via Caddy L4 (`pg.ops.eblu.me`), no new operator needed | | **Redis** | Co-deployed in authentik namespace | Required for caching/sessions/task queue | | **Containers** | Nix-built (`dockerTools.buildLayeredImage`) | Supply chain control, consistent with Dex/ntfy pattern | | **Manifests** | Kustomize (no Helm) | Consistent with all other BlumeOps services | | **Networking** | Tailscale Ingress + Caddy reverse proxy | Same pattern as Dex | -## Open Questions +## What Was Done -- ~~**nixpkgs:** Verify `pkgs.authentik` exists.~~ **Resolved:** exists at v2025.10.1, entrypoint is `ak` (bash wrapper). See [[build-authentik-container]]. -- **Cross-cluster metrics:** Prometheus on indri scraping authentik on ringtail needs a new pattern (Dex has no metrics collection today). -- **Dex decommission:** Separate effort after all OIDC clients migrate to Authentik. +1. Built Nix container image (`v1.1.0-nix`) — `pkgs.authentik` + `coreutils` + `bashInteractive` +2. Created 1Password item "Authentik (blumeops)" with secret key and DB credentials +3. Provisioned `authentik` database and CNPG managed role on `blumeops-pg` +4. Deployed to ringtail k3s: server, worker, Redis (3 deployments) +5. ExternalSecret pulls config from 1Password +6. Tailscale Ingress at `authentik.tail8d86e.ts.net` +7. Caddy reverse proxy at `authentik.ops.eblu.me` + +## URLs + +- **Admin:** https://authentik.ops.eblu.me/if/admin/ +- **Tailscale:** https://authentik.tail8d86e.ts.net + +## Remaining Work + +- **Initial setup:** Complete first-run wizard (create admin account) +- **Forgejo connector:** Configure OAuth2 source for Forgejo federation +- **Client migration:** Move Grafana (and future services) from Dex to Authentik +- **Cross-cluster metrics:** Prometheus on indri scraping authentik on ringtail +- **Dex decommission:** Separate effort after all OIDC clients migrate +- **Redis image:** Replace upstream `redis:7-alpine` with Nix-built container ## Related -- 2.50.1 (Apple Git-155) From 4e3f7bead714cf37886b8086564f7fbadccc3c36 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 11:19:34 -0800 Subject: [PATCH 15/23] Mikado: add migrate-grafana-to-authentik prerequisite Authentik is deployed but no services use it yet. New leaf node to migrate Grafana's OIDC from Dex to Authentik, then decommission Dex. Goal card re-activated with new dependency. Co-Authored-By: Claude Opus 4.6 --- docs/how-to/authentik/deploy-authentik.md | 16 +++--- .../authentik/migrate-grafana-to-authentik.md | 51 +++++++++++++++++++ docs/how-to/how-to.md | 1 + 3 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 docs/how-to/authentik/migrate-grafana-to-authentik.md diff --git a/docs/how-to/authentik/deploy-authentik.md b/docs/how-to/authentik/deploy-authentik.md index ce4165d..d9ae037 100644 --- a/docs/how-to/authentik/deploy-authentik.md +++ b/docs/how-to/authentik/deploy-authentik.md @@ -1,10 +1,12 @@ --- title: Deploy Authentik Identity Provider +status: active modified: 2026-02-20 requires: - build-authentik-container - provision-authentik-database - create-authentik-secrets + - migrate-grafana-to-authentik tags: - how-to - authentik @@ -14,18 +16,20 @@ tags: # Deploy Authentik Identity Provider -Replace [[dex]] with [Authentik](https://goauthentik.io/) as the SSO identity provider. Authentik adds central user/group management, multi-protocol support (OIDC, SAML, LDAP), self-service flows, and an admin UI that Dex lacks. Forgejo remains the upstream identity source via OAuth2 connector. +Replace [[dex]] with [Authentik](https://goauthentik.io/) as the SSO identity provider. Authentik is the **source of truth** for user identity in BlumeOps. Users are created and managed in Authentik; services authenticate against it via OIDC. Forgejo federation is deferred to a future effort (existing `eblume` account has extensive automations that need careful migration). ## Architecture Decisions | Decision | Choice | Rationale | |----------|--------|-----------| +| **Identity model** | Authentik is source of truth | Central user/group management, not Forgejo-upstream like Dex | | **Cluster** | [[ringtail]] (k3s) | IdP independent of main services cluster, same as Dex | | **Database** | CNPG `blumeops-pg` on [[indri]] | Cross-cluster via Caddy L4 (`pg.ops.eblu.me`), no new operator needed | | **Redis** | Co-deployed in authentik namespace | Required for caching/sessions/task queue | | **Containers** | Nix-built (`dockerTools.buildLayeredImage`) | Supply chain control, consistent with Dex/ntfy pattern | | **Manifests** | Kustomize (no Helm) | Consistent with all other BlumeOps services | | **Networking** | Tailscale Ingress + Caddy reverse proxy | Same pattern as Dex | +| **IaC** | Authentik Blueprints (YAML in ConfigMap) | GitOps-native, config stored in repo | ## What Was Done @@ -36,24 +40,22 @@ Replace [[dex]] with [Authentik](https://goauthentik.io/) as the SSO identity pr 5. ExternalSecret pulls config from 1Password 6. Tailscale Ingress at `authentik.tail8d86e.ts.net` 7. Caddy reverse proxy at `authentik.ops.eblu.me` +8. Completed first-run wizard (admin account created) ## URLs - **Admin:** https://authentik.ops.eblu.me/if/admin/ - **Tailscale:** https://authentik.tail8d86e.ts.net -## Remaining Work +## Future Work (not blocking this card) -- **Initial setup:** Complete first-run wizard (create admin account) -- **Forgejo connector:** Configure OAuth2 source for Forgejo federation -- **Client migration:** Move Grafana (and future services) from Dex to Authentik +- **Forgejo federation:** Make Forgejo an OIDC client of Authentik (deferred — needs careful `eblume` account migration) - **Cross-cluster metrics:** Prometheus on indri scraping authentik on ringtail -- **Dex decommission:** Separate effort after all OIDC clients migrate - **Redis image:** Replace upstream `redis:7-alpine` with Nix-built container ## Related -- [[dex]] — Current IdP (to be replaced) +- [[dex]] — Current IdP (to be replaced by [[migrate-grafana-to-authentik]]) - [[federated-login]] — How authentication works across BlumeOps - [[adopt-oidc-provider]] — Dex deployment plan (completed) - [[ringtail]] — Target cluster diff --git a/docs/how-to/authentik/migrate-grafana-to-authentik.md b/docs/how-to/authentik/migrate-grafana-to-authentik.md new file mode 100644 index 0000000..3844151 --- /dev/null +++ b/docs/how-to/authentik/migrate-grafana-to-authentik.md @@ -0,0 +1,51 @@ +--- +title: Migrate Grafana to Authentik +status: active +modified: 2026-02-20 +tags: + - how-to + - authentik + - grafana +--- + +# Migrate Grafana to Authentik + +Move Grafana's OIDC authentication from Dex to Authentik, then decommission Dex. + +## Context + +Discovered while attempting [[deploy-authentik]]: Authentik is deployed and running, but no services use it yet. Grafana is the first client to migrate. Once Grafana is off Dex, Dex has no remaining clients and can be decommissioned. + +## What to Do + +### Authentik configuration (via API, then capture as Blueprint) + +1. Create an `admins` group in Authentik +2. Ensure user `blume.erich@gmail.com` is in the `admins` group +3. Create an OAuth2/OIDC provider for Grafana (client ID: `grafana`, redirect URIs for both `grafana.ops.eblu.me` and `grafana.tail8d86e.ts.net`) +4. Create an Application for Grafana linked to the provider, gated to the `admins` group +5. Store the client secret in 1Password "Authentik (blumeops)" as `grafana-client-secret` +6. Capture the configuration as an Authentik Blueprint YAML in the manifests + +### Grafana configuration + +1. Update `argocd/manifests/grafana/values.yaml` — change `auth.generic_oauth` from Dex to Authentik endpoints +2. Replace `external-secret-dex-oauth.yaml` with one that pulls from "Authentik (blumeops)" instead of "Dex (blumeops)" +3. Sync Grafana via ArgoCD and verify SSO login works + +### Dex decommission + +1. Delete ArgoCD app `dex` +2. Remove `argocd/manifests/dex/` and `argocd/apps/dex.yaml` +3. Remove `dex` entry from Caddy reverse proxy (`ansible/roles/caddy/defaults/main.yml`) +4. Provision Caddy to apply the change + +## Notes + +- Requires an Authentik API token — create one in Admin > System > Tokens, store as `api-token` field in "Authentik (blumeops)" 1Password item. + +## Related + +- [[deploy-authentik]] — Parent goal +- [[grafana]] — Grafana reference +- [[dex]] — Current IdP being replaced diff --git a/docs/how-to/how-to.md b/docs/how-to/how-to.md index e4b625e..4d1c364 100644 --- a/docs/how-to/how-to.md +++ b/docs/how-to/how-to.md @@ -73,3 +73,4 @@ Mikado chain for replacing Dex with Authentik. Track progress with `mise run doc - [[build-authentik-container]] - [[provision-authentik-database]] - [[create-authentik-secrets]] +- [[migrate-grafana-to-authentik]] -- 2.50.1 (Apple Git-155) From 00e4dc46e358f01ecb6c7cca6d6306bc49767c6f Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 11:47:17 -0800 Subject: [PATCH 16/23] Migrate Grafana OIDC from Dex to Authentik - Add Authentik Blueprint (ConfigMap) defining Grafana OAuth2 provider, application, admins group, and policy binding - Mount blueprint in worker, pass grafana client secret via env - Switch Grafana auth.generic_oauth from Dex to Authentik endpoints - Replace dex-oauth ExternalSecret with authentik-oauth Co-Authored-By: Claude Opus 4.6 --- .../authentik/configmap-blueprint.yaml | 72 +++++++++++++++++++ .../authentik/deployment-worker.yaml | 13 ++++ .../manifests/authentik/external-secret.yaml | 4 ++ argocd/manifests/authentik/kustomization.yaml | 1 + .../external-secret-authentik-oauth.yaml | 22 ++++++ .../grafana-config/kustomization.yaml | 2 +- argocd/manifests/grafana/values.yaml | 10 +-- 7 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 argocd/manifests/authentik/configmap-blueprint.yaml create mode 100644 argocd/manifests/grafana-config/external-secret-authentik-oauth.yaml diff --git a/argocd/manifests/authentik/configmap-blueprint.yaml b/argocd/manifests/authentik/configmap-blueprint.yaml new file mode 100644 index 0000000..527748d --- /dev/null +++ b/argocd/manifests/authentik/configmap-blueprint.yaml @@ -0,0 +1,72 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: authentik-blueprints + namespace: authentik +data: + grafana.yaml: | + version: 1 + metadata: + name: BlumeOps Grafana SSO + labels: + blueprints.goauthentik.io/description: "Grafana OIDC provider and application" + entries: + # admins group — gates access to admin-only applications + - model: authentik_core.group + id: admins-group + identifiers: + name: admins + attrs: + name: admins + + # OAuth2 provider for Grafana + - model: authentik_providers_oauth2.oauth2provider + id: grafana-provider + identifiers: + name: Grafana + attrs: + name: Grafana + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + client_type: confidential + client_id: grafana + client_secret: !Env [AUTHENTIK_GRAFANA_CLIENT_SECRET] + redirect_uris: + - matching_mode: strict + url: https://grafana.ops.eblu.me/login/generic_oauth + - matching_mode: strict + url: https://grafana.tail8d86e.ts.net/login/generic_oauth + signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + sub_mode: hashed_user_id + include_claims_in_id_token: true + + # Grafana application — linked to the OAuth2 provider + - model: authentik_core.application + id: grafana-app + identifiers: + slug: grafana + attrs: + name: Grafana + slug: grafana + provider: !KeyOf grafana-provider + meta_launch_url: https://grafana.ops.eblu.me + policy_engine_mode: any + + # Policy binding — restrict Grafana to admins group + - model: authentik_policies.policybinding + identifiers: + order: 0 + target: !KeyOf grafana-app + group: !KeyOf admins-group + attrs: + target: !KeyOf grafana-app + group: !KeyOf admins-group + order: 0 + enabled: true + negate: false + timeout: 30 diff --git a/argocd/manifests/authentik/deployment-worker.yaml b/argocd/manifests/authentik/deployment-worker.yaml index 152b460..0ab1067 100644 --- a/argocd/manifests/authentik/deployment-worker.yaml +++ b/argocd/manifests/authentik/deployment-worker.yaml @@ -53,6 +53,15 @@ spec: key: postgresql-password - name: AUTHENTIK_REDIS__HOST value: authentik-redis + - name: AUTHENTIK_GRAFANA_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: authentik-config + key: grafana-client-secret + volumeMounts: + - name: blueprints + mountPath: /blueprints/custom + readOnly: true resources: requests: memory: "256Mi" @@ -60,3 +69,7 @@ spec: limits: memory: "1Gi" cpu: "1000m" + volumes: + - name: blueprints + configMap: + name: authentik-blueprints diff --git a/argocd/manifests/authentik/external-secret.yaml b/argocd/manifests/authentik/external-secret.yaml index 2c17d91..b7072a0 100644 --- a/argocd/manifests/authentik/external-secret.yaml +++ b/argocd/manifests/authentik/external-secret.yaml @@ -37,3 +37,7 @@ spec: remoteRef: key: "Authentik (blumeops)" property: postgresql-password + - secretKey: grafana-client-secret + remoteRef: + key: "Authentik (blumeops)" + property: grafana-client-secret diff --git a/argocd/manifests/authentik/kustomization.yaml b/argocd/manifests/authentik/kustomization.yaml index 385ae5b..f639eba 100644 --- a/argocd/manifests/authentik/kustomization.yaml +++ b/argocd/manifests/authentik/kustomization.yaml @@ -4,6 +4,7 @@ kind: Kustomization namespace: authentik resources: - external-secret.yaml + - configmap-blueprint.yaml - deployment-server.yaml - deployment-worker.yaml - deployment-redis.yaml diff --git a/argocd/manifests/grafana-config/external-secret-authentik-oauth.yaml b/argocd/manifests/grafana-config/external-secret-authentik-oauth.yaml new file mode 100644 index 0000000..a83d35e --- /dev/null +++ b/argocd/manifests/grafana-config/external-secret-authentik-oauth.yaml @@ -0,0 +1,22 @@ +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: grafana-authentik-oauth + namespace: monitoring +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-blumeops + target: + name: grafana-authentik-oauth + creationPolicy: Owner + template: + data: + GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: "{{ .clientSecret }}" + data: + - secretKey: clientSecret + remoteRef: + key: "Authentik (blumeops)" + property: grafana-client-secret diff --git a/argocd/manifests/grafana-config/kustomization.yaml b/argocd/manifests/grafana-config/kustomization.yaml index 59e4e19..7322144 100644 --- a/argocd/manifests/grafana-config/kustomization.yaml +++ b/argocd/manifests/grafana-config/kustomization.yaml @@ -6,7 +6,7 @@ namespace: monitoring resources: - ingress-tailscale.yaml - external-secret-admin.yaml - - external-secret-dex-oauth.yaml + - external-secret-authentik-oauth.yaml - external-secret-teslamate-datasource.yaml # Dashboard ConfigMaps - discovered by Grafana sidecar via label grafana_dashboard=1 - dashboards/configmap-borgmatic.yaml diff --git a/argocd/manifests/grafana/values.yaml b/argocd/manifests/grafana/values.yaml index 24c406c..b07c74e 100644 --- a/argocd/manifests/grafana/values.yaml +++ b/argocd/manifests/grafana/values.yaml @@ -12,7 +12,7 @@ admin: envFromSecrets: - name: grafana-teslamate-datasource optional: true - - name: grafana-dex-oauth + - name: grafana-authentik-oauth optional: true # Persistence with PVC for SQLite database @@ -32,13 +32,13 @@ grafana.ini: allow_embedding: false auth.generic_oauth: enabled: true - name: Dex + name: Authentik client_id: grafana client_secret: $__env{GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET} scopes: openid profile email - auth_url: https://dex.ops.eblu.me/auth - token_url: https://dex.ops.eblu.me/token - api_url: https://dex.ops.eblu.me/userinfo + auth_url: https://authentik.ops.eblu.me/application/o/authorize/ + token_url: https://authentik.ops.eblu.me/application/o/token/ + api_url: https://authentik.ops.eblu.me/application/o/userinfo/ allow_sign_up: true role_attribute_path: "'Admin'" auto_login: false -- 2.50.1 (Apple Git-155) From 9417bdb451b1f793aa0407dd14fab5693eaa2223 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 11:55:25 -0800 Subject: [PATCH 17/23] Mikado: document blueprint loading issue on Nix container Nix-built authentik hardcodes blueprints_dir to the Nix store path. Custom blueprints at /blueprints/custom/ are not discovered. Need to override AUTHENTIK_BLUEPRINTS_DIR or patch the container. Co-Authored-By: Claude Opus 4.6 --- .../authentik/migrate-grafana-to-authentik.md | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/how-to/authentik/migrate-grafana-to-authentik.md b/docs/how-to/authentik/migrate-grafana-to-authentik.md index 3844151..495bdb9 100644 --- a/docs/how-to/authentik/migrate-grafana-to-authentik.md +++ b/docs/how-to/authentik/migrate-grafana-to-authentik.md @@ -40,9 +40,34 @@ Discovered while attempting [[deploy-authentik]]: Authentik is deployed and runn 3. Remove `dex` entry from Caddy reverse proxy (`ansible/roles/caddy/defaults/main.yml`) 4. Provision Caddy to apply the change +## What Was Done So Far + +### Completed + +- API token created and stored in 1Password "Authentik (blumeops)" field `api-token` +- `grafana-client-secret` generated and stored in 1Password "Authentik (blumeops)" +- Blueprint YAML created at `argocd/manifests/authentik/configmap-blueprint.yaml` defining: admins group, Grafana OAuth2 provider, Grafana application, and policy binding +- Blueprint ConfigMap mounted into worker at `/blueprints/custom/` +- ExternalSecret updated to pull `grafana-client-secret` from 1Password +- Grafana `values.yaml` updated to point at Authentik OIDC endpoints +- `external-secret-authentik-oauth.yaml` created to replace `external-secret-dex-oauth.yaml` + +### Blocked: Blueprint not loading + +**Root cause:** The Nix-built container hardcodes `blueprints_dir` to `/nix/store/3h1g...authentik-django-2025.10.1/blueprints` in its `default.yml`. Custom blueprints mounted at `/blueprints/custom/` are invisible because that path is not on the search path. + +**Fix options:** +1. Set env var `AUTHENTIK_BLUEPRINTS_DIR=/blueprints` and mount custom blueprints alongside copies/symlinks of the built-in ones — risky, could break built-in blueprints if the path doesn't include them. +2. Mount the custom blueprint ConfigMap directly into the Nix store blueprints path (e.g., `/nix/store/.../blueprints/custom/`) — fragile, path changes on rebuild. +3. Use the API to apply the configuration and skip file-based blueprints for now. Store the API calls in a mise task for reproducibility. +4. Patch the Nix container to set a writable `blueprints_dir` or create a wrapper that symlinks. + +**Recommendation:** Option 4 (patch container) or option 1 (override env var) are the cleanest. Need to test whether `AUTHENTIK_BLUEPRINTS_DIR` is respected and whether built-in blueprints still load from the Nix store path when overridden. + ## Notes -- Requires an Authentik API token — create one in Admin > System > Tokens, store as `api-token` field in "Authentik (blumeops)" 1Password item. +- Authentik API token stored as `api-token` in 1Password "Authentik (blumeops)". +- The `admins` group and Grafana provider/application created via API during investigation were cleaned up (deleted). ## Related -- 2.50.1 (Apple Git-155) From b99c655c478281955c441b8b010526d11f3111fc Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 12:09:12 -0800 Subject: [PATCH 18/23] Fix blueprint loading: create /blueprints symlink dir in container The nixpkgs authentik-django package hardcodes blueprints_dir to its Nix store path, making custom blueprints mounted at /blueprints/custom invisible to the discovery system. Add extraCommands to create a /blueprints directory with symlinks to the built-in blueprint dirs, and set AUTHENTIK_BLUEPRINTS_DIR=/blueprints so authentik scans the unified directory. Co-Authored-By: Claude Opus 4.6 --- containers/authentik/default.nix | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/containers/authentik/default.nix b/containers/authentik/default.nix index c950d08..9733f7f 100644 --- a/containers/authentik/default.nix +++ b/containers/authentik/default.nix @@ -15,11 +15,24 @@ pkgs.dockerTools.buildLayeredImage { pkgs.tzdata ]; + # Create /blueprints with symlinks to built-in blueprint dirs from the Nix store. + # The nixpkgs authentik-django package hardcodes blueprints_dir to its Nix store path, + # making custom blueprints mounted at /blueprints/custom invisible. This creates a + # stable /blueprints root that includes both built-in and custom blueprint directories. + extraCommands = '' + mkdir -p blueprints + for item in nix/store/*authentik-django*/blueprints/*; do + name=$(basename "$item") + ln -s "/$item" "blueprints/$name" + done + ''; + config = { Entrypoint = [ "${pkgs.authentik}/bin/ak" ]; Env = [ "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" "TZDIR=${pkgs.tzdata}/share/zoneinfo" + "AUTHENTIK_BLUEPRINTS_DIR=/blueprints" ]; ExposedPorts = { "9000/tcp" = { }; -- 2.50.1 (Apple Git-155) From 746435a9052d07d46e0a49ad63760428efe28dd7 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 12:11:41 -0800 Subject: [PATCH 19/23] Bump authentik image to v1.1.1-nix (blueprint path fix) Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/authentik/deployment-server.yaml | 2 +- argocd/manifests/authentik/deployment-worker.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/argocd/manifests/authentik/deployment-server.yaml b/argocd/manifests/authentik/deployment-server.yaml index 5f23842..5fecd96 100644 --- a/argocd/manifests/authentik/deployment-server.yaml +++ b/argocd/manifests/authentik/deployment-server.yaml @@ -18,7 +18,7 @@ spec: spec: containers: - name: server - image: registry.ops.eblu.me/blumeops/authentik:v1.1.0-nix + image: registry.ops.eblu.me/blumeops/authentik:v1.1.1-nix args: ["server"] ports: - name: http diff --git a/argocd/manifests/authentik/deployment-worker.yaml b/argocd/manifests/authentik/deployment-worker.yaml index 0ab1067..00f30ca 100644 --- a/argocd/manifests/authentik/deployment-worker.yaml +++ b/argocd/manifests/authentik/deployment-worker.yaml @@ -18,7 +18,7 @@ spec: spec: containers: - name: worker - image: registry.ops.eblu.me/blumeops/authentik:v1.1.0-nix + image: registry.ops.eblu.me/blumeops/authentik:v1.1.1-nix args: ["worker"] env: - name: AUTHENTIK_SECRET_KEY -- 2.50.1 (Apple Git-155) From 3e3fe0b2eb3a7a4407f07b26e8daa23a2ce980ff Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 12:16:07 -0800 Subject: [PATCH 20/23] Fix blueprint symlinks: use runtime entrypoint wrapper extraCommands in buildLayeredImage can't access store paths from contents (they're in separate layers), so the glob matched nothing. Instead, create a wrapper entrypoint that symlinks built-in blueprint dirs from the Nix store into /blueprints at container start. The directory is created world-writable so user 65534 can create links. Co-Authored-By: Claude Opus 4.6 --- containers/authentik/default.nix | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/containers/authentik/default.nix b/containers/authentik/default.nix index 9733f7f..01cc2f1 100644 --- a/containers/authentik/default.nix +++ b/containers/authentik/default.nix @@ -3,6 +3,20 @@ # Built with dockerTools.buildLayeredImage for efficient layer caching { pkgs ? import { } }: +let + # Wrapper entrypoint that sets up /blueprints symlinks before running ak. + # buildLayeredImage's extraCommands can't access store paths from contents (they're + # in separate layers), so we create the symlinks at container start instead. + entrypoint = pkgs.writeShellScript "authentik-entrypoint" '' + # Link built-in blueprint dirs from the Nix store into /blueprints + for item in /nix/store/*authentik-django*/blueprints/*/; do + name=$(basename "$item") + [ ! -e "/blueprints/$name" ] && ln -s "$item" "/blueprints/$name" 2>/dev/null || true + done + exec ${pkgs.authentik}/bin/ak "$@" + ''; +in + pkgs.dockerTools.buildLayeredImage { name = "blumeops/authentik"; tag = "latest"; @@ -15,20 +29,17 @@ pkgs.dockerTools.buildLayeredImage { pkgs.tzdata ]; - # Create /blueprints with symlinks to built-in blueprint dirs from the Nix store. + # Create /blueprints as world-writable so user 65534 can create symlinks at runtime. # The nixpkgs authentik-django package hardcodes blueprints_dir to its Nix store path, - # making custom blueprints mounted at /blueprints/custom invisible. This creates a - # stable /blueprints root that includes both built-in and custom blueprint directories. + # making custom blueprints mounted at /blueprints/custom invisible. The entrypoint + # wrapper populates this directory with symlinks to built-in blueprints on each start. extraCommands = '' mkdir -p blueprints - for item in nix/store/*authentik-django*/blueprints/*; do - name=$(basename "$item") - ln -s "/$item" "blueprints/$name" - done + chmod 777 blueprints ''; config = { - Entrypoint = [ "${pkgs.authentik}/bin/ak" ]; + Entrypoint = [ "${entrypoint}" ]; Env = [ "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" "TZDIR=${pkgs.tzdata}/share/zoneinfo" -- 2.50.1 (Apple Git-155) From b3f30fd9477d7b9bd7f1e6b95d1abcd78be269fc Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 12:16:34 -0800 Subject: [PATCH 21/23] Bump authentik image to v1.1.2-nix (entrypoint blueprint fix) Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/authentik/deployment-server.yaml | 2 +- argocd/manifests/authentik/deployment-worker.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/argocd/manifests/authentik/deployment-server.yaml b/argocd/manifests/authentik/deployment-server.yaml index 5fecd96..0a6c514 100644 --- a/argocd/manifests/authentik/deployment-server.yaml +++ b/argocd/manifests/authentik/deployment-server.yaml @@ -18,7 +18,7 @@ spec: spec: containers: - name: server - image: registry.ops.eblu.me/blumeops/authentik:v1.1.1-nix + image: registry.ops.eblu.me/blumeops/authentik:v1.1.2-nix args: ["server"] ports: - name: http diff --git a/argocd/manifests/authentik/deployment-worker.yaml b/argocd/manifests/authentik/deployment-worker.yaml index 00f30ca..f9bbdaf 100644 --- a/argocd/manifests/authentik/deployment-worker.yaml +++ b/argocd/manifests/authentik/deployment-worker.yaml @@ -18,7 +18,7 @@ spec: spec: containers: - name: worker - image: registry.ops.eblu.me/blumeops/authentik:v1.1.1-nix + image: registry.ops.eblu.me/blumeops/authentik:v1.1.2-nix args: ["worker"] env: - name: AUTHENTIK_SECRET_KEY -- 2.50.1 (Apple Git-155) From 25d43aa74392d460ec98b2f7266ef40b5bfb3f33 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 12:22:01 -0800 Subject: [PATCH 22/23] Fix blueprint !Env tag: use scalar not sequence !Env expects a bare string (e.g. !Env FOO), not a YAML sequence (!Env [FOO]). The list form caused IndexError during blueprint discovery. Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/authentik/configmap-blueprint.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argocd/manifests/authentik/configmap-blueprint.yaml b/argocd/manifests/authentik/configmap-blueprint.yaml index 527748d..f3bd32a 100644 --- a/argocd/manifests/authentik/configmap-blueprint.yaml +++ b/argocd/manifests/authentik/configmap-blueprint.yaml @@ -31,7 +31,7 @@ data: invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] client_type: confidential client_id: grafana - client_secret: !Env [AUTHENTIK_GRAFANA_CLIENT_SECRET] + client_secret: !Env AUTHENTIK_GRAFANA_CLIENT_SECRET redirect_uris: - matching_mode: strict url: https://grafana.ops.eblu.me/login/generic_oauth -- 2.50.1 (Apple Git-155) From 7ac7c6a3e5ac809f7bad9833a0975afe4f901f31 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 20 Feb 2026 12:51:09 -0800 Subject: [PATCH 23/23] Decommission Dex: remove all references, replace with Authentik - Delete dex manifests, ArgoCD app, container build, and reference doc - Remove dex from Caddy reverse proxy config - Create authentik.md reference doc - Rewrite federated-login.md for Authentik architecture - Update grafana, forgejo, ringtail, harden-zot-registry docs - Update services-check: replace dex health/pod checks with authentik - Fix all broken [[dex]] wiki-links Co-Authored-By: Claude Opus 4.6 --- ansible/roles/caddy/defaults/main.yml | 3 - argocd/apps/dex.yaml | 18 ---- argocd/manifests/dex/deployment.yaml | 53 ----------- argocd/manifests/dex/external-secret.yaml | 55 ------------ argocd/manifests/dex/ingress-tailscale.yaml | 26 ------ argocd/manifests/dex/kustomization.yaml | 9 -- argocd/manifests/dex/service.yaml | 13 --- .../external-secret-dex-oauth.yaml | 22 ----- .../grafana-config/kustomization.yaml | 2 +- containers/dex/default.nix | 28 ------ docs/explanation/explanation.md | 2 +- docs/explanation/federated-login.md | 60 +++++-------- docs/how-to/authentik/deploy-authentik.md | 9 +- .../authentik/migrate-grafana-to-authentik.md | 64 ++++--------- docs/how-to/how-to.md | 2 +- .../plans/completed/adopt-oidc-provider.md | 4 +- docs/how-to/plans/completed/completed.md | 2 +- docs/how-to/plans/harden-zot-registry.md | 12 +-- docs/reference/infrastructure/ringtail.md | 2 +- docs/reference/reference.md | 2 +- docs/reference/services/authentik.md | 78 ++++++++++++++++ docs/reference/services/dex.md | 89 ------------------- docs/reference/services/forgejo.md | 8 +- docs/reference/services/grafana.md | 8 +- mise-tasks/services-check | 4 +- 25 files changed, 148 insertions(+), 427 deletions(-) delete mode 100644 argocd/apps/dex.yaml delete mode 100644 argocd/manifests/dex/deployment.yaml delete mode 100644 argocd/manifests/dex/external-secret.yaml delete mode 100644 argocd/manifests/dex/ingress-tailscale.yaml delete mode 100644 argocd/manifests/dex/kustomization.yaml delete mode 100644 argocd/manifests/dex/service.yaml delete mode 100644 argocd/manifests/grafana-config/external-secret-dex-oauth.yaml delete mode 100644 containers/dex/default.nix create mode 100644 docs/reference/services/authentik.md delete mode 100644 docs/reference/services/dex.md diff --git a/ansible/roles/caddy/defaults/main.yml b/ansible/roles/caddy/defaults/main.yml index 411381e..b0fc046 100644 --- a/ansible/roles/caddy/defaults/main.yml +++ b/ansible/roles/caddy/defaults/main.yml @@ -79,9 +79,6 @@ caddy_services: - name: nvr host: "nvr.{{ caddy_domain }}" backend: "https://nvr.tail8d86e.ts.net" - - name: dex - host: "dex.{{ caddy_domain }}" - backend: "https://dex.tail8d86e.ts.net" - name: authentik host: "authentik.{{ caddy_domain }}" backend: "https://authentik.tail8d86e.ts.net" diff --git a/argocd/apps/dex.yaml b/argocd/apps/dex.yaml deleted file mode 100644 index 2da0939..0000000 --- a/argocd/apps/dex.yaml +++ /dev/null @@ -1,18 +0,0 @@ ---- -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: dex - namespace: argocd -spec: - project: default - source: - repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git - targetRevision: main - path: argocd/manifests/dex - destination: - server: https://ringtail.tail8d86e.ts.net:6443 - namespace: dex - syncPolicy: - syncOptions: - - CreateNamespace=true diff --git a/argocd/manifests/dex/deployment.yaml b/argocd/manifests/dex/deployment.yaml deleted file mode 100644 index ba02856..0000000 --- a/argocd/manifests/dex/deployment.yaml +++ /dev/null @@ -1,53 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: dex - namespace: dex -spec: - replicas: 1 - selector: - matchLabels: - app: dex - template: - metadata: - labels: - app: dex - spec: - containers: - - name: dex - image: registry.ops.eblu.me/blumeops/dex:v1.0.0-nix - ports: - - name: http - containerPort: 5556 - volumeMounts: - - name: config - mountPath: /etc/dex/cfg - readOnly: true - - name: data - mountPath: /var/dex - livenessProbe: - httpGet: - path: /healthz - port: 5556 - initialDelaySeconds: 5 - periodSeconds: 30 - readinessProbe: - httpGet: - path: /healthz - port: 5556 - initialDelaySeconds: 3 - periodSeconds: 10 - resources: - requests: - memory: "64Mi" - cpu: "50m" - limits: - memory: "128Mi" - cpu: "100m" - volumes: - - name: config - secret: - secretName: dex-config - - name: data - emptyDir: {} diff --git a/argocd/manifests/dex/external-secret.yaml b/argocd/manifests/dex/external-secret.yaml deleted file mode 100644 index 3b9e685..0000000 --- a/argocd/manifests/dex/external-secret.yaml +++ /dev/null @@ -1,55 +0,0 @@ ---- -apiVersion: external-secrets.io/v1 -kind: ExternalSecret -metadata: - name: dex-config - namespace: dex -spec: - refreshInterval: 1h - secretStoreRef: - kind: ClusterSecretStore - name: onepassword-blumeops - target: - name: dex-config - creationPolicy: Owner - template: - data: - config.yaml: | - issuer: https://dex.ops.eblu.me - storage: - type: sqlite3 - config: - file: /var/dex/dex.db - web: - http: 0.0.0.0:5556 - oauth2: - skipApprovalScreen: true - connectors: - - type: gitea - id: forgejo - name: Forgejo - config: - baseURL: https://forge.ops.eblu.me - clientID: "{{ .forgejoClientID }}" - clientSecret: "{{ .forgejoClientSecret }}" - redirectURI: https://dex.ops.eblu.me/callback - staticClients: - - id: grafana - name: Grafana - secret: "{{ .grafanaClientSecret }}" - redirectURIs: - - "https://grafana.ops.eblu.me/login/generic_oauth" - - "https://grafana.tail8d86e.ts.net/login/generic_oauth" - data: - - secretKey: forgejoClientID - remoteRef: - key: "Dex (blumeops)" - property: forgejo-client-id - - secretKey: forgejoClientSecret - remoteRef: - key: "Dex (blumeops)" - property: forgejo-client-secret - - secretKey: grafanaClientSecret - remoteRef: - key: "Dex (blumeops)" - property: grafana-client-secret diff --git a/argocd/manifests/dex/ingress-tailscale.yaml b/argocd/manifests/dex/ingress-tailscale.yaml deleted file mode 100644 index 4fc1958..0000000 --- a/argocd/manifests/dex/ingress-tailscale.yaml +++ /dev/null @@ -1,26 +0,0 @@ ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: dex-tailscale - namespace: dex - annotations: - tailscale.com/proxy-class: "default" - tailscale.com/proxy-group: "ingress" - gethomepage.dev/enabled: "true" - gethomepage.dev/name: "Dex" - gethomepage.dev/group: "Infrastructure" - gethomepage.dev/icon: "mdi-shield-key" - gethomepage.dev/description: "OIDC identity provider" - gethomepage.dev/href: "https://dex.ops.eblu.me" - gethomepage.dev/pod-selector: "app=dex" -spec: - ingressClassName: tailscale - defaultBackend: - service: - name: dex - port: - number: 5556 - tls: - - hosts: - - dex diff --git a/argocd/manifests/dex/kustomization.yaml b/argocd/manifests/dex/kustomization.yaml deleted file mode 100644 index cffcba8..0000000 --- a/argocd/manifests/dex/kustomization.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: dex -resources: - - external-secret.yaml - - deployment.yaml - - service.yaml - - ingress-tailscale.yaml diff --git a/argocd/manifests/dex/service.yaml b/argocd/manifests/dex/service.yaml deleted file mode 100644 index f29a20d..0000000 --- a/argocd/manifests/dex/service.yaml +++ /dev/null @@ -1,13 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - name: dex - namespace: dex -spec: - selector: - app: dex - ports: - - name: http - port: 5556 - targetPort: 5556 diff --git a/argocd/manifests/grafana-config/external-secret-dex-oauth.yaml b/argocd/manifests/grafana-config/external-secret-dex-oauth.yaml deleted file mode 100644 index c2c4261..0000000 --- a/argocd/manifests/grafana-config/external-secret-dex-oauth.yaml +++ /dev/null @@ -1,22 +0,0 @@ ---- -apiVersion: external-secrets.io/v1 -kind: ExternalSecret -metadata: - name: grafana-dex-oauth - namespace: monitoring -spec: - refreshInterval: 1h - secretStoreRef: - kind: ClusterSecretStore - name: onepassword-blumeops - target: - name: grafana-dex-oauth - creationPolicy: Owner - template: - data: - GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: "{{ .clientSecret }}" - data: - - secretKey: clientSecret - remoteRef: - key: "Dex (blumeops)" - property: grafana-client-secret diff --git a/argocd/manifests/grafana-config/kustomization.yaml b/argocd/manifests/grafana-config/kustomization.yaml index 7322144..845e837 100644 --- a/argocd/manifests/grafana-config/kustomization.yaml +++ b/argocd/manifests/grafana-config/kustomization.yaml @@ -6,8 +6,8 @@ namespace: monitoring resources: - ingress-tailscale.yaml - external-secret-admin.yaml - - external-secret-authentik-oauth.yaml - external-secret-teslamate-datasource.yaml + - external-secret-authentik-oauth.yaml # Dashboard ConfigMaps - discovered by Grafana sidecar via label grafana_dashboard=1 - dashboards/configmap-borgmatic.yaml - dashboards/configmap-devpi.yaml diff --git a/containers/dex/default.nix b/containers/dex/default.nix deleted file mode 100644 index 08f6926..0000000 --- a/containers/dex/default.nix +++ /dev/null @@ -1,28 +0,0 @@ -# Nix-built Dex OIDC identity provider -# Uses nixpkgs dex-oidc package with Kubernetes CRD storage backend -# Built with dockerTools.buildLayeredImage for efficient layer caching -{ pkgs ? import { } }: - -pkgs.dockerTools.buildLayeredImage { - name = "blumeops/dex"; - tag = "latest"; - - contents = [ - pkgs.dex-oidc - pkgs.cacert - pkgs.tzdata - ]; - - config = { - Entrypoint = [ "${pkgs.dex-oidc}/bin/dex" ]; - Cmd = [ "serve" "/etc/dex/cfg/config.yaml" ]; - Env = [ - "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" - "TZDIR=${pkgs.tzdata}/share/zoneinfo" - ]; - ExposedPorts = { - "5556/tcp" = { }; - }; - User = "65534"; - }; -} diff --git a/docs/explanation/explanation.md b/docs/explanation/explanation.md index 59ffb0a..1f46eaa 100644 --- a/docs/explanation/explanation.md +++ b/docs/explanation/explanation.md @@ -21,5 +21,5 @@ Understanding-oriented content explaining the "why" behind BlumeOps design decis | Article | Description | |---------|-------------| | [[architecture]] | How all the pieces fit together | -| [[federated-login]] | How SSO works across BlumeOps (Dex + Forgejo) | +| [[federated-login]] | How SSO works across BlumeOps (Authentik) | | [[security-model]] | Network security, secrets, and access control | diff --git a/docs/explanation/federated-login.md b/docs/explanation/federated-login.md index 0537ebb..ea89477 100644 --- a/docs/explanation/federated-login.md +++ b/docs/explanation/federated-login.md @@ -1,7 +1,7 @@ --- title: Federated Login -modified: 2026-02-19 -last-reviewed: 2026-02-19 +modified: 2026-02-20 +last-reviewed: 2026-02-20 tags: - explanation - security @@ -23,65 +23,53 @@ Without centralized authentication, every service manages its own users independ - **No single sign-on** — logging into Grafana doesn't help you access ArgoCD - **Inconsistent security** — some services have auth, some don't, and there's no central audit trail -## The Solution: Dex + Forgejo +## The Solution: Authentik -BlumeOps uses a two-layer federated authentication model: +BlumeOps uses [[authentik]] as the central OIDC identity provider. Authentik is the **source of truth** for user identity — users are created and managed in Authentik, and services authenticate against it via OpenID Connect. -1. **[[dex]]** is the OIDC identity provider (IdP). Services like [[grafana]] delegate their login flow to Dex using the OpenID Connect protocol. Dex issues standardized tokens that carry user identity. - -2. **[[forgejo]]** is the upstream identity source. Dex doesn't store users itself — it delegates authentication to Forgejo via OAuth2. Users log in with their Forgejo credentials. - -This separation is intentional. Dex handles the OIDC protocol (token issuance, discovery endpoints, client registration), while Forgejo handles user management (accounts, passwords, 2FA). Each does what it's good at. +This is a deliberate choice: Authentik provides a full-featured identity management UI, Blueprint-driven GitOps configuration, and support for multiple authentication protocols. Services like [[grafana]] delegate their login flow to Authentik using OIDC, and Authentik issues standardized tokens that carry user identity. ## The Login Flow -When a user clicks "Sign in with Dex" on Grafana: +When a user clicks "Sign in with Authentik" on Grafana: ``` -1. Grafana redirects browser to Dex (dex.ops.eblu.me/auth) -2. Dex redirects browser to Forgejo (forge.ops.eblu.me/login/oauth/authorize) -3. User logs in at Forgejo (or is already logged in) -4. Forgejo redirects back to Dex (dex.ops.eblu.me/callback) -5. Dex issues an OIDC token -6. Dex redirects back to Grafana (grafana.ops.eblu.me/login/generic_oauth) -7. Grafana accepts the token, user is logged in +1. Grafana redirects browser to Authentik (authentik.ops.eblu.me/application/o/authorize/) +2. User logs in at Authentik (or is already logged in) +3. Authentik issues an OIDC token +4. Authentik redirects back to Grafana (grafana.ops.eblu.me/login/generic_oauth) +5. Grafana accepts the token, user is logged in ``` -After step 3, if the user is already logged into Forgejo, the remaining steps happen instantly — it feels like a single click. - -## Why Not Just Use Forgejo Directly? - -Forgejo supports OAuth2 provider mode, so services could authenticate against it directly. Dex adds a layer of indirection, which provides: - -- **Protocol translation** — Dex speaks OIDC (a standardized protocol) to downstream services. Not all services speak the same OAuth2 dialect that Forgejo does, but most speak OIDC. -- **Connector flexibility** — Dex can federate to multiple identity sources simultaneously. If a Google or GitHub connector is added later, downstream services don't change at all — they still talk to Dex. -- **Separation of concerns** — Forgejo is a git forge first. Its OAuth2 provider is a secondary feature. Dex is purpose-built for identity federation and handles edge cases (token refresh, JWKS rotation, discovery) more robustly. - -For a single-user homelab, the indirection is admittedly overkill today. But it keeps the architecture clean for future growth — adding a second identity source or a new downstream service is a config change, not an architecture change. +If the user is already logged into Authentik, the flow happens instantly — it feels like a single click. ## Break-Glass Access -Every service that uses Dex SSO also keeps a local admin login. If Dex goes down (or ringtail is offline), recovery works through: +Every service that uses Authentik SSO also keeps a local admin login. If Authentik goes down (or ringtail is offline), recovery works through: 1. SSH to indri 2. Log into ArgoCD with local admin password (from 1Password) 3. Fix whatever is broken -Dex is additive — it's a convenience layer, not a hard dependency. Services never lose their local auth capability. +Authentik is additive — it's a convenience layer, not a hard dependency. Services never lose their local auth capability. ## Cross-Cluster Communication -Dex runs on [[ringtail]]'s k3s cluster while most services run on indri's minikube. This is deliberate — the IdP is independent of the main services cluster. Communication happens via the Tailscale network: +Authentik runs on [[ringtail]]'s k3s cluster while most services run on indri's minikube. This is deliberate — the IdP is independent of the main services cluster. Communication happens via the Tailscale network: -- Grafana (minikube) → `dex.ops.eblu.me` → Caddy (indri) → Tailscale → Dex (ringtail k3s) -- Browser redirects go through `dex.ops.eblu.me` and `forge.ops.eblu.me`, both resolved via Caddy +- Grafana (minikube) → `authentik.ops.eblu.me` → Caddy (indri) → Tailscale → Authentik (ringtail k3s) +- Browser redirects go through `authentik.ops.eblu.me`, resolved via Caddy No k8s-internal DNS crosses cluster boundaries. Everything uses the `*.ops.eblu.me` domain. +## Future Work + +- **Forgejo OIDC:** Make Forgejo an OIDC client of Authentik (deferred — existing `eblume` account needs careful migration) +- **Additional services:** ArgoCD, Miniflux, Immich, Zot (see [[harden-zot-registry]]) + ## Related -- [[dex]] - OIDC identity provider reference -- [[forgejo]] - Upstream OAuth2 provider +- [[authentik]] - OIDC identity provider reference - [[grafana]] - First OIDC client - [[security-model]] - Network security and access control -- [[adopt-oidc-provider]] - Implementation plan (completed) +- [[deploy-authentik]] - Deployment how-to diff --git a/docs/how-to/authentik/deploy-authentik.md b/docs/how-to/authentik/deploy-authentik.md index d9ae037..224701d 100644 --- a/docs/how-to/authentik/deploy-authentik.md +++ b/docs/how-to/authentik/deploy-authentik.md @@ -1,6 +1,5 @@ --- title: Deploy Authentik Identity Provider -status: active modified: 2026-02-20 requires: - build-authentik-container @@ -16,7 +15,7 @@ tags: # Deploy Authentik Identity Provider -Replace [[dex]] with [Authentik](https://goauthentik.io/) as the SSO identity provider. Authentik is the **source of truth** for user identity in BlumeOps. Users are created and managed in Authentik; services authenticate against it via OIDC. Forgejo federation is deferred to a future effort (existing `eblume` account has extensive automations that need careful migration). +Replace Dex with [Authentik](https://goauthentik.io/) as the SSO identity provider. Authentik is the **source of truth** for user identity in BlumeOps. Users are created and managed in Authentik; services authenticate against it via OIDC. Forgejo federation is deferred to a future effort (existing `eblume` account has extensive automations that need careful migration). ## Architecture Decisions @@ -33,7 +32,7 @@ Replace [[dex]] with [Authentik](https://goauthentik.io/) as the SSO identity pr ## What Was Done -1. Built Nix container image (`v1.1.0-nix`) — `pkgs.authentik` + `coreutils` + `bashInteractive` +1. Built Nix container image (`v1.1.2-nix`) — `pkgs.authentik` + `coreutils` + `bashInteractive` + entrypoint wrapper for blueprint symlinks 2. Created 1Password item "Authentik (blumeops)" with secret key and DB credentials 3. Provisioned `authentik` database and CNPG managed role on `blumeops-pg` 4. Deployed to ringtail k3s: server, worker, Redis (3 deployments) @@ -41,6 +40,8 @@ Replace [[dex]] with [Authentik](https://goauthentik.io/) as the SSO identity pr 6. Tailscale Ingress at `authentik.tail8d86e.ts.net` 7. Caddy reverse proxy at `authentik.ops.eblu.me` 8. Completed first-run wizard (admin account created) +9. Migrated Grafana OIDC from Dex to Authentik (Blueprint-driven) +10. Decommissioned Dex (ArgoCD app deleted, manifests removed, Caddy entry removed) ## URLs @@ -55,7 +56,7 @@ Replace [[dex]] with [Authentik](https://goauthentik.io/) as the SSO identity pr ## Related -- [[dex]] — Current IdP (to be replaced by [[migrate-grafana-to-authentik]]) +- [[authentik]] — OIDC identity provider - [[federated-login]] — How authentication works across BlumeOps - [[adopt-oidc-provider]] — Dex deployment plan (completed) - [[ringtail]] — Target cluster diff --git a/docs/how-to/authentik/migrate-grafana-to-authentik.md b/docs/how-to/authentik/migrate-grafana-to-authentik.md index 495bdb9..3729cc2 100644 --- a/docs/how-to/authentik/migrate-grafana-to-authentik.md +++ b/docs/how-to/authentik/migrate-grafana-to-authentik.md @@ -1,6 +1,5 @@ --- title: Migrate Grafana to Authentik -status: active modified: 2026-02-20 tags: - how-to @@ -12,65 +11,38 @@ tags: Move Grafana's OIDC authentication from Dex to Authentik, then decommission Dex. -## Context +## What Was Done -Discovered while attempting [[deploy-authentik]]: Authentik is deployed and running, but no services use it yet. Grafana is the first client to migrate. Once Grafana is off Dex, Dex has no remaining clients and can be decommissioned. +### Blueprint loading fix -## What to Do +The Nix-built container hardcoded `blueprints_dir` to its Nix store path, making custom blueprints invisible. Fixed by adding a wrapper entrypoint that symlinks built-in blueprint dirs from `/nix/store/*authentik-django*/blueprints/` into `/blueprints/` at container start, with `AUTHENTIK_BLUEPRINTS_DIR=/blueprints` set in the container env. The `/blueprints` dir is created world-writable by `extraCommands` so user 65534 can write symlinks. Also fixed the `!Env` tag syntax in the blueprint YAML — `!Env` takes a scalar, not a sequence (`!Env FOO` not `!Env [FOO]`). -### Authentik configuration (via API, then capture as Blueprint) +### Authentik configuration (via Blueprint) -1. Create an `admins` group in Authentik -2. Ensure user `blume.erich@gmail.com` is in the `admins` group -3. Create an OAuth2/OIDC provider for Grafana (client ID: `grafana`, redirect URIs for both `grafana.ops.eblu.me` and `grafana.tail8d86e.ts.net`) -4. Create an Application for Grafana linked to the provider, gated to the `admins` group -5. Store the client secret in 1Password "Authentik (blumeops)" as `grafana-client-secret` -6. Capture the configuration as an Authentik Blueprint YAML in the manifests +- Blueprint at `argocd/manifests/authentik/configmap-blueprint.yaml` defines: `admins` group, Grafana OAuth2 provider (client ID: `grafana`), Grafana application, and policy binding +- Blueprint mounted as ConfigMap into worker at `/blueprints/custom/` +- `grafana-client-secret` stored in 1Password "Authentik (blumeops)" +- API token stored as `api-token` in same item ### Grafana configuration -1. Update `argocd/manifests/grafana/values.yaml` — change `auth.generic_oauth` from Dex to Authentik endpoints -2. Replace `external-secret-dex-oauth.yaml` with one that pulls from "Authentik (blumeops)" instead of "Dex (blumeops)" -3. Sync Grafana via ArgoCD and verify SSO login works +- `values.yaml` updated to point at Authentik OIDC endpoints (`authentik.ops.eblu.me`) +- `external-secret-authentik-oauth.yaml` pulls client secret from "Authentik (blumeops)" +- Old Dex OAuth user deleted from Grafana (different `auth_id` caused "user already exists") ### Dex decommission -1. Delete ArgoCD app `dex` -2. Remove `argocd/manifests/dex/` and `argocd/apps/dex.yaml` -3. Remove `dex` entry from Caddy reverse proxy (`ansible/roles/caddy/defaults/main.yml`) -4. Provision Caddy to apply the change +- ArgoCD app `dex` deleted (cascade removed k8s resources from ringtail) +- Removed `argocd/manifests/dex/`, `argocd/apps/dex.yaml`, `external-secret-dex-oauth.yaml` +- Removed `dex` entry from Caddy reverse proxy config -## What Was Done So Far +## Lessons Learned -### Completed - -- API token created and stored in 1Password "Authentik (blumeops)" field `api-token` -- `grafana-client-secret` generated and stored in 1Password "Authentik (blumeops)" -- Blueprint YAML created at `argocd/manifests/authentik/configmap-blueprint.yaml` defining: admins group, Grafana OAuth2 provider, Grafana application, and policy binding -- Blueprint ConfigMap mounted into worker at `/blueprints/custom/` -- ExternalSecret updated to pull `grafana-client-secret` from 1Password -- Grafana `values.yaml` updated to point at Authentik OIDC endpoints -- `external-secret-authentik-oauth.yaml` created to replace `external-secret-dex-oauth.yaml` - -### Blocked: Blueprint not loading - -**Root cause:** The Nix-built container hardcodes `blueprints_dir` to `/nix/store/3h1g...authentik-django-2025.10.1/blueprints` in its `default.yml`. Custom blueprints mounted at `/blueprints/custom/` are invisible because that path is not on the search path. - -**Fix options:** -1. Set env var `AUTHENTIK_BLUEPRINTS_DIR=/blueprints` and mount custom blueprints alongside copies/symlinks of the built-in ones — risky, could break built-in blueprints if the path doesn't include them. -2. Mount the custom blueprint ConfigMap directly into the Nix store blueprints path (e.g., `/nix/store/.../blueprints/custom/`) — fragile, path changes on rebuild. -3. Use the API to apply the configuration and skip file-based blueprints for now. Store the API calls in a mise task for reproducibility. -4. Patch the Nix container to set a writable `blueprints_dir` or create a wrapper that symlinks. - -**Recommendation:** Option 4 (patch container) or option 1 (override env var) are the cleanest. Need to test whether `AUTHENTIK_BLUEPRINTS_DIR` is respected and whether built-in blueprints still load from the Nix store path when overridden. - -## Notes - -- Authentik API token stored as `api-token` in 1Password "Authentik (blumeops)". -- The `admins` group and Grafana provider/application created via API during investigation were cleaned up (deleted). +- `buildLayeredImage`'s `extraCommands` can't access Nix store paths from `contents` — they're in separate layers. Use a runtime entrypoint wrapper for symlinks instead. +- Authentik `!Env` tag takes a bare scalar (`!Env FOO`), not a YAML sequence (`!Env [FOO]`). The `!Find` tag does use sequences. +- When migrating OAuth providers, the subject ID (`auth_id`) changes. Existing Grafana users must be deleted before the new provider can recreate them. ## Related - [[deploy-authentik]] — Parent goal - [[grafana]] — Grafana reference -- [[dex]] — Current IdP being replaced diff --git a/docs/how-to/how-to.md b/docs/how-to/how-to.md index 4d1c364..4e594ef 100644 --- a/docs/how-to/how-to.md +++ b/docs/how-to/how-to.md @@ -67,7 +67,7 @@ Migration and transition plans for upcoming infrastructure changes. ## Authentik -Mikado chain for replacing Dex with Authentik. Track progress with `mise run docs-mikado deploy-authentik`. +Mikado chain for deploying Authentik. Track progress with `mise run docs-mikado deploy-authentik`. - [[deploy-authentik]] - [[build-authentik-container]] diff --git a/docs/how-to/plans/completed/adopt-oidc-provider.md b/docs/how-to/plans/completed/adopt-oidc-provider.md index 73d5568..627a6c5 100644 --- a/docs/how-to/plans/completed/adopt-oidc-provider.md +++ b/docs/how-to/plans/completed/adopt-oidc-provider.md @@ -10,7 +10,7 @@ tags: # Plan: Adopt OIDC Identity Provider -> **Status:** Completed (2026-02-19) — Phase 1 (Dex + Grafana) +> **Status:** Completed (2026-02-19) — Phase 1 (Dex + Grafana). Dex was subsequently replaced by [[authentik]] (see [[deploy-authentik]]). > **PR:** #222 ## Background @@ -98,7 +98,7 @@ Key design decisions: ## Related -- [[dex]] - Service reference card +- [[authentik]] - Current OIDC identity provider (replaced Dex) - [[federated-login]] - How authentication works across BlumeOps - [[harden-zot-registry]] - Future OIDC client - [[forgejo]] - Upstream OAuth2 provider diff --git a/docs/how-to/plans/completed/completed.md b/docs/how-to/plans/completed/completed.md index d3b1c9f..3ebfb06 100644 --- a/docs/how-to/plans/completed/completed.md +++ b/docs/how-to/plans/completed/completed.md @@ -15,4 +15,4 @@ Plans that have been fully implemented and verified. Kept for historical referen | [[adopt-dagger-ci]] | 2026-02-11 | Adopt Dagger as CI/CD build engine (Phases 1–3) | | [[segment-home-network]] | 2026-02-14 | Manual three-network segmentation for UniFi Express 7 | | [[operationalize-reolink-camera]] | 2026-02-15 | Deploy Frigate NVR stack with Mosquitto, Ntfy, and frigate-notify | -| [[adopt-oidc-provider]] | 2026-02-19 | Deploy Dex OIDC identity provider with Forgejo backend and Grafana SSO | +| [[adopt-oidc-provider]] | 2026-02-19 | Deploy OIDC identity provider with Grafana SSO (initially Dex, replaced by Authentik) | diff --git a/docs/how-to/plans/harden-zot-registry.md b/docs/how-to/plans/harden-zot-registry.md index 78efd70..dc4a516 100644 --- a/docs/how-to/plans/harden-zot-registry.md +++ b/docs/how-to/plans/harden-zot-registry.md @@ -65,7 +65,7 @@ Zot supports native OIDC authentication with a built-in API key feature designed "oidc": { "name": "BlumeOps", "credentialsFile": "/Users/erichblume/.config/zot/oidc-credentials.json", - "issuer": "https://dex.ops.eblu.me", + "issuer": "https://authentik.ops.eblu.me", "scopes": ["openid", "profile", "email"] } } @@ -97,7 +97,7 @@ Zot supports native OIDC authentication with a built-in API key feature designed The OIDC credentials file (client ID and secret) is deployed by Ansible from 1Password — never committed to the repo. **CI push flow after setup:** -1. Log in to zot UI via browser (OIDC redirect to Dex) +1. Log in to zot UI via browser (OIDC redirect to Authentik) 2. Generate an API key: `POST /zot/auth/apikey` with label `forgejo-ci`, scoped to `blumeops/**` 3. Store the key in 1Password (`op://blumeops/zot-ci-apikey/credential`) 4. CI uses the key: `docker login -u eblume -p zak_... registry.ops.eblu.me` @@ -122,7 +122,7 @@ The push-side approach is pragmatic: it prevents accidental overwrites in the no The `ansible/roles/zot/` role needs: -- **New template:** `oidc-credentials.json.j2` (client ID and secret for the Dex OIDC client) +- **New template:** `oidc-credentials.json.j2` (client ID and secret for the Authentik OIDC client) - **Updated config template:** `config.json.j2` gains `http.auth` (openid + apikey) and `accessControl` sections - **Updated config template:** `config.json.j2` gains `externalUrl` set to `https://registry.ops.eblu.me` (required for OIDC callback redirects behind Caddy) - **New variables:** `zot_oidc_client_id` and `zot_oidc_client_secret` sourced from 1Password in the playbook's `pre_tasks` @@ -146,7 +146,7 @@ The minikube containerd config (`ansible/roles/minikube/tasks/main.yml`) current ## Execution Steps 1. **Prerequisite: OIDC provider is running** (see [[adopt-oidc-provider]]) - - Dex (or chosen provider) is deployed and serving `https://dex.ops.eblu.me` + - Authentik (or chosen provider) is deployed and serving `https://authentik.ops.eblu.me` - A zot OIDC client is registered with the provider 2. **Update Ansible role** @@ -161,7 +161,7 @@ The minikube containerd config (`ansible/roles/minikube/tasks/main.yml`) current - Verify unauthenticated push fails: `skopeo copy ... docker://registry.ops.eblu.me/blumeops/test:fail` (should get 401) 4. **Set up OIDC login and generate CI API key** - - Log in to zot UI via browser (OIDC flow through Dex) + - Log in to zot UI via browser (OIDC flow through Authentik) - Generate an API key for CI use, store in 1Password - Verify authenticated push works: `docker login -u eblume -p zak_... registry.ops.eblu.me` @@ -178,7 +178,7 @@ The minikube containerd config (`ansible/roles/minikube/tasks/main.yml`) current - [ ] Anonymous pull works (k8s pods, containerd, curl) - [ ] Pull-through caching still works (pull an uncached image from docker.io) - [ ] Unauthenticated push is rejected (401) -- [ ] OIDC browser login works (redirect to Dex and back) +- [ ] OIDC browser login works (redirect to Authentik and back) - [ ] API key generation works from zot UI - [ ] Authenticated push with API key succeeds - [ ] Pushing a duplicate version tag fails (immutability check) diff --git a/docs/reference/infrastructure/ringtail.md b/docs/reference/infrastructure/ringtail.md index 940246d..70b4ebe 100644 --- a/docs/reference/infrastructure/ringtail.md +++ b/docs/reference/infrastructure/ringtail.md @@ -68,7 +68,7 @@ Sync order: `1password-connect-ringtail` -> `external-secrets-crds-ringtail` -> | [[frigate]] | `frigate` | NVR with GPU-accelerated detection (RTX 4080) | | [[frigate]]-notify | `frigate` | MQTT-to-ntfy alert bridge | | Mosquitto | `mqtt` | MQTT broker for Frigate events | -| [[dex]] | `dex` | OIDC identity provider (Forgejo-backed) | +| [[authentik]] | `authentik` | OIDC identity provider | | [[ntfy]] | `ntfy` | Push notification server | | nvidia-device-plugin | `nvidia-device-plugin` | Exposes GPU to pods via CDI + nvidia RuntimeClass | diff --git a/docs/reference/reference.md b/docs/reference/reference.md index 5c60599..f070857 100644 --- a/docs/reference/reference.md +++ b/docs/reference/reference.md @@ -37,7 +37,7 @@ Individual service reference cards with URLs and configuration details. | [[zot]] | Container registry | indri | | [[devpi]] | PyPI caching proxy | k8s | | [[cv]] | Resume / CV site | k8s | -| [[dex]] | OIDC identity provider | k8s (ringtail) | +| [[authentik]] | OIDC identity provider | k8s (ringtail) | | [[docs]] | Documentation site (Quartz) | k8s | | [[flyio-proxy]] | Public reverse proxy (Fly.io + Tailscale) | Fly.io | | [[automounter]] | SMB share automounter | indri | diff --git a/docs/reference/services/authentik.md b/docs/reference/services/authentik.md new file mode 100644 index 0000000..f52f56a --- /dev/null +++ b/docs/reference/services/authentik.md @@ -0,0 +1,78 @@ +--- +title: Authentik +modified: 2026-02-20 +tags: + - service + - security + - oidc +--- + +# Authentik + +OIDC identity provider for BlumeOps. Authentik is the **source of truth** for user identity — users are created and managed in Authentik, and services authenticate against it via OIDC. + +## Quick Reference + +| Property | Value | +|----------|-------| +| **URL** | https://authentik.ops.eblu.me | +| **Admin UI** | https://authentik.ops.eblu.me/if/admin/ | +| **Tailscale URL** | https://authentik.tail8d86e.ts.net | +| **Namespace** | `authentik` | +| **Cluster** | k3s (ringtail) | +| **Image** | `registry.ops.eblu.me/blumeops/authentik:v1.1.2-nix` | +| **Manifests** | `argocd/manifests/authentik/` | +| **Container build** | `containers/authentik/default.nix` | + +## Architecture + +Authentik runs on [[ringtail]]'s k3s cluster, isolated from the main services on indri's minikube. This means the IdP is independent of the minikube cluster lifecycle. + +Three deployments: +- **server** — HTTP/HTTPS interface, handles OIDC flows +- **worker** — Background tasks, blueprint application +- **redis** — Caching, sessions, task queue + +## Database + +Uses the shared CNPG `blumeops-pg` cluster on [[indri]], accessed cross-cluster via `pg.ops.eblu.me:5432`. Database `authentik` with managed role. + +## Blueprints + +Authentik configuration is managed via Blueprints (YAML) stored as a ConfigMap mounted into the worker at `/blueprints/custom/`. Current blueprints define: + +- `admins` group +- Grafana OAuth2 provider (client ID: `grafana`) +- Grafana application with group-based policy binding + +Blueprint file: `argocd/manifests/authentik/configmap-blueprint.yaml` + +## OIDC Clients + +| Client | Status | +|--------|--------| +| [[grafana]] | Active | + +Future clients: [[forgejo]], [[argocd]], [[miniflux]], [[zot]] + +## Secrets + +Injected via [[external-secrets]] from the "Authentik (blumeops)" 1Password item. + +| 1Password Field | Purpose | +|-----------------|---------| +| `secret-key` | Authentik secret key | +| `db-password` | PostgreSQL password | +| `grafana-client-secret` | OIDC client secret for Grafana | +| `api-token` | Authentik API token | + +## Container Image + +Nix-built via `dockerTools.buildLayeredImage`. The entrypoint wrapper symlinks built-in blueprint directories from the Nix store into `/blueprints/` at runtime, allowing custom blueprints to coexist with defaults. `AUTHENTIK_BLUEPRINTS_DIR=/blueprints` overrides the hardcoded Nix store path. + +## Related + +- [[federated-login]] - How authentication works across BlumeOps +- [[grafana]] - First OIDC client +- [[deploy-authentik]] - Deployment how-to +- [[external-secrets]] - Secrets injection from 1Password diff --git a/docs/reference/services/dex.md b/docs/reference/services/dex.md deleted file mode 100644 index 09b0b30..0000000 --- a/docs/reference/services/dex.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: Dex -modified: 2026-02-19 -tags: - - service - - security - - oidc ---- - -# Dex - -OIDC identity provider for BlumeOps. Dex federates authentication — downstream services (Grafana, future ArgoCD, etc.) delegate login to Dex, and Dex delegates to [[forgejo]] as the upstream OAuth2 provider. - -## Quick Reference - -| Property | Value | -|----------|-------| -| **URL** | https://dex.ops.eblu.me | -| **Tailscale URL** | https://dex.tail8d86e.ts.net | -| **Namespace** | `dex` | -| **Cluster** | k3s (ringtail) | -| **Image** | `registry.ops.eblu.me/blumeops/dex:v1.0.0-nix` | -| **Upstream** | https://github.com/dexidp/dex | -| **Manifests** | `argocd/manifests/dex/` | -| **Container build** | `containers/dex/default.nix` | - -## Architecture - -Dex runs on [[ringtail]]'s k3s cluster, isolated from the main services on indri's minikube. This means the IdP is independent of the minikube cluster lifecycle — if minikube goes down, Dex stays up and services can still authenticate once restored. - -``` -User Browser - | - v -Grafana (indri/minikube) --OIDC--> Dex (ringtail/k3s) --OAuth2--> Forgejo (indri/native) - ^ | - | | - +---------------------- redirect back with token -------------------+ -``` - -Cross-cluster communication works because Grafana reaches Dex via `https://dex.ops.eblu.me` (Caddy → Tailscale → ringtail), not k8s-internal DNS. - -## Identity Source - -Dex uses a **Gitea connector** pointed at [[forgejo]] (`https://forge.ops.eblu.me`). Users authenticate with their Forgejo credentials. There are no static passwords — user management happens entirely in Forgejo. - -This means adding a new user to BlumeOps SSO is just creating a Forgejo account. - -## Storage - -SQLite3 with an `emptyDir` volume. This stores refresh tokens and auth codes. A pod restart invalidates active sessions (users re-login), which is acceptable for a homelab. No PVC needed. - -## OIDC Clients - -| Client | Redirect URIs | Status | -|--------|---------------|--------| -| [[grafana]] | `grafana.ops.eblu.me/login/generic_oauth`, `grafana.tail8d86e.ts.net/login/generic_oauth` | Active | - -Future clients: [[argocd]], [[forgejo]], [[miniflux]], [[zot]] - -## Secrets - -All sensitive configuration is injected via [[external-secrets]] from the "Dex (blumeops)" 1Password item. The entire `config.yaml` is templated in the ExternalSecret — nothing sensitive is committed to git. - -| 1Password Field | Purpose | -|-----------------|---------| -| `forgejo-client-id` | OAuth2 app client ID from Forgejo | -| `forgejo-client-secret` | OAuth2 app client secret from Forgejo | -| `grafana-client-secret` | OIDC client secret for Grafana | - -## Endpoints - -| Path | Purpose | -|------|---------| -| `/.well-known/openid-configuration` | OIDC discovery | -| `/auth` | Authorization (browser redirect) | -| `/token` | Token exchange | -| `/userinfo` | User info | -| `/keys` | JWKS (public keys) | -| `/callback` | OAuth2 callback from Forgejo | -| `/healthz` | Health check | - -## Related - -- [[federated-login]] - How authentication works across BlumeOps -- [[forgejo]] - Upstream OAuth2 provider -- [[grafana]] - First OIDC client -- [[routing]] - How Dex is exposed via Caddy -- [[external-secrets]] - Secrets injection from 1Password diff --git a/docs/reference/services/forgejo.md b/docs/reference/services/forgejo.md index cbbed04..7db0515 100644 --- a/docs/reference/services/forgejo.md +++ b/docs/reference/services/forgejo.md @@ -77,11 +77,9 @@ The Ansible role authenticates to the Forgejo API using a Personal Access Token This is a bootstrapping requirement - the PAT enables IaC for all other secrets. -## OAuth2 Provider for Dex +## Identity Provider -Forgejo acts as the upstream OAuth2 provider for [[dex]], the BlumeOps OIDC identity provider. An OAuth2 application is registered in Forgejo's Site Administration with a redirect URI pointing to Dex's callback (`https://dex.ops.eblu.me/callback`). Client credentials are stored in 1Password ("Dex (blumeops)"). - -This means Forgejo accounts are the source of truth for BlumeOps SSO identity. Adding a user to any Dex-integrated service (currently [[grafana]]) is just creating a Forgejo account. +[[authentik]] is the BlumeOps OIDC identity provider and source of truth for user identity. Forgejo will eventually authenticate against Authentik as an OIDC client, with user provisioning managed in Authentik. This migration is deferred — the existing `eblume` account has extensive automations that need careful migration. ## Future: Public Access @@ -104,5 +102,5 @@ See [[expose-service-publicly]] for the full howto and dynamic service checklist ## Related - [[argocd]] - Uses Forgejo as git source -- [[dex]] - OIDC identity provider (Forgejo is the upstream OAuth2 source) +- [[authentik]] - OIDC identity provider - [[zot]] - Container registry for built images diff --git a/docs/reference/services/grafana.md b/docs/reference/services/grafana.md index d6a38e7..ef48321 100644 --- a/docs/reference/services/grafana.md +++ b/docs/reference/services/grafana.md @@ -24,10 +24,10 @@ Dashboards and visualization for BlumeOps observability. Grafana supports two login methods: -- **SSO via [[dex]]** — federated login through [[forgejo]] (`auth.generic_oauth`). Users click "Sign in with Dex", authenticate at Forgejo, and are redirected back as Admin. -- **Local admin** — break-glass login using the password from 1Password ("Grafana (blumeops)"). Always available if Dex is down. +- **SSO via [[authentik]]** — OIDC login through Authentik (`auth.generic_oauth`). Users click "Sign in with Authentik", authenticate at Authentik, and are redirected back as Admin. +- **Local admin** — break-glass login using the password from 1Password ("Grafana (blumeops)"). Always available if Authentik is down. -The OIDC client secret is injected via [[external-secrets]] (`grafana-dex-oauth` secret in monitoring namespace). +The OIDC client secret is injected via [[external-secrets]] (`grafana-authentik-oauth` secret in monitoring namespace). ## Datasources @@ -57,7 +57,7 @@ Optional annotation: `grafana_folder: "FolderName"` ## Related -- [[dex]] - OIDC identity provider for SSO +- [[authentik]] - OIDC identity provider for SSO - [[prometheus]] - Metrics datasource - [[loki]] - Logs datasource - [[alloy|Alloy]] - Data collector diff --git a/mise-tasks/services-check b/mise-tasks/services-check index f21cae8..d09b237 100755 --- a/mise-tasks/services-check +++ b/mise-tasks/services-check @@ -81,7 +81,7 @@ check_http "Immich" "https://photos.ops.eblu.me/" check_http "Navidrome" "https://dj.ops.eblu.me/" check_http "CV" "https://cv.ops.eblu.me/" check_http "Ntfy" "https://ntfy.ops.eblu.me/v1/health" -check_http "Dex" "https://dex.ops.eblu.me/healthz" +check_http "Authentik" "https://authentik.ops.eblu.me/-/health/live/" check_http "Frigate" "https://nvr.ops.eblu.me/api/version" echo "" @@ -96,7 +96,7 @@ echo "" echo "Ringtail k3s pods:" check_service "mosquitto" "kubectl --context=k3s-ringtail -n mqtt get pods -l app=mosquitto -o jsonpath='{.items[0].status.phase}' | grep -q Running" check_service "ntfy" "kubectl --context=k3s-ringtail -n ntfy get pods -l app=ntfy -o jsonpath='{.items[0].status.phase}' | grep -q Running" -check_service "dex" "kubectl --context=k3s-ringtail -n dex get pods -l app=dex -o jsonpath='{.items[0].status.phase}' | grep -q Running" +check_service "authentik" "kubectl --context=k3s-ringtail -n authentik get pods -l component=server -o jsonpath='{.items[0].status.phase}' | grep -q Running" check_service "frigate" "kubectl --context=k3s-ringtail -n frigate get pods -l app=frigate -o jsonpath='{.items[0].status.phase}' | grep -q Running" check_service "frigate-notify" "kubectl --context=k3s-ringtail -n frigate get pods -l app=frigate-notify -o jsonpath='{.items[0].status.phase}' | grep -q Running" check_service "nvidia-device-plugin" "kubectl --context=k3s-ringtail -n nvidia-device-plugin get pods -l app=nvidia-device-plugin -o jsonpath='{.items[0].status.phase}' | grep -q Running" -- 2.50.1 (Apple Git-155)