Deploy Dex OIDC identity provider with Grafana SSO (#222)
## Summary - Deploys Dex OIDC identity provider on ringtail k3s cluster as central authentication service - Integrates Grafana as first SSO client via `auth.generic_oauth` - Uses Kubernetes CRD storage backend (no PVC needed) - All secrets (bcrypt hash, client secrets) injected via ExternalSecrets from 1Password item "Dex (blumeops)" - NixOS-built container image via `containers/dex/default.nix` ## Pre-requisites (manual, before deployment) 1. Create 1Password item "Dex (blumeops)" in `blumeops` vault with fields: - `password`: strong generated password for Dex login - `static-password-hash`: bcrypt hash of above (`htpasswd -BnC 10 eblume`, copy hash after `eblume:`) - `grafana-client-secret`: random 32-char hex (`openssl rand -hex 16`) 2. Build container: `mise run container-tag-and-release dex v1.0.0` ## Deployment sequence 1. Build container: `mise run container-tag-and-release dex v1.0.0` 2. Deploy Caddy: `mise run provision-indri -- --tags caddy` 3. Sync ArgoCD: `argocd app sync apps` → `argocd app sync dex` 4. Verify Dex: `curl https://dex.ops.eblu.me/.well-known/openid-configuration` 5. Sync Grafana: `argocd app sync grafana-config` → `argocd app sync grafana` 6. Test SSO: Visit `https://grafana.ops.eblu.me/login`, click "Sign in with Dex" ## Verification - [ ] Container image exists: `mise run container-list` shows `dex:v1.0.0-nix` - [ ] `curl https://dex.ops.eblu.me/.well-known/openid-configuration` returns valid OIDC discovery - [ ] `curl https://dex.ops.eblu.me/healthz` returns healthy - [ ] Grafana login shows "Sign in with Dex" button alongside local login - [ ] OIDC flow: click Dex → enter credentials → redirect back → logged in as Admin - [ ] Break-glass: local admin login still works - [ ] `mise run services-check` passes ## Files changed | File | Action | Purpose | |------|--------|---------| | `containers/dex/default.nix` | Create | NixOS container build | | `argocd/apps/dex.yaml` | Create | ArgoCD app targeting ringtail | | `argocd/manifests/dex/*` (8 files) | Create | K8s manifests (RBAC, ExternalSecret, Deployment, Service, Ingress) | | `argocd/manifests/grafana-config/external-secret-dex-oauth.yaml` | Create | Grafana OIDC client secret | | `argocd/manifests/grafana-config/kustomization.yaml` | Modify | Add new ExternalSecret resource | | `argocd/manifests/grafana/values.yaml` | Modify | Add `auth.generic_oauth` config + envFromSecrets | | `ansible/roles/caddy/defaults/main.yml` | Modify | Add `dex.ops.eblu.me` reverse proxy entry | | `docs/changelog.d/feature-dex-oidc.feature.md` | Create | Changelog fragment | Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/222
This commit is contained in:
parent
b876e39981
commit
0cdc143227
12 changed files with 244 additions and 1 deletions
|
|
@ -79,6 +79,9 @@ caddy_services:
|
||||||
- name: nvr
|
- name: nvr
|
||||||
host: "nvr.{{ caddy_domain }}"
|
host: "nvr.{{ caddy_domain }}"
|
||||||
backend: "https://nvr.tail8d86e.ts.net"
|
backend: "https://nvr.tail8d86e.ts.net"
|
||||||
|
- name: dex
|
||||||
|
host: "dex.{{ caddy_domain }}"
|
||||||
|
backend: "https://dex.tail8d86e.ts.net"
|
||||||
- name: ntfy
|
- name: ntfy
|
||||||
host: "ntfy.{{ caddy_domain }}"
|
host: "ntfy.{{ caddy_domain }}"
|
||||||
backend: "https://ntfy.tail8d86e.ts.net"
|
backend: "https://ntfy.tail8d86e.ts.net"
|
||||||
|
|
|
||||||
18
argocd/apps/dex.yaml
Normal file
18
argocd/apps/dex.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
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
|
||||||
53
argocd/manifests/dex/deployment.yaml
Normal file
53
argocd/manifests/dex/deployment.yaml
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
---
|
||||||
|
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: {}
|
||||||
55
argocd/manifests/dex/external-secret.yaml
Normal file
55
argocd/manifests/dex/external-secret.yaml
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
---
|
||||||
|
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
|
||||||
26
argocd/manifests/dex/ingress-tailscale.yaml
Normal file
26
argocd/manifests/dex/ingress-tailscale.yaml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
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
|
||||||
9
argocd/manifests/dex/kustomization.yaml
Normal file
9
argocd/manifests/dex/kustomization.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: dex
|
||||||
|
resources:
|
||||||
|
- external-secret.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress-tailscale.yaml
|
||||||
13
argocd/manifests/dex/service.yaml
Normal file
13
argocd/manifests/dex/service.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: dex
|
||||||
|
namespace: dex
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: dex
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 5556
|
||||||
|
targetPort: 5556
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
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
|
||||||
|
|
@ -6,6 +6,7 @@ namespace: monitoring
|
||||||
resources:
|
resources:
|
||||||
- ingress-tailscale.yaml
|
- ingress-tailscale.yaml
|
||||||
- external-secret-admin.yaml
|
- external-secret-admin.yaml
|
||||||
|
- external-secret-dex-oauth.yaml
|
||||||
- external-secret-teslamate-datasource.yaml
|
- external-secret-teslamate-datasource.yaml
|
||||||
# Dashboard ConfigMaps - discovered by Grafana sidecar via label grafana_dashboard=1
|
# Dashboard ConfigMaps - discovered by Grafana sidecar via label grafana_dashboard=1
|
||||||
- dashboards/configmap-borgmatic.yaml
|
- dashboards/configmap-borgmatic.yaml
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ admin:
|
||||||
envFromSecrets:
|
envFromSecrets:
|
||||||
- name: grafana-teslamate-datasource
|
- name: grafana-teslamate-datasource
|
||||||
optional: true
|
optional: true
|
||||||
|
- name: grafana-dex-oauth
|
||||||
|
optional: true
|
||||||
|
|
||||||
# Persistence with PVC for SQLite database
|
# Persistence with PVC for SQLite database
|
||||||
persistence:
|
persistence:
|
||||||
|
|
@ -24,10 +26,22 @@ persistence:
|
||||||
# Grafana configuration via grafana.ini
|
# Grafana configuration via grafana.ini
|
||||||
grafana.ini:
|
grafana.ini:
|
||||||
server:
|
server:
|
||||||
root_url: https://grafana.tail8d86e.ts.net
|
root_url: https://grafana.ops.eblu.me
|
||||||
security:
|
security:
|
||||||
# Embedding disabled - iframe approach didn't work well for Homepage
|
# Embedding disabled - iframe approach didn't work well for Homepage
|
||||||
allow_embedding: false
|
allow_embedding: false
|
||||||
|
auth.generic_oauth:
|
||||||
|
enabled: true
|
||||||
|
name: Dex
|
||||||
|
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
|
||||||
|
allow_sign_up: true
|
||||||
|
role_attribute_path: "'Admin'"
|
||||||
|
auto_login: false
|
||||||
analytics:
|
analytics:
|
||||||
check_for_updates: false
|
check_for_updates: false
|
||||||
reporting_enabled: false
|
reporting_enabled: false
|
||||||
|
|
|
||||||
28
containers/dex/default.nix
Normal file
28
containers/dex/default.nix
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# 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 <nixpkgs> { } }:
|
||||||
|
|
||||||
|
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";
|
||||||
|
};
|
||||||
|
}
|
||||||
1
docs/changelog.d/feature-dex-oidc.feature.md
Normal file
1
docs/changelog.d/feature-dex-oidc.feature.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Deploy Dex OIDC identity provider on ringtail with Grafana as first SSO client.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue