Deploy Dex OIDC identity provider with Grafana SSO #222
15 changed files with 267 additions and 0 deletions
Deploy Dex OIDC identity provider on ringtail with Grafana SSO
Adds Dex as a central OIDC identity provider running on ringtail's k3s cluster. Grafana is integrated as the first SSO client via generic_oauth. Dex uses Kubernetes CRD storage and ExternalSecrets for all sensitive config (bcrypt hash, client secrets from 1Password). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
commit
8765ee8706
|
|
@ -79,6 +79,9 @@ 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: ntfy
|
||||
host: "ntfy.{{ caddy_domain }}"
|
||||
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
|
||||
12
argocd/manifests/dex/clusterrole.yaml
Normal file
12
argocd/manifests/dex/clusterrole.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: dex
|
||||
rules:
|
||||
- apiGroups: ["dex.coreos.com"]
|
||||
resources: ["*"]
|
||||
verbs: ["*"]
|
||||
- apiGroups: ["apiextensions.k8s.io"]
|
||||
resources: ["customresourcedefinitions"]
|
||||
verbs: ["create"]
|
||||
13
argocd/manifests/dex/clusterrolebinding.yaml
Normal file
13
argocd/manifests/dex/clusterrolebinding.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: dex
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: dex
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: dex
|
||||
namespace: dex
|
||||
50
argocd/manifests/dex/deployment.yaml
Normal file
50
argocd/manifests/dex/deployment.yaml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dex
|
||||
namespace: dex
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: dex
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: dex
|
||||
spec:
|
||||
serviceAccountName: dex
|
||||
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
|
||||
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
|
||||
48
argocd/manifests/dex/external-secret.yaml
Normal file
48
argocd/manifests/dex/external-secret.yaml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
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: kubernetes
|
||||
config:
|
||||
inCluster: true
|
||||
web:
|
||||
http: 0.0.0.0:5556
|
||||
oauth2:
|
||||
skipApprovalScreen: true
|
||||
enablePasswordDB: true
|
||||
staticPasswords:
|
||||
- email: "blume.erich@gmail.com"
|
||||
hash: "{{ .staticPasswordHash }}"
|
||||
username: "eblume"
|
||||
userID: "eblume-001"
|
||||
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: staticPasswordHash
|
||||
remoteRef:
|
||||
key: "Dex (blumeops)"
|
||||
property: static-password-hash
|
||||
- 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
|
||||
12
argocd/manifests/dex/kustomization.yaml
Normal file
12
argocd/manifests/dex/kustomization.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: dex
|
||||
resources:
|
||||
- serviceaccount.yaml
|
||||
- clusterrole.yaml
|
||||
- clusterrolebinding.yaml
|
||||
- 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
|
||||
6
argocd/manifests/dex/serviceaccount.yaml
Normal file
6
argocd/manifests/dex/serviceaccount.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: dex
|
||||
namespace: dex
|
||||
|
|
@ -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:
|
||||
- ingress-tailscale.yaml
|
||||
- external-secret-admin.yaml
|
||||
- external-secret-dex-oauth.yaml
|
||||
- external-secret-teslamate-datasource.yaml
|
||||
# Dashboard ConfigMaps - discovered by Grafana sidecar via label grafana_dashboard=1
|
||||
- dashboards/configmap-borgmatic.yaml
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ admin:
|
|||
envFromSecrets:
|
||||
- name: grafana-teslamate-datasource
|
||||
optional: true
|
||||
- name: grafana-dex-oauth
|
||||
optional: true
|
||||
|
||||
# Persistence with PVC for SQLite database
|
||||
persistence:
|
||||
|
|
@ -28,6 +30,18 @@ grafana.ini:
|
|||
security:
|
||||
# Embedding disabled - iframe approach didn't work well for Homepage
|
||||
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:
|
||||
check_for_updates: 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