Document Dex OIDC and add services-check integration (#223)

## Summary
- Create Dex reference card (`docs/reference/services/dex.md`) with quick reference, architecture, identity source, storage, OIDC clients, secrets, and endpoints
- Write federated login explanation article (`docs/explanation/federated-login.md`) covering the Dex + Forgejo two-layer auth model, login flow, and break-glass access
- Add Dex to `services-check` (HTTP health endpoint + k3s pod check)
- Update Grafana docs with new Authentication section documenting SSO via Dex
- Update Forgejo docs with OAuth2 Provider section documenting its role as upstream identity source
- Add Dex to ringtail workloads table and reference service index
- Move `adopt-oidc-provider` plan to `completed/` with final design reflecting actual implementation

## Test plan
- [ ] `mise run services-check` passes (includes new Dex checks)
- [ ] `docs-check-links` passes (all wiki-links resolve)
- [ ] `docs-check-index` passes (new docs are indexed)

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/223
This commit is contained in:
Erich Blume 2026-02-19 20:44:23 -08:00
commit d21798b1f3
13 changed files with 306 additions and 209 deletions

View file

@ -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 |

View file

@ -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 |

View file

@ -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

View file

@ -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

View file

@ -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