From cdb0c923f8e45e1f4f9b5defc13ca8854f2947e6 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Thu, 19 Feb 2026 20:43:08 -0800 Subject: [PATCH] Add Dex OIDC documentation and services-check integration - Create Dex reference card (docs/reference/services/dex.md) - Write federated login explanation article - Add Dex to services-check (HTTP health + k3s pod) - Update Grafana docs with SSO authentication section - Update Forgejo docs with OAuth2 provider role - Add Dex to ringtail workloads table and reference index - Move adopt-oidc-provider plan to completed with final design Co-Authored-By: Claude Opus 4.6 --- docs/changelog.d/docs-dex-oidc.doc.md | 1 + docs/explanation/explanation.md | 1 + docs/explanation/federated-login.md | 87 ++++++++ docs/how-to/plans/adopt-oidc-provider.md | 208 ------------------ .../plans/completed/adopt-oidc-provider.md | 105 +++++++++ docs/how-to/plans/completed/completed.md | 1 + docs/how-to/plans/plans.md | 2 +- docs/reference/infrastructure/ringtail.md | 1 + docs/reference/reference.md | 1 + docs/reference/services/dex.md | 89 ++++++++ docs/reference/services/forgejo.md | 7 + docs/reference/services/grafana.md | 10 + mise-tasks/services-check | 2 + 13 files changed, 306 insertions(+), 209 deletions(-) create mode 100644 docs/changelog.d/docs-dex-oidc.doc.md create mode 100644 docs/explanation/federated-login.md delete mode 100644 docs/how-to/plans/adopt-oidc-provider.md create mode 100644 docs/how-to/plans/completed/adopt-oidc-provider.md create mode 100644 docs/reference/services/dex.md diff --git a/docs/changelog.d/docs-dex-oidc.doc.md b/docs/changelog.d/docs-dex-oidc.doc.md new file mode 100644 index 0000000..3e61ac4 --- /dev/null +++ b/docs/changelog.d/docs-dex-oidc.doc.md @@ -0,0 +1 @@ +Add Dex OIDC documentation: reference card, federated login explanation, services-check integration, and updated plan. diff --git a/docs/explanation/explanation.md b/docs/explanation/explanation.md index bcc0509..59ffb0a 100644 --- a/docs/explanation/explanation.md +++ b/docs/explanation/explanation.md @@ -21,4 +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) | | [[security-model]] | Network security, secrets, and access control | diff --git a/docs/explanation/federated-login.md b/docs/explanation/federated-login.md new file mode 100644 index 0000000..0537ebb --- /dev/null +++ b/docs/explanation/federated-login.md @@ -0,0 +1,87 @@ +--- +title: Federated Login +modified: 2026-02-19 +last-reviewed: 2026-02-19 +tags: + - explanation + - security + - oidc +--- + +# Federated Login + +> **Note:** This article was drafted by AI and reviewed by Erich. I plan to rewrite all explanatory content in my own words - these serve as placeholders to establish the documentation structure. + +How authentication works across BlumeOps services, and why it's designed this way. + +## The Problem + +Without centralized authentication, every service manages its own users independently. Grafana has an admin password, ArgoCD has a different admin password, Forgejo has local accounts, and zot has no auth at all. This creates several problems: + +- **Password sprawl** — different credentials for every service, all stored separately in 1Password +- **No onboarding path** — adding a collaborator means creating accounts in every service individually +- **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 + +BlumeOps uses a two-layer federated authentication model: + +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. + +## The Login Flow + +When a user clicks "Sign in with Dex" 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 +``` + +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. + +## 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: + +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. + +## 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: + +- 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 + +No k8s-internal DNS crosses cluster boundaries. Everything uses the `*.ops.eblu.me` domain. + +## Related + +- [[dex]] - OIDC identity provider reference +- [[forgejo]] - Upstream OAuth2 provider +- [[grafana]] - First OIDC client +- [[security-model]] - Network security and access control +- [[adopt-oidc-provider]] - Implementation plan (completed) diff --git a/docs/how-to/plans/adopt-oidc-provider.md b/docs/how-to/plans/adopt-oidc-provider.md deleted file mode 100644 index 6e0d8bc..0000000 --- a/docs/how-to/plans/adopt-oidc-provider.md +++ /dev/null @@ -1,208 +0,0 @@ ---- -title: "Plan: Adopt OIDC Identity Provider" -modified: 2026-02-11 -tags: - - how-to - - plans - - security - - oidc ---- - -# Plan: Adopt OIDC Identity Provider - -> **Status:** Planning (design sketch — not yet ready to execute) - -## Background - -BlumeOps services currently handle authentication independently — ArgoCD has its own admin password, Grafana has its own login, Forgejo has local accounts, and zot has no auth at all. There is no single sign-on, no centralized user management, and no way to issue scoped API keys or service tokens from a shared identity. - -Adding an OpenID Connect (OIDC) identity provider gives BlumeOps a central authentication layer. Services delegate login to the IdP, and the IdP issues tokens that carry identity and group claims. This unlocks: - -- **SSO across services** — one login for Grafana, ArgoCD, Forgejo, zot, and future services -- **API keys derived from identity** — zot's API key feature requires OIDC; CI service accounts get scoped, expirable tokens tied to a real identity -- **Group-based authorization** — services can make access decisions based on IdP group claims rather than per-service user lists -- **Audit trail** — authentication events flow through one system - -### Goals - -- Deploy a lightweight OIDC provider on the BlumeOps infrastructure -- Configure at least one service (zot) as a relying party to validate the setup -- Establish patterns for adding future OIDC clients (Grafana, ArgoCD, Forgejo) -- Keep complexity appropriate for a single-user homelab - -## Provider Comparison - -| Provider | Language | Resources | UI | OIDC Maturity | Zot Integration | Notes | -|----------|----------|-----------|-----|---------------|-----------------|-------| -| **Dex** | Go | ~20-50MB RAM | None (config-driven) | Mature, purpose-built | Explicitly documented in zot examples | CNCF Sandbox; `staticPasswords` connector for single-user | -| **Authentik** | Python | ~200-300MB RAM, needs PostgreSQL + Redis | Full web UI, visual flow builder | Mature | [Proven community guide](https://integrations.goauthentik.io/infrastructure/zot/) | Best for small teams; heavier than needed for one user | -| **Authelia** | Go | ~30MB RAM | None (YAML config) | Maturing (OIDC provider still on roadmap) | [Unresolved integration issues](https://github.com/authelia/authelia/discussions/7615) | Primarily a forward-auth proxy; OIDC is secondary | -| **Keycloak** | Java | ~500MB+ RAM | Enterprise admin console | Battle-tested | Works via generic OIDC | Massive overkill for homelab | - -### Recommendation: Investigate Dex First - -Dex is the strongest candidate for BlumeOps: - -- **Lightest footprint** — single Go binary, no database dependencies (in-memory or SQLite storage) -- **Designed for exactly this** — Dex is an OIDC provider that federates identity; it's not a full IAM suite bolted onto other things -- **Zot uses Dex in its own examples** — lowest integration risk -- **`staticPasswords` connector** — define the single `eblume` user directly in YAML config, no external user store needed -- **Future flexibility** — if SSO via GitHub or Google is ever wanted, add a connector without changing the architecture -- **CNCF project** — actively maintained, well-documented - -The main trade-off is no web UI for user management — but for a single-user setup, that's a non-issue. Config changes go through the normal PR workflow. - -If Dex proves insufficient during execution (e.g., missing features for a specific service integration), Authentik is the fallback — heavier but more capable. - -## Architecture - -``` - Caddy (TLS termination) - | - +--------------+--------------+ - | | | - Browser SSO CLI / CI k8s services - | | | - v v v - Dex (OIDC IdP) API Keys OIDC tokens - issuer: (generated (validated by - dex.ops.eblu.me after OIDC each service) - | login) - v - staticPasswords - connector (eblume) -``` - -### Deployment Options - -Dex can run as: - -1. **k8s pod** (via ArgoCD) — follows the pattern of other BlumeOps services, gets automatic restarts, lives alongside its consumers -2. **Native on indri** (via Ansible/LaunchAgent) — follows the zot/Forgejo pattern, simpler networking - -The k8s option is preferred since most OIDC consumers (Grafana, ArgoCD) are already in k8s. Evaluate during execution. - -### Endpoints - -| Endpoint | URL | Purpose | -|----------|-----|---------| -| Issuer | `https://dex.ops.eblu.me` | OIDC discovery (`/.well-known/openid-configuration`) | -| Auth | `https://dex.ops.eblu.me/auth` | Browser login redirect | -| Token | `https://dex.ops.eblu.me/token` | Token exchange | -| Callback | Per-client (e.g., `https://registry.ops.eblu.me/zot/auth/callback/oidc`) | OAuth2 redirect URI | - -## Dex Configuration Sketch - -```yaml -issuer: https://dex.ops.eblu.me - -storage: - type: sqlite3 - config: - file: /var/dex/dex.db - -web: - http: 0.0.0.0:5556 - -connectors: - - type: local - id: local - name: Local - -staticPasswords: - - email: eblume@eblume.net - hash: "" # generated at deploy time - username: eblume - userID: "" - -staticClients: - - id: zot-registry - name: Zot Registry - secret: "" - redirectURIs: - - https://registry.ops.eblu.me/zot/auth/callback/oidc - - # Future clients: - # - id: grafana - # ... - # - id: argocd - # ... - # - id: forgejo - # ... -``` - -Secrets (static password hash, client secrets) are stored in 1Password and injected at deploy time — never committed to the repo. - -## Planned OIDC Clients - -Initial rollout targets zot only. Future services to integrate: - -| Service | OIDC Support | Priority | Notes | -|---------|-------------|----------|-------| -| **Zot** | Native (`openid.providers.oidc`) | First (validates IdP) | See [[harden-zot-registry]] | -| **Grafana** | Native (`auth.generic_oauth`) | High | Currently uses default admin password | -| **ArgoCD** | Native (`oidc.config` in `argocd-cm`) | High | Currently uses local admin password | -| **Forgejo** | Native (OAuth2 provider in admin settings) | Medium | Currently uses local accounts | - -## Execution Steps - -1. **Choose deployment method** (k8s vs native) and set up the service - - If k8s: create `argocd/manifests/dex/` with Deployment, Service, ConfigMap - - If native: create `ansible/roles/dex/` following the zot pattern - - Add Caddy reverse proxy entry for `dex.ops.eblu.me` - -2. **Configure Dex** - - Generate static password hash and client secrets - - Store all secrets in 1Password - - Deploy initial config with `staticPasswords` connector and zot as the first client - -3. **Verify OIDC discovery** - - `curl https://dex.ops.eblu.me/.well-known/openid-configuration` returns valid JSON - - Issuer URL matches config - -4. **Integrate first client (zot)** - - This is covered by [[harden-zot-registry]] — configure zot's `openid.providers.oidc` to point at Dex - - Test browser login → API key generation → CLI push flow - -5. **Documentation** - - Create `docs/reference/services/dex.md` reference card - - Update service indexes - - Add changelog fragment - -## Verification Checklist - -- [ ] Dex is running and healthy -- [ ] OIDC discovery endpoint returns valid configuration -- [ ] Browser login flow works (redirect → Dex login → redirect back) -- [ ] At least one client (zot) successfully authenticates via Dex -- [ ] Caddy proxies `dex.ops.eblu.me` correctly -- [ ] `mise run services-check` passes (if health check is added) - -## Open Questions - -- **Service dependency and recovery:** If Dex runs in k8s and k8s goes down, services that depend on Dex for authentication may become inaccessible — potentially including tools needed to bring k8s back up. This circular dependency **must be resolved** before execution. Options include: running Dex natively on indri (outside k8s), ensuring all critical recovery paths have break-glass credentials that bypass OIDC, or designing the system so that OIDC is additive (services fall back to local auth when the IdP is unreachable). This needs its own design pass during implementation planning. -- **Dex vs Authentik:** Dex is the starting recommendation, but evaluate during execution. If multiple services need dynamic user management or a web UI for client registration, Authentik may be worth the extra weight. -- **Storage backend:** SQLite is simplest for single-node. If Dex runs in k8s, it needs a PersistentVolume or could use the k8s CRD storage backend instead. -- **Tailscale ACL interaction:** Should the Dex endpoint be tailnet-only, or accessible from the public internet (for potential external SSO)? Start with tailnet-only. -- **Token lifetime and refresh:** Dex defaults are reasonable, but may need tuning for long-running CI jobs. - -## Future Considerations - -- **Additional connectors** — add GitHub or Google as upstream identity sources for SSO convenience -- **Group claims** — define groups in Dex config (e.g., `admin`, `ci`) and use them for authorization across services -- **Mutual TLS** — Dex supports mTLS for service-to-service token exchange, which could harden the CI credential path - -## Reference Pattern Files - -| File | Purpose | -|------|---------| -| `argocd/manifests/grafana-config/` | Example k8s service with ConfigMap-based config | -| `ansible/roles/zot/` | Example native service deployment pattern | -| `pulumi/tailscale/` | Example of secrets injection from 1Password | - -## Related - -- [[harden-zot-registry]] — first OIDC client (execute after this plan) -- [[zot]] — container registry reference -- [[cluster]] — k8s cluster (potential Dex host) -- [[indri]] — native service host (alternative Dex host) diff --git a/docs/how-to/plans/completed/adopt-oidc-provider.md b/docs/how-to/plans/completed/adopt-oidc-provider.md new file mode 100644 index 0000000..73d5568 --- /dev/null +++ b/docs/how-to/plans/completed/adopt-oidc-provider.md @@ -0,0 +1,105 @@ +--- +title: "Plan: Adopt OIDC Identity Provider" +modified: 2026-02-19 +tags: + - how-to + - plans + - security + - oidc +--- + +# Plan: Adopt OIDC Identity Provider + +> **Status:** Completed (2026-02-19) — Phase 1 (Dex + Grafana) +> **PR:** #222 + +## Background + +BlumeOps services currently handle authentication independently — ArgoCD has its own admin password, Grafana has its own login, Forgejo has local accounts, and zot has no auth at all. There is no single sign-on, no centralized user management, and no way to issue scoped API keys or service tokens from a shared identity. + +Adding an OpenID Connect (OIDC) identity provider gives BlumeOps a central authentication layer. Services delegate login to the IdP, and the IdP issues tokens that carry identity claims. + +## Final Design + +### Provider: Dex + +Dex was chosen for its lightweight footprint (single Go binary, ~50MB RAM), config-driven operation (no web UI needed), and native Gitea/Forgejo connector support. + +### Architecture + +``` +User Browser + | + v +Grafana (indri/minikube) --OIDC--> Dex (ringtail/k3s) --OAuth2--> Forgejo (indri/native) + ^ | + | | + +---------------------- redirect back with token -------------------+ +``` + +Key design decisions: + +- **Dex runs on ringtail's k3s cluster** — isolates the IdP from indri's minikube. If minikube goes down, Dex stays up. Recovery path: SSH → indri → ArgoCD local admin → fix. +- **Forgejo is the upstream identity source** — not static passwords. Users authenticate with their Forgejo account. Adding a user to SSO = creating a Forgejo account. +- **SQLite3 storage with emptyDir** — avoids a Kubernetes CRD storage bug (Go URL parsing issue with in-cluster API address). Pod restart invalidates sessions (users re-login), acceptable for a homelab. +- **NixOS-built container** — `containers/dex/default.nix` using `pkgs.dex-oidc`, consistent with the ntfy pattern. +- **Full config templated via ExternalSecret** — the entire `config.yaml` lives in the ExternalSecret template with secrets injected from 1Password. Nothing sensitive in git. +- **Cross-cluster communication** — Grafana reaches Dex via `https://dex.ops.eblu.me` (Caddy → Tailscale → ringtail), not k8s-internal DNS. + +### Resolved Open Questions + +- **Service dependency and recovery:** Dex on ringtail is independent of minikube. All services keep local admin logins as break-glass. If Dex goes down, users log in locally. +- **Dex vs Authentik:** Dex confirmed as the right choice. Config-driven, minimal resource usage, native Forgejo connector. +- **Storage backend:** SQLite3 (not Kubernetes CRDs). The CRD backend crashes due to a Go URL parsing bug with k3s's in-cluster API address. SQLite3 with emptyDir is simpler and avoids the issue. +- **User management scaling:** Forgejo connector solves this. Users are managed in Forgejo, not in Dex config files. Future option to add Google/GitHub connectors alongside Forgejo. +- **Tailscale ACL interaction:** Dex is tailnet-only via Caddy. Public access is a future consideration tied to exposing Forgejo publicly. + +## Execution (as completed) + +1. Created `containers/dex/default.nix` and built `dex:v1.0.0-nix` +2. Created 1Password item "Dex (blumeops)" with Forgejo OAuth2 credentials and Grafana client secret +3. Created OAuth2 application in Forgejo (Site Administration → Applications, confidential client, redirect URI `https://dex.ops.eblu.me/callback`) +4. Created ArgoCD app (`argocd/apps/dex.yaml`) targeting ringtail +5. Created k8s manifests: ExternalSecret, Deployment, Service, Ingress (5 files in `argocd/manifests/dex/`) +6. Added `dex.ops.eblu.me` to Caddy reverse proxy config +7. Created `grafana-dex-oauth` ExternalSecret for Grafana's OIDC client secret +8. Added `auth.generic_oauth` to Grafana's `values.yaml` with Dex endpoints +9. Fixed Grafana `root_url` from `grafana.tail8d86e.ts.net` to `grafana.ops.eblu.me` (OAuth state cookie mismatch) +10. Deployed and verified end-to-end SSO flow + +## Verification (completed) + +- [x] Container image exists: `dex:v1.0.0-nix` in registry +- [x] OIDC discovery endpoint returns valid configuration +- [x] Health check passes (`/healthz`) +- [x] Grafana login page shows "Sign in with Dex" button +- [x] OIDC flow: click Dex → Forgejo login → redirect back → logged in as Admin +- [x] Break-glass: local admin login still works +- [x] `mise run services-check` passes +- [x] ArgoCD shows dex app healthy and synced + +## Key Files + +| File | Purpose | +|------|---------| +| `containers/dex/default.nix` | NixOS container build | +| `argocd/apps/dex.yaml` | ArgoCD app (ringtail target) | +| `argocd/manifests/dex/` | K8s manifests (ExternalSecret, Deployment, Service, Ingress) | +| `argocd/manifests/grafana-config/external-secret-dex-oauth.yaml` | Grafana OIDC client secret | +| `argocd/manifests/grafana/values.yaml` | Grafana OIDC config (`auth.generic_oauth`) | +| `ansible/roles/caddy/defaults/main.yml` | Caddy reverse proxy entry | + +## Future Phases + +- **Phase 2:** ArgoCD OIDC (keep local admin, RBAC: `g, blume.erich@gmail.com, role:admin`) +- **Phase 3:** Forgejo OAuth2 provider integration (keep local accounts) +- **Phase 4:** Miniflux, Immich, other services +- **Phase 5:** Zot OIDC + hardening (per [[harden-zot-registry]]) + +## Related + +- [[dex]] - Service reference card +- [[federated-login]] - How authentication works across BlumeOps +- [[harden-zot-registry]] - Future OIDC client +- [[forgejo]] - Upstream OAuth2 provider +- [[grafana]] - First OIDC client diff --git a/docs/how-to/plans/completed/completed.md b/docs/how-to/plans/completed/completed.md index 3c349d1..d3b1c9f 100644 --- a/docs/how-to/plans/completed/completed.md +++ b/docs/how-to/plans/completed/completed.md @@ -15,3 +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 | diff --git a/docs/how-to/plans/plans.md b/docs/how-to/plans/plans.md index ee71efa..e3acdd5 100644 --- a/docs/how-to/plans/plans.md +++ b/docs/how-to/plans/plans.md @@ -17,7 +17,7 @@ Plans differ from regular how-to guides in that they describe work that has been | [[migrate-forgejo-from-brew]] | Planned | Transition Forgejo from Homebrew to source-built binary with LaunchAgent | | [[add-unifi-pulumi-stack]] | Abandoned | Add Pulumi IaC for UniFi Express 7 (provider bugs — see doc) | | [[upstream-fork-strategy]] | Planned | Stacked-branch forking strategy for tracking upstream projects | -| [[adopt-oidc-provider]] | Planning | Deploy OIDC identity provider for SSO across services | +| [[adopt-oidc-provider]] | Completed | Deploy OIDC identity provider for SSO across services | | [[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) | diff --git a/docs/reference/infrastructure/ringtail.md b/docs/reference/infrastructure/ringtail.md index 772566c..940246d 100644 --- a/docs/reference/infrastructure/ringtail.md +++ b/docs/reference/infrastructure/ringtail.md @@ -68,6 +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) | | [[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 97ada9f..5c60599 100644 --- a/docs/reference/reference.md +++ b/docs/reference/reference.md @@ -37,6 +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) | | [[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/dex.md b/docs/reference/services/dex.md new file mode 100644 index 0000000..09b0b30 --- /dev/null +++ b/docs/reference/services/dex.md @@ -0,0 +1,89 @@ +--- +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 e7c0b10..cbbed04 100644 --- a/docs/reference/services/forgejo.md +++ b/docs/reference/services/forgejo.md @@ -77,6 +77,12 @@ 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 + +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. + ## Future: Public Access Forgejo can be exposed publicly at `forge.eblu.me` via [[flyio-proxy]]. Since Forgejo runs natively on [[indri]] (not in k8s), the pattern is: @@ -98,4 +104,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) - [[zot]] - Container registry for built images diff --git a/docs/reference/services/grafana.md b/docs/reference/services/grafana.md index 4a135cc..d6a38e7 100644 --- a/docs/reference/services/grafana.md +++ b/docs/reference/services/grafana.md @@ -20,6 +20,15 @@ Dashboards and visualization for BlumeOps observability. | **Helm Chart** | grafana (mirrored to forge) | | **Values** | `argocd/manifests/grafana/values.yaml` | +## Authentication + +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. + +The OIDC client secret is injected via [[external-secrets]] (`grafana-dex-oauth` secret in monitoring namespace). + ## Datasources | Name | Type | Target | @@ -48,6 +57,7 @@ Optional annotation: `grafana_folder: "FolderName"` ## Related +- [[dex]] - 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 8ef7559..f21cae8 100755 --- a/mise-tasks/services-check +++ b/mise-tasks/services-check @@ -81,6 +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 "Frigate" "https://nvr.ops.eblu.me/api/version" echo "" @@ -95,6 +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 "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"