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>
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
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
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>
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
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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).
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>
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).
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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).
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>
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>
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>
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>
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>
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>
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>
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>