Deploy Authentik identity provider (C2 Mikado) (#227)

## Summary
C2 Mikado chain for deploying Authentik as the SSO identity provider, replacing Dex.

This PR will evolve over multiple sessions. Each iteration adds documentation (prerequisite cards) and eventually code as leaf nodes are resolved.

## Current Mikado State
- **Goal:** `deploy-authentik` (active)
- **Leaf prerequisites:**
  - `build-authentik-container` — Build Nix container image
  - `provision-authentik-database` — Create PostgreSQL database on CNPG cluster
  - `create-authentik-secrets` — Create 1Password item with credentials

## Process refinements
- Updated agent-change-process with lessons from first attempt: reset code before committing cards, open PRs early

## Test plan
- [ ] `mise run docs-mikado` shows correct dependency chain
- [ ] Leaf nodes can be worked independently
- [ ] Container builds on ringtail
- [ ] Authentik starts and reaches healthy state
- [ ] Forgejo OAuth2 connector works

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/227
This commit is contained in:
Erich Blume 2026-02-20 12:55:59 -08:00
commit 71cb256527
46 changed files with 848 additions and 395 deletions

View file

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

View file

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

View file

@ -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.1.2-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"

View file

@ -0,0 +1,75 @@
---
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.1.2-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
- 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"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "1000m"
volumes:
- name: blueprints
configMap:
name: authentik-blueprints

View file

@ -0,0 +1,43 @@
---
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
- secretKey: grafana-client-secret
remoteRef:
key: "Authentik (blumeops)"
property: grafana-client-secret

View file

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

View file

@ -0,0 +1,13 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: authentik
resources:
- external-secret.yaml
- configmap-blueprint.yaml
- deployment-server.yaml
- deployment-worker.yaml
- deployment-redis.yaml
- service.yaml
- service-redis.yaml
- ingress-tailscale.yaml

View file

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

View file

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