Commit Graph
103 Commits
Author SHA1 Message Date
claude-botandClaude Opus 4.8 de402ba531 Expose Zigbee2MQTT frontend via Authentik gate
ci/woodpecker/pr/pr-review Pipeline was successful
Gate zigbee2mqtt.home.renner.uno behind the same passkey-step-up Authentik
proxy-outpost pattern used for esphome/shelly-manager/vscode. The Z2M frontend
has no auth of its own (anyone on :8080 controls the whole Zigbee network), so
Traefik only ever routes the hostname to a dedicated outpost that validates the
session before forwarding.

New:
  - apps/home-assistant/zigbee2mqtt-gate-service.yaml — ExternalName mirror to
    ak-outpost-zigbee2mqtt-gate in the authentik ns (:9000).
  - apps/home-assistant/zigbee2mqtt-ingress.yaml — zigbee2mqtt.home.renner.uno →
    the outpost; cert-manager ingress-shim issues zigbee2mqtt-tls.
  - apps/authentik/zigbee2mqtt-gate-egress.yaml — CNP letting the outpost egress
    to the zigbee2mqtt pod on :8080.

Edited:
  - apps/authentik/blueprints-configmap.yaml — 5-entry blueprint block (group /
    proxyprovider / application / policybinding / outpost) named zigbee2mqtt-gate;
    internal_host carries the explicit :8080 (Service listens on 8080, unlike
    esphome's :80). Authentik spawns the ak-outpost-zigbee2mqtt-gate Deployment
    +Service from the outpost entry.
  - infrastructure/network-policies/home-assistant-ns.yaml — fromEndpoints rule
    admitting the zigbee2mqtt-gate outpost to :8080 (shared 8080 with vscode/
    shelly-manager; the fromEndpoints selector + the authentik-side egress scope
    it to the zigbee2mqtt pod).
  - the two kustomizations registering the new files.

DNS (k8s-gateway watches Ingress) and TLS (cert-manager) are automatic. Access
limited to the "Zigbee2MQTT Managers" group (k.renner). Verified: kustomize
builds, dry-run OK, blueprint YAML parses with the new block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:03:37 +02:00
claude-botandClaude Fable 5 ca61ff493c feat(dns): label-driven registration replaces the excludeDomains list
The dns.renner.uno/public=true label on an Ingress marks it as publicly
registered (Cloudflare wildcard -> NetBird proxy); external-dns-unifi
now runs with labelFilter dns.renner.uno/public!=true and skips those
resources entirely. Visibility is a property of the app resource itself
— there is no central name list to keep in sync with the proxy.

Also removes the dead .k.renner.uno hosts from the public apps'
Ingresses (their TLS certs reissue without those SANs) and splits
photos.renner.uno onto its own unlabeled Ingress: the label filter is
per-resource and that name stays LAN-registered. Remaining .k. names on
LAN-only apps (loki, ical-proxy, syslog) are untouched here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
2026-07-23 08:38:17 +02:00
claude-botandClaude Opus 4.8 dc42781a47 feat(authentik): automated SECRET_KEY rotation (openbao-ns)
Rotates authentik-secrets:secret-key (Django signing key) quarterly + on-demand via the openbao-ns pattern (per-secret rotator-authentik role/policy, no netpol). Self-sync 2-sided: server + worker read the same secret; Reloader restarts both via global.podAnnotations. smtp-password in the same KV entry is preserved. externalsecret refreshInterval 1h->10m. BLAST RADIUS: invalidates active SSO sessions + in-flight signed links (re-login); downstream LDAP/proxy outposts unaffected. Requires the one-time OpenBao bootstrap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015K7gLUVVnutvDnzV2TrMRJ
2026-07-18 22:06:58 +02:00
claude-botandClaude Opus 4.8 a9cb28949f feat(zone-configurator): Authentik-gated ingress an zone-configurator.home.renner.uno
Der Everything-Presence mmWave Zone-Configurator lief bereits im Cluster
(Deployment in ns home-assistant, UI :42069), war aber nur ClusterIP-intern
erreichbar. Er hält ein HA Long-Lived Token und kann die ESPHome-Zonen-Entities
aller Präsenzmelder umschreiben, hat aber keine eigene Auth — daher wird er
(wie vscode/shelly-manager/esphome) hinter ein Authentik Passkey-Gate gelegt,
nicht nackt exponiert.

Neu:
- Authentik-Blueprint zone-configurator-gate: Group "Zone Configurators"
  (k.renner), ProxyProvider (passkey-stepup, internal_host …:42069,
  external_host zone-configurator.home.renner.uno), Application, PolicyBinding,
  dedizierter Outpost.
- ExternalName-Service ak-outpost-zone-configurator-gate + Traefik-Ingress
  (cert-manager letsencrypt-cluster) im app-Verzeichnis.
- Cilium-Egress (authentik) + Ingress-Allow (home-assistant-ns) für
  outpost -> zone-configurator:42069. Port 42069 ist eindeutig (wie esphome
  :6052), daher kein Port-Overlap mit anderen Apps.

Traffic: browser -> traefik -> ak-outpost-zone-configurator-gate -> Outpost
(Passkey) -> zone-configurator.svc:42069.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 08:12:15 +02:00
claude-botandClaude Fable 5 fa437a608c feat(secrets): migrate authentik secrets to OpenBao (final batch)
authentik-secrets + samba-bridge config: values in OpenBao KV, both
round-trip-verified text. The identity provider was deliberately last —
every other consumer proved the pipeline first. ZERO sops files remain
under kubernetes/; authentik KS decryption dropped. Phase 4 (age-key
retirement) is now unblocked pending soak.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRgBFn9m8a2ncFMLusikH6
2026-07-14 01:34:04 +02:00
claude-botandClaude Opus 4.8 948852d83f feat(esphome): deploy ESPHome Device Builder behind an Authentik gate
Adds the ESPHome Device Builder (ghcr.io/esphome/esphome:2026.6.2, :6052) to the
home-assistant namespace so ESPHome devices — starting with an M5 Atom Echo BLE
proxy for shelly-manager's BLE provisioning — can be built, browser/USB-flashed
(ESP Web Tools / Web Serial), and config-managed in-cluster.

v1 deliberately runs with NO macvlan. Unlike shelly-manager it needs no L2
presence on the IoT VLAN: initial flashing is browser-side over USB and builds
are offline. That also sidesteps the macvlan-on-0.0.0.0 hole (a gated pod with a
macvlan bypasses Cilium policy and exposes its unauthenticated UI on the IoT VLAN
— confirmed against shelly-manager/matter-server/otbr). Live status / logs / OTA
would need cluster→v30iot reachability (a separate, still-open item); a macvlan
can be added later if wanted.

Mirrors the shelly-manager gate pattern exactly:
- home-assistant: esphome deployment (Recreate, RWO 10Gi ceph-block PVC for the
  PlatformIO build cache), service, gate ExternalName, and a Traefik ingress that
  backends the Authentik esphome-gate outpost (not esphome directly).
- authentik: an esphome-gate proxy provider + application + dedicated outpost
  (passkey step-up, "ESPHome Managers" group, git-owned membership) in
  blueprints-configmap.yaml, plus esphome-gate-egress CNP (outpost → :6052).
- network-policies: the home-assistant default-deny-ingress grants ONLY the
  esphome-gate outpost → :6052. :6052 is unique (no collision with the shared
  :8080 the vscode/shelly gates ride on).

Validated: kubectl kustomize builds cleanly for the home-assistant, authentik,
and network-policies kustomizations, all rendering the new resources.

Post-deploy checks: confirm the dashboard has no credentials:'omit' service
worker/manifest that 302-loops at the gate (add skip_path_regex if so); confirm
the image serves the dashboard on :6052 with the default entrypoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 19:08:59 +02:00
k.rennerandClaude Opus 4.8 ce0c78b503 Gate Prometheus behind Authentik
Prometheus (prometheus.renner.uno, monitoring ns, :9090) had no auth of its
own. Its admin API is disabled (read-only), but the full 90d metrics DB is
an infra-recon oracle — every hostname, IP and service in the cluster
(2026-07-10 audit). Gate it the same way as alertmanager-gate (#146).

- Blueprint: prometheus-gate proxy provider (internal_host
  http://kube-prometheus-stack-prometheus.monitoring.svc:9090, external_host
  https://prometheus.renner.uno), application, outpost. REUSES the existing
  git-owned "Monitoring Access" group — created generically in #146 for
  exactly this, so no new group.
- implicit-consent flow (same rationale as alertmanager/paperless: a
  frequently-checked monitoring UI; an authenticated session already closes
  the exposure).
- No skip_path_regex: the Prometheus UI is a plain React SPA — verified its
  index.html has no serviceWorker/manifest/webmanifest.
- Helm-generated Prometheus Ingress disabled (prometheus.ingress.enabled=
  false); standalone prometheus-gate-ingress.yaml backends the outpost
  ExternalName. Mirrors the alertmanager gate.
- Scoped CNPs: monitoring ingress rule allowing only the outpost -> :9090;
  matching egress in the authentik namespace.

Grafana is UNAFFECTED: its Prometheus datasource targets the ClusterIP
Service (kube-prometheus-stack-prometheus.monitoring:9090), verified in the
live datasource provisioning, not the ingress hostname.

Known side effect, documented not silently shipped: three blackbox
`http_2xx` uptime probes target https://prometheus.renner.uno
(kubernetes/apps/monitoring-rules/probes-blackbox.yaml, the srv-host
ansible prometheus.yml.j2, and the ansible blackbox-exporter role). After
gating they follow the 302 to the Authentik login (200), so they stay GREEN
— but now measure Authentik reachability, not Prometheus. This is already
true for alerts.renner.uno since #146 (verified: probe_success=1,
final_status=200 via redirect-follow). No probe goes red; the monitoring
just becomes a gate-liveness check. Repointing those probes to an internal
endpoint is a separate follow-up, not done here.

Verified: kubectl kustomize builds for authentik, monitoring,
network-policies; embedded outpost survived the blueprint insert; rendered
ingress backends ak-outpost-prometheus-gate:9000; both prometheus and
alertmanager helm ingresses now disabled; egress + CNP scoped to
prometheus:9090.

Post-merge checks: only the gate Ingress serves prometheus.renner.uno (helm
one gone); unauthenticated GET -> 302 to auth.renner.uno; unauthenticated
GET /api/v1/query -> 302; outpost forwards to :9090 after login; Grafana
dashboards still render (ClusterIP datasource intact).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 18:41:28 +02:00
k.rennerandClaude Opus 4.8 6ec3725371 Gate Alertmanager behind Authentik
Alertmanager (alerts.renner.uno, monitoring ns, :9093) had no auth of its
own, and its v2 API is writable: an unauthenticated LAN client could POST
/api/v2/silences and suppress ALL alerting — a tamper capability, not just
disclosure (2026-07-10 audit, highest-teeth of the deferred unauth
findings).

Gate it with the same Authentik proxy-outpost pattern as
paperless/vscode/shelly-manager:

- New proxy provider + application + dedicated outpost
  (ak-outpost-alertmanager-gate), mode proxy, forwarding to the ClusterIP
  Service (kube-prometheus-stack-alertmanager.monitoring.svc:9093).
- New git-owned "Monitoring Access" group (k.renner) bound to the app.
  Named generically so it can also gate the still-deferred Prometheus UI.
- The Helm-chart-generated Alertmanager Ingress is disabled
  (alertmanager.ingress.enabled=false); a standalone gate Ingress
  (alertmanager-gate-ingress.yaml) backends the outpost ExternalName —
  two Ingresses for the same host would conflict. Mirrors loki-ingress.
- Scoped CNPs: monitoring ingress rule allowing only the outpost -> :9093;
  matching egress rule in the authentik namespace.

implicit-consent flow, NOT passkey step-up: Alertmanager is a
frequently-checked monitoring UI, and requiring an authenticated Authentik
session already closes the unauthenticated-LAN exposure. Passkey re-auth on
every visit is disproportionate friction for a dashboard (paperless-gate
uses implicit-consent for the same reason). Bump to
passkey-stepup-authorization if the write API warrants it.

No skip_path_regex: the Alertmanager UI is a plain Elm SPA with no service
worker and no manifest (verified 2026-07-10 — index.html has no
serviceWorker/manifest/webmanifest), so none of the credentials:'omit' PWA
fetches that forced skip carve-outs elsewhere.

Prometheus's alerting path is UNAFFECTED: it posts to the ClusterIP Service
(verified: spec.alerting.alertmanagers targets
monitoring/kube-prometheus-stack-alertmanager:http-web), not this ingress.
Prometheus's own ingress is untouched (still enabled) — its unauth exposure
is a separate deferred finding, and it is read-only.

Verified: kubectl kustomize builds for authentik, monitoring and
network-policies; the embedded outpost survived the blueprint insert; the
rendered ingress backends ak-outpost-alertmanager-gate:9000, egress is
scoped to alertmanager:9093, and alertmanager.ingress.enabled=false while
prometheus.ingress.enabled=true.

Post-merge checks to run: only the gate Ingress exists for alerts.renner.uno
(the Helm one is gone); unauthenticated GET -> 302 to auth.renner.uno;
POST /api/v2/silences unauthenticated is refused; the outpost forwards to
:9093 after login; Prometheus is still delivering alerts to Alertmanager.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 17:27:45 +02:00
k.rennerandClaude Opus 4.8 2efd54ef82 Gate shelly-manager behind Authentik passkey step-up
shelly-manager has no authentication of its own. Anything that can reach
:8080 can rewrite Shelly device config, push scripts, and read the stored
per-device AdminPassword values. Until now the only control on it was
network reachability of shelly-manager.home.renner.uno.

Put it behind Authentik using the same proxy-outpost pattern as the
vscode gate (760e187) and paperless before it:

- New proxy provider + application + dedicated outpost
  (ak-outpost-shelly-manager-gate) in mode: proxy. authorization_flow is
  passkey-stepup-authorization, the same flow vscode-gate uses, so every
  authorize requires a passkey.
- New git-owned "Shelly Managers" group (membership in the blueprint,
  k.renner only) bound to the application as the explicit allowlist.
- shelly-manager Ingress now backends the gate outpost via an ExternalName
  mirror instead of the app Service directly.
- home-assistant CNP gains a scoped ingress rule: only the
  shelly-manager-gate outpost pod may reach :8080. Matching scoped egress
  CNP added in the authentik namespace.

No skip_path_regex, deliberately. The music-assistant gate died on PWA
assets fetched with credentials:'omit' (manifest, service worker), and
paperless/vscode carve those out. shelly-manager ships neither -- wwwroot
is app.css, favicon.png and lib/ only -- so every asset is a same-origin
credentialed fetch. It is a Blazor Web app whose SignalR circuit at
/_blazor upgrades to a websocket carrying the session cookie, which the
proxy outpost forwards.

This also restores the app. It has been returning 504 through Traefik
since 760e187 merged at 01:20 today: `default-deny-ingress` has
endpointSelector {}, so its traefik -> :8080 grant, written for the
code-server sidecar, was namespace-wide and was in fact the only rule
letting Traefik reach shelly-manager, which also listens on 8080. Removing
that grant to gate vscode took shelly-manager's ingress down as collateral.
Gating it properly is the correct fix rather than re-widening the rule --
the app should not have been reachable unauthenticated in the first place.

Verified: kubectl kustomize builds clean for authentik, home-assistant and
network-policies; the rendered Ingress backends ak-outpost-shelly-manager-
gate:9000, the egress CNP is scoped to shelly-manager:8080, and the two
gate ingress rules cannot reach each other's target.

The Authentik proxy flow, the passkey login, and the SignalR circuit
through the outpost all require a browser test after apply.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 13:42:51 +02:00
k.rennerandClaude Opus 4.8 760e187ba1 feat(home-assistant): gate vscode/code-server behind Authentik passkey step-up
code-server (the HA /config editor sidecar) runs with --auth=none, as
root, mounting the entire /config incl. plaintext secrets.yaml, .storage,
and the git PAT in the PVC remote URL. Until now the only control on it
was network reachability of vscode.home.renner.uno — any LAN client could
read every HA secret and edit config.

Put it behind Authentik using the existing paperless-gate proxy-outpost
pattern:

- New Authentik proxy provider + application + dedicated outpost
  (ak-outpost-vscode-gate) in mode: proxy. authorization_flow is the
  passkey-stepup-authorization flow (same one Homeassistant/Immich/
  Audiobookshelf/Netbird use), so every authorize requires a passkey.
- New "Home Assistant Config Editors" group (git-owned membership,
  k.renner only) bound to the app — explicit allowlist for who can read
  HA secrets via the editor.
- vscode Ingress now backends the gate outpost (ExternalName mirror)
  instead of code-server directly.
- home-assistant CNP tightened: traefik no longer allowed to :8080; only
  the vscode-gate outpost pod may reach code-server. Matching scoped
  egress CNP added in the authentik namespace.
- code-server is a service-worker app; skip_path_regex carves out its
  static/manifest/SW tree to avoid the credentials:'omit' redirect loop
  that forced retirement of the music-assistant gate. Exact skip set to
  be confirmed post-deploy (see PR checklist).

Verified: kubectl kustomize builds clean for home-assistant, authentik,
and network-policies. The Authentik proxy flow + code-server PWA
behaviour require a browser passkey login test after apply.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 01:17:46 +02:00
k.rennerandClaude Fable 5 eac9a6cd6f authentik: Passkey-only Login auch für Gitea und Grafana
Beide Provider (nur k.renner via Infrastructure Admins, 4 Passkeys
enrolled) an den Passwordless- und Step-up-Flow gebunden; Live-Stand
der Pflichtfelder gespiegelt. PVE, PBS und paperless-gate bleiben
bewusst auf Passwort+MFA.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 10:32:05 +02:00
k.rennerandClaude Fable 5 5ba5c3108a authentik: Passkey-only Login auch für Audiobookshelf und Immich
Bindet die OAuth2-Provider Audiobookshelf und immich-oauth-provider
an den Passwordless-Authentication-Flow (Passkey-Autofill inklusive)
und den Step-up-Authorization-Flow — gleiche Mechanik wie Home
Assistant und NetBird (#115/#118). Pflichtfelder (invalidation_flow,
client_type, redirect_uris) spiegeln den Live-Stand vom 2026-07-07.

Nutzerabdeckung geprüft: Audiobookshelf Users (k.renner, s.renner)
haben Passkeys; Immich Users enthält g.renner ohne Passkey — sie
bekommt beim ersten Zugriff das Inline-Enrollment über die Step-up-
Stage (configure), kein Lockout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 10:26:36 +02:00
k.rennerandClaude Fable 5 fb6ac12cbe authentik: Passkey-Autofill im Passwordless-Flow (kein Username-Tippen)
Setzt webauthn_stage auf der Identification-Stage des Passwordless-
Flows (Conditional WebAuthn, authentik >= 2025.12): der Browser bietet
enrollte Passkeys direkt im Autofill des Username-Felds an — ein Tap,
kein Username. Username-Tippen bleibt als Fallback für nicht-
discoverable Credentials erhalten.

last_auth_threshold der Passwordless-Validation-Stage von seconds=0
auf minutes=5: verhindert Doppel-Challenge derselben Flow-Ausführung
nach Autofill-Validierung; der Skip ist session-gebunden.

Validation-Stage-Entry vor den Identification-Entry gezogen, weil
!KeyOf nur rückwärts auflöst.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 10:01:37 +02:00
k.rennerandClaude Fable 5 e9e5d8a82c authentik: Passwordless-Passkey-Login für Home Assistant und NetBird
Ergänzt den Step-up um den eigentlichen Kern: einen passwortlosen
Authentication-Flow (Identification ohne Password-Stage → WebAuthn →
Login), per authentication_flow an beide Provider gebunden. Frische
Logins in HA/NetBird zeigen damit kein Passwortfeld mehr; der
Authentik-eigene Login (Default-Brand-Flow) bleibt unverändert.

Sicherheitsdetail: die Validation-Stage im Authentication-Flow ist
eine SEPARATE Stage mit not_configured_action: deny — `configure`
wäre hier eine Account-Übernahme (User ist nur identifiziert, nicht
authentifiziert). Inline-Enrollment bleibt exklusiv im Authorization-
Step-up mit require_authenticated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 09:08:54 +02:00
k.rennerandClaude Fable 5 3ff1b472d7 authentik: Passkey-Step-up-Flow für Home Assistant und NetBird
Neuer Authorization-Flow `passkey-stepup-authorization` mit einer
WebAuthn-only Authenticator-Validation-Stage, per Blueprint auf die
OAuth2-Provider von Home Assistant und NetBird gebunden. Login in
diese Apps erfordert damit einen Passkey — auch mit bestehender
SSO-Session aus einem Passwort-Login.

- not_configured_action: configure — g.renner (HA-Zugriff, kein
  Passkey enrolled) wird beim ersten Login zur Enrollment geführt
  statt ausgesperrt
- last_auth_threshold hours=8 — kein Doppel-Touch direkt nach
  Passkey-Login; NetBirds /silent-auth (prompt=none) bleibt innerhalb
  des Fensters funktionsfähig
- Provider-Pflichtfelder (invalidation_flow, redirect_uris,
  client_type) spiegeln den Live-Stand vom 2026-07-07; ab jetzt
  besitzt Git diese Felder

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 08:54:30 +02:00
k.rennerandClaude Opus 4.8 0c889bc36b authentik: move auth.renner.uno public edge off Cloudflare tunnel to NetBird proxy
Drop the external-dns/cloudflare label from the Authentik ingress (helm
values) and remove the auth.renner.uno cloudflared route. auth must be
served with NO proxy auth (it is the IdP every other SSO redirects to).
This is the last of the 8 public names; cloudflared now routes nothing
but a 404 catch-all.

MERGE ONLY AFTER the auth proxy service + DNS are live, then roll
cloudflared, or all SSO logins break in the gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 12:06:02 +02:00
k.rennerandClaude Opus 4.8 8a26c2a33c authentik: roll server + ldap-outpost to 2026.5.3, pin chart to match
Bumps the samba-bridge fork images 2026.2.3 -> 2026.5.3 (built from
authentik-samba-bridge#f02b945 / PR #1) and pins the Helm chart to
2026.5.3 so chart appVersion and the running app image are in lockstep.

- global.image.tag:           2026.2.3-b06668e -> 2026.5.3-f02b945
- ldap-outpost container_image: 2026.2.3-0140972 -> 2026.5.3-f02b945
- chart version: ">=2025.12.0" (floated to 2026.5.3) -> pinned "2026.5.3"

Both images verified present in the gitea registry. The fork bump
validated the samba patch and server build assumptions (TENANT_APPS
sed, ManagedAppConfig, python3.14 venv, ldap runtime contract) against
upstream version/2026.5.3 before building.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 11:02:32 +02:00
k.rennerandClaude Opus 4.7 d187799490 authentik: bump memory limits to 2Gi, lift requests
All three authentik components were sitting at 70-80% of their 1Gi
memory limit at steady state, with no headroom for spikes
(LDAP sync, blueprint apply, pg checkpoint/vacuum, gunicorn worker
restart). Today's outage during the kubelet config rollout on k8s-2
saw ak-pg-1 (the primary at the time) shut down on a readiness-probe
failure and not come back cleanly; suspected contributing cause is
that brief memory pressure tipped it past the 1Gi limit.

- ak-pg cluster: 512Mi req / 1Gi limit -> 768Mi req / 2Gi limit
  (steady-state usage on primary is ~580MiB; vacuums and checkpoint
  spikes were eating into the cap with no margin)
- authentik-server: 768Mi req / 1Gi limit -> 1Gi req / 2Gi limit
  (live pods sitting at 720-745MiB, ~74% of cap)
- authentik-worker: 768Mi req / 1Gi limit -> 1Gi req / 2Gi limit
  (workers run heavy background tasks - LDAP sync, policy eval,
  blueprint application - and need headroom)

Limit bump is the safety net (no OOM). Request bump matches steady
state so scheduler accounts for real footprint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-13 10:10:30 +02:00
k.rennerandClaude Opus 4.7 f52b69152a authentik: clean up duplicate unifi PolicyBindings
Blueprint identifiers for PolicyBindings include `order`, so changing
the order value between commits created orphan bindings instead of
updating in-place. UI showed four bindings on the unifi Application:
two Admins (order 0 + 1) and two Users (order 0 + 10). Add explicit
state:absent entries for the order-0 Admins and order-10 Users
orphans; the keeper bindings (Users/0, Admins/1) are unchanged.
Entries left in the blueprint permanently — state:absent is
idempotent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:54:00 +02:00
k.rennerandClaude Opus 4.7 98e5608c36 authentik: admit Unifi Users (not only Unifi Admins) on UDM LDAP
Existing Synology Application admits both `Synology Users` and
`Synology Admins`; I only added the admins binding for Unifi which
left the pre-migration `Unifi Users` group (g/j/s.renner, t.geiger,
…) unable to bind via the unifi Application policy. Mirror the
Synology pattern with two PolicyBindings — Users at order 0, Admins
at order 1. The Unifi Users group is declared `state: present`
identifier-only so the M2M membership is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:48:42 +02:00
k.rennerandClaude Opus 4.7 ecdba251c5 authentik: pin shared LE cert on dormant LDAP Jellyfin too
LDAP outpost dispatch in ldap_tls.go falls back to the outpost's
self-signed default whenever any attached provider has cert=nil — so
attaching the LE cert to Unifi and Synology alone wasn't enough,
because the all-identical-certs branch keeps failing on Jellyfin's
empty cert. Set Jellyfin's certificate + tls_server_name to the same
shared keypair; provider stays otherwise dormant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 21:48:47 +02:00
k.rennerandClaude Opus 4.7 47163f204c authentik: wire LDAP Unifi on sibling DN + LDAPS via cert-manager
Activates the dormant `LDAP Unifi` provider for UniFi Network admin
auth: rebases onto sibling `DC=unifi,DC=renner,DC=uno` to dodge the
existing Jellyfin/Unifi base-DN dispatch collision (same fix Synology
got in 90906e2), adds Unifi Admins group / unifi Application /
PolicyBinding. Exposes LDAPS on 636 using an LE cert from
cert-manager (DNS-01 via letsencrypt-cluster), imported into Authentik
as a CertificateKeyPair from env vars on the server + worker pods.
Cert is also attached to LDAP Synology so DSM's eventual 389→636
cutover only needs a DSM-side reconfigure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 21:27:02 +02:00
k.renner af9c58a890 authentik: bump bridge-server tag — synthesise uid=<username>
ci/woodpecker/cron/e2e Pipeline failed
ci/woodpecker/cron/security-scan Pipeline was successful
DSM's smbd hardcodes the user lookup filter to
(&(uid=%u)(objectClass=sambaSamAccount)). Authentik's LDAP outpost
emits uid=<random User.uid hash> by default, so that filter never
matched and SMB authentication was failing at user lookup —
before NTLM verification even ran.

bridge commit b06668e: synthesise "uid": user.username into
user.attributes, so the outpost's AttributesToLDAP path overrides
the EnsureAttributes default. The override is gated by
SAMBA_BRIDGE_ENABLED_GROUP ("Synology Users"); users outside that
group still emit upstream's uid=<hash>.

Verified live before this rollout: smbclient -L //nas-1.renner.uno
-U k.renner authenticates and lists shares including the personal
"home" share, after directly injecting uid=k.renner via SQL into
the same JSONField the bridge writes. Bumping the bridge image so
the next password-change cycle (and the migration backfill for
existing users) writes this value automatically.

The ldap-outpost image tag is unchanged (sambaSamAccount +
sambaDomain GetUserOCs() patches already in 0140972; this commit
only updates Python in the server image).
2026-05-22 13:42:17 +02:00
k.rennerandClaude Opus 4.7 b9854efb49 authentik: bump patched LDAP outpost + add sambaDomain marker
DSM smbd live diagnostic on /var/log/samba/log.smbd revealed a
second LDAP gate after the sambaSamAccount one: every SMB session
attempt fires (&(sambaDomainName=synology)(objectClass=sambaDomain))
before user auth. Without a matching entry, smbd's
smbldap_search_domain_info tries to ADD one (outpost is read-only)
and bails with NT_STATUS_UNSUCCESSFUL, blocking SMB auth entirely.

Two changes:

- Patched outpost bumped to 2026.2.3-0140972, which extends
  GetUserOCs() to recognise sambaDomain (in addition to
  sambaSamAccount). Without this the outpost's search dispatcher
  short-circuits sambaDomain queries at the gate before consulting
  any user entry.

- New service-account user "samba-domain-synology" with
  user.attributes carrying objectClass=sambaDomain and the required
  sambaDomainName / sambaSID / sambaNextRid fields. The outpost
  emits this entry on DSM's sambaDomain query; DSM accepts it as
  the domain-info answer and proceeds with auth.

sambaSID on the marker MUST match SAMBA_BRIDGE_DOMAIN_SID and the
SID DSM has been pinned at, or DSM rejects the domain. All three
share S-1-5-21-700609483-19441523-1711414274 .

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:26:22 +02:00
k.renner c8035fcfc0 authentik: bump samba-bridge-server tag to 2026.2.3-09b29dd
09b29dd carries default=True on SambaBridgeConfig so Django actually
picks our AppConfig subclass instead of falling back to the vanilla
django.apps.AppConfig (which has no tenant_schedule_specs property
and caused the scheduler exception-loop in the previous deploy).
2026-05-22 11:55:38 +02:00
k.rennerandClaude Opus 4.7 723d30dbea authentik: pin samba-bridge-server tag to 2026.2.3-279da54
Floating :latest doesn't trigger re-pull on Kubernetes reschedule —
need an explicit tag bump to roll the deployment. 279da54 carries the
ManagedAppConfig fix that resolves the tenant_schedule_specs
AttributeError seen in the 62f9540 rollout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 11:46:48 +02:00
k.rennerandClaude Opus 4.7 ac011223ec authentik: wire samba-bridge for DSM SMB authentication
Deploys the authentik-samba-bridge stack:
gitea.renner.uno/k.renner/authentik-samba-bridge

Three pieces:

1. Server image override (helmrelease.yaml). global.image points at
   the bridge-server image (Authentik server + our Django app that
   subscribes to password_changed and writes Samba schema attrs to
   user.attributes). global.envFrom adds SAMBA_BRIDGE_DOMAIN_SID,
   SAMBA_BRIDGE_ENABLED_GROUP, SAMBA_BRIDGE_UID_OFFSET from a new
   SOPS secret. Both server AND worker pods inherit the bridge —
   the signal receiver registers in any Authentik server process
   that calls User.set_password().

2. LDAP outpost image override (blueprints-configmap.yaml).
   container_image on the LDAP Outpost record swaps the upstream
   outpost for our patched build that adds "sambaSamAccount" to
   GetUserOCs(). Without this, DSM's smbd filter
   (&(uid=...)(objectClass=sambaSamAccount)) short-circuits to zero
   entries inside the upstream outpost regardless of what's in
   user.attributes.

3. samba-bridge-secret.sops.yaml — the bridge's env contract. The
   domain SID was generated once from /dev/urandom and is now
   frozen forever (must match the SID typed into DSM's "Configure
   Samba Authentication" wizard).

Post-deploy: existing Synology Users members need to change their
password once via auth.renner.uno for the bridge to capture the
plaintext and synthesise the NT hash. Argon2 hashes already on
file can't be reverse-engineered into NT hashes; password reset
is the only path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 11:37:42 +02:00
k.rennerandClaude Opus 4.7 90906e23b6 authentik: move LDAP Synology base DN to sibling subtree
DC=synology,DC=authentik,DC=renner,DC=uno is a child of the existing
Unifi/Jellyfin base; the outpost's HasSuffixNoCase dispatch matches
on first iteration without longest-match preference, so binds still
landed on Jellyfin's policy. Moving Synology to DC=synology,DC=renner,
DC=uno makes it a sibling subtree the others' suffix check can't claim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 20:37:45 +02:00
k.rennerandClaude Opus 4.7 e563cbfecd authentik: scope LDAP Synology under its own base DN
Live binds from nas-1 land in the Jellyfin Application's policy
evaluation because the outpost dispatches by base_dn suffix match —
with LDAP Unifi/Jellyfin/Synology all sharing DC=authentik,DC=renner,
DC=uno, the alphabetically-first provider wins every request. Moving
Synology under DC=synology,DC=authentik,DC=renner,DC=uno gives the
outpost a unique suffix to route on. Unifi/Jellyfin are left alone;
neither is LAN-reachable yet so the duplicate-base-DN there is dormant
config, not in this change's scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 20:26:54 +02:00
k.rennerandClaude Opus 4.7 3ededebfdd authentik: declare LDAP Synology provider + expose outpost on ldap.renner.uno
Blueprint-manages a new LDAP integration for nas-1 DSM: dedicated
LDAPProvider, Application, Synology Users/Admins groups, and PolicyBindings.
Standalone LoadBalancer Service on the BGP pool (pinned 10.2.99.88/fd99::88,
LAN hostname ldap.renner.uno via external-dns-unifi) sidesteps the operator-
owned ClusterIP; CNP scopes :3389 ingress to the mgmt LAN so the cleartext
bind stays on a trusted segment until LDAPS is wired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 20:09:04 +02:00
k.rennerandClaude Opus 4.7 fee6a58a47 authentik: scrape radius outpost via standalone Service + ServiceMonitor
The radius binary listens on :9300 like ldap/proxy do — verified by
port-forward, returns authentik_outpost_* metrics. But authentik's
Python operator at providers/radius/controllers/kubernetes.py omits
the DeploymentPort(9300, "http-metrics", "tcp", 9300) entry from its
`deployment_ports` list. ldap's controller has it, radius does not.
Net effect: the operator-managed Service exposes only UDP 1812 and
the operator-created ServiceMonitor points at a port that does not
exist on the Service it selects.

Add a sibling headless Service that selects the radius pod labels
and exposes 9300/TCP, plus a ServiceMonitor with the release label
so Prometheus scrapes it. Zero pod changes — the listener is
already there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 07:43:39 +02:00
k.rennerandClaude Opus 4.7 a956529a2d authentik: standalone ServiceMonitors for outposts (ldap + paperless-gate)
The goauthentik.io operator auto-creates ServiceMonitors per outpost
but does not stamp `release: kube-prometheus-stack`, so this cluster's
serviceMonitorSelector silently ignores them. Declare standalone SMs
with the correct label, following the Velero precedent.

Radius is omitted on purpose: its container exposes only UDP 1812,
no /metrics endpoint. The operator-created SM for radius is dead
regardless of label.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 07:30:28 +02:00
k.rennerandClaude Opus 4.7 718840dc20 authentik: bump server CPU req to 200m, expose /metrics + ServiceMonitor
Server pods read 75m sustained CPU on a 100m request, tripping
KubeHpaMaxedOut without the cluster being under real load. The
asymmetry between pods correlates with outpost HTTP/2 connection
pinning + a brand_tls thundering herd in the Go router, but the
log-visible work only accounts for ~30m of the 75m. Authentik's
native /metrics endpoint (port 9300) was never enabled and the
chart's built-in ServiceMonitor was off, so per-handler and queue
metrics were invisible. Enable both and add the release label so
kube-prometheus-stack's serviceMonitorSelector picks it up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 07:14:41 +02:00
k.rennerandClaude Opus 4.7 121d7b9242 ingress: cut auth.renner.uno (authentik-server + paperless-gate-outpost) to traefik
Both Ingresses on the host flip together (path-based: `/` to
authentik-server, `/outpost.goauthentik.io` to the paperless-gate
outpost — Traefik picks the more specific match the same way nginx
did). authentik chart's nginx proxy-buffer-size/number drop because
Traefik streams.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:43:31 +02:00
k.rennerandClaude Opus 4.7 865fff79b4 authentik(paperless-gate): skip auth on /static/* assets
ci/woodpecker/cron/e2e Pipeline failed
ci/woodpecker/cron/security-scan Pipeline was successful
paperless ships <link rel="manifest"> in its HTML; browsers fetch the
manifest with credentials:'omit' per W3C spec, so the gate redirected
every page load to auth.renner.uno and burned an orphan session JWT.
The wider /static tree carries no auth value either — Django STATIC is
public by design; MEDIA stays gated. Pass through anonymously.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 13:22:23 +02:00
k.rennerandClaude Opus 4.7 03723f491e authentik: bump worker CPU request 100m → 200m
ci/woodpecker/cron/security-scan Pipeline was successful
ci/woodpecker/cron/e2e Pipeline failed
Worker has been pinned at maxReplicas=4 for 45+ min despite steady-
state per-pod CPU of ~47m (47% of 100m request). Root cause: the
5-min scaleDown stabilization window picks the highest recommendation
across the window, so brief peaks above 70% during warmup phases
keep the HPA from ever scaling back down.

200m request puts the current steady-state at ~24% utilization,
well below the 70% target — HPA can scale back to minReplicas
between bursts, while still scaling up reactively when sustained
load exceeds 140m per pod.

Server unchanged: it's been steady at 24%/70% with 100m, no peaks
near target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:03:46 +02:00
k.rennerandClaude Opus 4.7 4111cb6d03 authentik: bump memory requests and limits
Server pods OOMKilled twice (exitCode 137) at the chart-suggested
512Mi/512Mi sizing — gunicorn router + worker processes plus blueprint
loading climb to ~485Mi steady-state on a 2-replica deployment, with
no headroom for bursts. Workers were sitting at ~430Mi, well within
the danger zone for the next spike.

Bumped both: requests 768Mi, limits 1Gi. The chart's commented-out
suggestions are too low for a real multi-replica setup; documented
for the wiki.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:20:32 +02:00
k.rennerandClaude Opus 4.7 a69042e3a8 authentik: enable chart-native HPA on server and worker
The upstream Helm chart ships built-in HPA support for both
deployments; using it instead of a standalone manifest keeps the
config in one place and tracks chart upgrades.

Both: min=2, max=4, target 70% CPU. Resources set to the chart's
documented suggested values (100m / 512Mi). Worker is queue-driven
(Celery + Redis) so replicas distribute work naturally; server is
HTTP-served via ingress-nginx, which maintains many concurrent
upstream connections, so distribution across pods works (this is
different from the immich-server → immich-ml case where a single
persistent connection pinned all traffic to one pod).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:58:56 +02:00
k.rennerandClaude Opus 4.7 9406cb535f music-assistant: drop Authentik gate
The gate fundamentally conflicted with MA's PWA model: Chrome fetches
the manifest, service worker, and icons with credentials omitted
(no crossorigin=use-credentials on MA's <link rel="manifest">), and
once installed the cached service worker bypasses fresh navigations
on cold-start so the Authentik cookie never lands in the WebAPK's
cookie jar. MA delegates auth to Home Assistant in-app; the gate's
marginal defense-in-depth wasn't worth the recurring breakage.

Ingress reverts to pointing at music-assistant directly. Cloudflared
keeps music.renner.uno wired to the tunnel. Blueprint marks the
provider/application/outpost state: absent so the worker tears down
ak-outpost-music-assistant-gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:31:48 +02:00
k.rennerandClaude Opus 4.7 8603f0c982 music-assistant: skip PWA-install assets in gate
Chrome fetches manifest.webmanifest, sw.js, favicon, and the icons it
references with credentials: 'omit' (no crossorigin=use-credentials
on MA's <link rel="manifest">), so the user's Authentik cookie never
reaches the gate on those requests. Without this skip, the gate 302s
the anonymous fetch and Chrome's installability check fails silently
- the install prompt stops being offered.

Skipped paths are static branding/manifest only; HTML, JS bundle,
/api, and /ws remain gated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:21:27 +02:00
k.rennerandClaude Opus 4.7 bd3580b678 music-assistant: gate music.renner.uno on WAN via authentik
Dedicated paperless-gate-shaped outpost (music-assistant-gate) fronts
MA at the WAN edge. MA's own HA-delegated login inside the UI remains
the primary access decision; the gate adds defense in depth.

No egress CNP needed: MA is hostNetwork and the authentik baseline
already allows toEntities: [host, remote-node].

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:45:23 +02:00
k.renner a00f1b2af4 external-dns: switch cloudflare instance from annotationFilter to labelFilter
The annotation filter caused records on annotated ingresses to flap
once the UniFi external-dns instance also managed them: external-dns
turns annotations in its prefix namespace into ProviderSpecific on the
desired endpoint, the kashalls UniFi webhook does not implement
AdjustEndpoints to strip Cloudflare-only properties, and Records()
returns observed endpoints with empty ProviderSpecific — so plan
computed an Update every reconcile and the records cycled in UDM.

Labels are not translated into ProviderSpecific, so swapping the
selector from `external-dns.alpha.kubernetes.io/cloudflare-proxied`
to a plain `external-dns/cloudflare=true` label removes the round
trip. The Cloudflare instance still proxies all matched records
because of the existing `--cloudflare-proxied` extraArg default.

Touches the seven resources that use the selector: the cloudflare
external-dns HelmRelease and the six ingresses currently published
through the cargo tunnel (audiobookshelf, gatus, home-assistant,
immich, paperless, authentik-server).
2026-05-10 18:53:33 +02:00
k.rennerandClaude Opus 4.7 3a14aab4b3 paperless: convert trust-boundary policy to CiliumNetworkPolicy + add gate egress
The previous standard NetworkPolicy in paperless allowed ingress from
the gate outpost on paper, but the gate outpost still timed out at
134s when forwarding to paperless. Two reasons:

1. The cluster baseline CiliumNetworkPolicy in the paperless namespace
   (`default-deny-ingress`, sets ingress allow-list paperless / ingress-
   nginx / database / monitoring + host/remote-node) doesn't include
   authentik. Standard NetworkPolicy allow rules don't compose
   reliably with Cilium baseline CNPs for cross-namespace ingress —
   directly verified: an in-namespace curl from ingress-nginx to
   paperless succeeds (302 in 3 ms, allowed by the baseline CNP),
   while the same curl from the gate outpost times out (no policy
   path admits it).

2. The cluster baseline CNP in the authentik namespace
   (`default-deny-ingress`, egress to authentik / rook-ceph / kube-dns
   / world) doesn't include paperless either, so even on the source
   side the connection is blocked.

Replace the standard NP with two CiliumNetworkPolicies:

- `paperless/paperless-gate-ingress` (paperless namespace): selects
  the paperless app pod and allows ingress from endpoints labeled
  `goauthentik.io/outpost-name=paperless-gate` in the authentik
  namespace. CNPs union additively with the baseline CNP so the gate
  outpost is added to the allow set without broadening it.
- `authentik/paperless-gate-egress` (authentik namespace): selects
  pods labeled `goauthentik.io/outpost-name=paperless-gate` and
  allows egress to the paperless app pod specifically. Scoped to the
  gate outpost's label, not the whole authentik namespace, so other
  authentik workloads can't reach paperless.

Egress for paperless itself is also expressed as a CNP
(`paperless/paperless-egress`): in-namespace, kube-dns, and external
HTTPS only. Public-internet HTTPS uses Cilium's `world` entity which
already excludes cluster-internal IPs.

Together: the gate outpost is the only ingress path to paperless; the
gate outpost is the only thing in the authentik namespace that's even
permitted to talk to paperless; paperless's egress is restricted to
its known dependencies. Trust boundary lives on the gate outpost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 09:38:55 +02:00
k.rennerandClaude Opus 4.7 56176db68b paperless: move trust boundary onto paperless-gate (proxy mode + tighter NP)
Switch the gate ProxyProvider from forward_domain to proxy mode so the
outpost itself terminates and forwards every request:
  ingress-nginx → ak-outpost-paperless-gate → paperless

In forward_domain, ingress-nginx held the trust boundary — it was what
forwarded X-Authentik-Username to paperless after auth_request, so a
compromised ingress-nginx could inject any username. In proxy mode the
outpost is the proxy: ingress-nginx routes the request to the
outpost's :9000 listener, the outpost validates the renner.uno-scoped
session cookie, and the outpost issues the upstream request to
paperless with the gate-attested headers. nginx never sees paperless's
listener.

NetworkPolicy `paperless-trust-boundary` (replaces the previous
nginx-only policy):
- Ingress: ONLY from pods labeled goauthentik.io/outpost-name=
  paperless-gate in the authentik namespace, on TCP/8000. ingress-
  nginx pods are now blocked from reaching paperless directly, so
  even with a valid cookie they cannot bypass the outpost.
- Egress (newly tightened):
    - kube-dns on UDP/TCP 53.
    - In-namespace traffic to postgres / redis / tika / gotenberg /
      protonmail-bridge on their respective ports.
    - HTTPS (TCP 443) to public IP space, with RFC1918 / ULA
      excluded so an attacker on a compromised paperless can't pivot
      to internal-only services over 443.

Ingress rewiring:
- paperless.renner.uno → ak-outpost-paperless-gate ExternalName
  (mirror in the paperless namespace), TLS via cert-manager.
- The other three hostnames (paperless.k.renner.uno, docs.renner.uno,
  docs.k.renner.uno) move to a `paperless-aliases` Ingress with
  permanent-redirect to https://paperless.renner.uno$request_uri.
  Authentik proxy mode matches the request Host against a single
  external_host per provider; collapsing to one canonical URL is
  cleaner than maintaining four parallel ProxyProviders.
- Drop the four nginx forward-auth annotations (auth-url,
  auth-signin, auth-response-headers, auth-snippet) and the
  defense-in-depth header strip — none of that is in this path
  anymore.

The auth.renner.uno/outpost.goauthentik.io ingress in the authentik
namespace stays — the outpost still serves /start and /callback there
during the OAuth dance (the redirect_uri is auto-derived to
paperless.renner.uno/outpost.goauthentik.io/callback now, so the
callback lands at paperless.renner.uno via the proxy ingress, but the
sign-in entry kept on the auth domain doesn't break anything and is
still useful if another forward-auth gate gets added later).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 09:07:42 +02:00
k.rennerandClaude Opus 4.7 e086c8a90a authentik: add type + providers to existing outpost entries
Authentik outpost serializer now requires both fields (validated
against /authentik/outposts/api/serializers.py + the live worker's
Importer.validate()). The Caddy/LDAP/Local Radius entries were missing
them — the blueprint apply errored out at the Caddy entry, which
blocked every other state change in the apply (including the OIDC
deletions in the previous commit).

- `LDAP Outpost` (ldap) and `Local Radius` (radius) get the right type
  + provider lists mirroring live state, so blueprint reconcile
  doesn't detach UI-managed providers.
- The `Caddy` outpost entry is dropped — its only provider (Caddy
  docs.renner.uno) was retired earlier in this session and the outpost
  is no longer in the cluster.
- Embedded outpost gets an explicit `type: proxy` for safety.

Validated against the live worker via Importer.validate() with
capture_logs disabled — returns True.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 08:42:47 +02:00
k.rennerandClaude Opus 4.7 c22add3c2b paperless: collapse to gate-only auth (drop the OIDC layer)
The dual-layer setup (gate ProxyProvider + paperless OIDC OAuth2Provider
on a separate Application) was structurally fine but added a second
consent screen on first login for no real defense-in-depth gain. With
both layers gated by the same Paperless Users group, the second consent
just confirms what the gate already validated.

Refactor to gate-only:
- Authentik: state: absent for the OAuth2Provider Paperless, the
  separate `paperless` Application that pinned it as primary, and the
  early-session orphan `paperless` ProxyProvider. The `paperless-gate`
  Application + ProxyProvider + Outpost stay; one Application now
  covers paperless end-to-end.
- Paperless config: drop PAPERLESS_APPS=allauth.socialaccount... and
  the PAPERLESS_SOCIALACCOUNT_PROVIDERS env binding, add
  PAPERLESS_ENABLE_HTTP_REMOTE_USER=true and point
  PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME at HTTP_X_AUTHENTIK_USERNAME
  so paperless reads the username straight from the gate's
  X-authentik-username header.
- sops secret: drop the oidc-providers-json field; keep only the
  paperless secret-key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 08:34:08 +02:00
k.rennerandClaude Opus 4.7 758f2c16ac authentik(paperless OIDC): add the default openid/profile/email scope mappings
ci/woodpecker/cron/update-all Pipeline failed
ci/woodpecker/cron/e2e Pipeline failed
Plain OAuth2Provider entries don't get scope mappings auto-attached
the way ProxyProvider.set_oauth_defaults() does. Without them the
access token issued during paperless's allauth callback has no scope
values, /application/o/userinfo/ returns 403, and paperless renders
"An error occurred while attempting to login via your social network
account" — which is what the user just hit. Bind the three default
managed ScopeMappings (openid / profile / email).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:47:05 +02:00
k.rennerandClaude Opus 4.7 00d0c19f9c authentik: route auth.renner.uno/outpost.goauthentik.io to the gate outpost
Each Authentik proxy outpost has its own session store. Sharing the
provider between the embedded outpost and the dedicated gate outpost
means the OAuth callback at /outpost.goauthentik.io/callback hits the
embedded outpost (which has no in-cluster /auth/nginx exposure), the
session is created in the embedded outpost's store, and the dedicated
gate outpost — which is what nginx auth_request actually queries —
never sees it. Result: an infinite redirect between /authorize and
/callback after every OAuth round-trip, surfaced as
ERR_TOO_MANY_REDIRECTS in the browser.

Add an Ingress in the authentik namespace that routes
auth.renner.uno/outpost.goauthentik.io/* to the dedicated outpost's
ClusterIP service, and detach the provider from the embedded outpost.
The dedicated outpost now serves /start, /callback, AND /auth/nginx
in the same process. The Authentik UI flow paths (/if, /application/o,
/api, /static, /flows, /media) continue to resolve via the chart's
authentik-server Ingress at the broader `/` rule, so 2FA still happens
on auth.renner.uno where credentials are bound.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 22:36:36 +02:00
k.rennerandClaude Opus 4.7 8f229a007c authentik(paperless-gate): set external_host to auth.renner.uno for callback routing
ProxyProvider.set_oauth_defaults() derives redirect_uris from
external_host via _get_callback_url. Pointing the gate provider's
external_host at https://auth.renner.uno makes the OAuth callback URL
land on auth.renner.uno/outpost.goauthentik.io/callback — handled by
the embedded outpost (which has the gate provider attached). The
session cookie is renner.uno-scoped via cookie_domain so the dedicated
paperless-gate outpost picks it up on its in-cluster /auth/nginx
checks against the protected paperless hostnames.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 22:29:25 +02:00
k.rennerandClaude Opus 4.7 f0db3f3293 authentik(paperless): bounce auth flow through auth.renner.uno (2FA origin)
WebAuthn / FIDO / TOTP credentials are bound to the origin they were
registered on (auth.renner.uno). Routing the OAuth flow through
paperless.renner.uno meant the browser couldn't release the credential
during the 2FA stage.

Restructure so the dedicated paperless-gate outpost only serves the
in-cluster /outpost.goauthentik.io/auth/nginx endpoint that nginx
auth_request hits, while every browser-facing leg of the flow (/start,
/callback, the /if/flow login UI, /application/o/authorize, /api,
/static, /flows, /media) happens on auth.renner.uno via the embedded
outpost. Both outposts have the gate's ProxyProvider attached, so the
embedded outpost can issue the OAuth code and the dedicated outpost
can validate the resulting cookie. The cookie is renner.uno-scoped so
it transfers between the two hosts.

Drop the paperless-namespace ExternalName services and the second
paperless-gate-outpost Ingress that was routing Authentik UI paths on
paperless.renner.uno — no longer needed. Update auth-signin to point
at auth.renner.uno.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 22:23:16 +02:00