Commit Graph
155 Commits
Author SHA1 Message Date
claude-bot dd954ce23a feat(hermes): run Hermes Agent in the cluster
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
review-bot reviewed: request changes
Hermes Agent as a pod in its own `hermes` namespace: gateway (messaging
platforms, cron, OpenAI-compatible API) and dashboard, supervised by the
image's s6-overlay, state on a 10Gi ceph-block PVC.

- hermes.renner.uno (LAN): dashboard and Hermes Desktop remote gateway.
  Plain Traefik plus the dashboard's own Authentik OIDC login (public PKCE
  client, Hermes Users group) — a forward-auth gate would break Desktop's
  Bearer-token calls.
- hermes-api.renner.uno (LAN): API server, Bearer key generated in-cluster
  by an ESO Password generator.
- Read-only kubectl via the built-in `view` ClusterRole.
- CNPs: world 443/80, kube-apiserver, Traefik for LAN hostnames, and
  Prometheus/Alertmanager/Loki (with the matching monitoring-side grant).
- Image automation for nousresearch/hermes-agent v-tags.

After deploy: log in to Anthropic inside the pod
(`hermes auth add anthropic --type oauth --no-browser`, then `hermes model`);
platform and Home Assistant tokens go in via the dashboard.
Not yet covered: a Velero schedule for the PVC.
2026-10-19 08:02:56 +02:00
security-bot 4baf5aa8be chore(image-automation): bump gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-10-19-0544
Updates managed by image-automation-controller.
2026-10-19 05:58:07 +00:00
security-bot 142a21883d chore(image-automation): bump gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-10-19-0518 docker.gitea.com/gitea-mcp-server:1.10.0
ci/woodpecker/cron/alert-monitor Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
Updates managed by image-automation-controller.
2026-10-19 05:28:07 +00:00
claude-botandClaude Opus 5.5 413f675bfa chore(authentik): run the samba-bridge images from konairius/*
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
review-bot reviewed: comment
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
authentik-samba-bridge moved to the konairius org and publishes as
registry-bot (konairius/authentik-samba-bridge#7, pipeline #42):
gitea.renner.uno/konairius/authentik-samba-bridge-{server,ldap-outpost}
:2026.8.3-9dd92dd. Between the deployed 872f754 and 9dd92dd only CI, OCI
labels and pyproject URLs changed; the bridge code is the same.

- HelmRelease: server image repository + tag 2026.8.3-9dd92dd.
- LDAP outpost blueprint: container_image 2026.8.3-9dd92dd.
- Repo URLs in the comments follow.

Restarts the authentik server and worker pods and redeploys the LDAP outpost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:22:55 +02:00
claude-botandClaude Opus 5.5 a17c4e8161 feat(cnpg): every cluster incarnation archives to a new prefix; recovery reads the newest
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
review-bot reviewed: comment
A CNPG cluster restored from barman archived back into the location it
recovered from. plugin-barman-cloud's restore job checks the archive target
(plugins[] ObjectStore + serverName) and stops with "Expected empty archive"
when it holds WAL, which after a disaster it always does. It has no
exception for target == recovery source (internal/cnpgi/restore/restore.go,
v0.15). Restore rehearsal run 28 hit it on wp-pg; the other six never got
that far.

The check stays. Instead, archive and source become two ObjectStores whose
paths are chosen per incarnation:

- kubernetes/cnpg-object-stores/: all 14 ObjectStores (<x>-store archives,
  <x>-store-source is read by bootstrap.recovery), in a Kustomization of their
  own with postBuild.substituteFrom: cnpg-paths. Nothing else lives there:
  substitution blanks every ${...}, which is why it is not on the app
  Kustomizations (home-assistant's scripts, the Grafana dashboards). The
  ConfigMap is not optional; without it the paths would render empty.
  woodpecker's objectstore.yaml moves there as wp-pg.yaml.
- bootstrap.sh writes cnpg-paths in the flux phase, before Flux starts, once
  per incarnation (an existing one is kept):
    CNPG_<X>_SOURCE   the prefix among <x>, <x>-g<14 digits> and the store's
                      cnpg.renner.uno/source-prefixes whose <x>/base/ holds
                      the newest completed base backup (backup.info
                      status=DONE; barman uploads it as STARTED when a
                      backup begins, barman src/barman/cloud.py
                      coordinate_backup)
    CNPG_<X>_ARCHIVE  <x>-g<UTC timestamp>, new and so empty
  Every ScheduledBackup has immediate: true, so a new incarnation has a base
  backup in its own prefix minutes after it starts, and a later recovery
  picks it. S3 access is curl --aws-sigv4 with the restore key (already in
  bootstrap.sh's secret list), so the CI image needs no new tool; the
  OpenBao restore reads the same S3 endpoint and region.
- The six clusters that recovered from their archive store now name
  <x>-store-source in externalClusters. serverName stays the cluster name
  everywhere. The app Kustomizations depend on cnpg-object-stores, which
  depends on infrastructure-cnpg-barman.

Verified against the bucket, read-only, 2026-10-16: the restore key lists
renner-uno-k8s-cnpg; the listing parser on real responses, top level and an
encoded nested prefix; for wp-pg it picks wp-pg-woodpecker (newest
20261016T024000) over wp-pg (20260818T023502); leftover test serverNames
hold no base backups and match no candidate pattern.

Production rollout, one time:
1. Create flux-system/cnpg-paths with the current prefixes as both source and
   archive (ak-pg, ha-pg, im-pg, mx-pg, pprl-pg, tr-pg; wp-pg-woodpecker), so
   the running clusters keep archiving where they do.
2. The app Kustomizations that give up their ObjectStores (authentik,
   home-assistant, immich, matrix, paperless, trmnl, woodpecker) must run
   with prune: false (the stack's first PR) when this merges;
   cnpg-object-stores adopts the objects. All 14 also carry prune: disabled.
3. Whether changing externalClusters on a live Cluster restarts its pods is
   not verified; check on one cluster first.

Older generations are not cleaned up: retention applies only to the current
archive prefix.

RECOVERY.md describes the new layout: where the current prefixes are
(cnpg-paths), the per-incarnation backup paths for the precondition check,
archiving to the new prefix after promotion, and adding a CNPG cluster to a
running cluster (its keys go into the existing cnpg-paths by hand). The
manual restore drill reads <x>-store, the current archive. The app
kustomization comments name cnpg-object-stores instead of
infrastructure-cnpg-barman. write_cnpg_paths checks for yq first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:18:14 +02:00
claude-botandClaude Opus 5.5 7f1d7ac777 feat(iot): v30iot workloads run only where a node declares the IoT VLAN; HA no longer waits on them
Five workloads attach a macvlan on ens19 (v30iot) with DHCP IPAM:
shelly-manager and the mDNS relay (inside the home-assistant Kustomization),
matter-server, music-assistant and otbr. On a node without that VLAN the CNI
add fails and the pod never starts.

- k8s-1..3 carry net.renner.uno/iot-vlan: "true"; each of them declares
  ens19 in its VLANConfig. The five Deployments select on it. The comments
  they replace described the retired k8s-edge taint.
- shelly-manager and the mDNS relay move to apps/home-assistant-iot, with a
  Kustomization of their own (depends on gitea, home-assistant and the
  infrastructure Kustomizations). home-assistant's wait: true no longer
  covers them, so HA and everything waiting on it (evcc, trmnl,
  zone-configurator, speech-to-phrase) are Ready without the VLAN, and a
  broken IoT bridge no longer holds them back in production either.
- shelly-manager-auth stays in apps/home-assistant: mqtt-account-controller
  mounts it too, and that Kustomization comes first. Moved with
  shelly-manager, mqtt-account-controller would wait for a Secret that only
  the later Kustomization creates.
- The probes-blackbox and authentik blueprint comments name the new paths.

On a site without the label the IoT Kustomizations fail at their timeout,
naming the Pending pods; nothing else depends on them.

Production rollout:
1. Label the nodes first (talosctl patch the nodeLabels), or the five pods go
   Pending.
2. home-assistant must run with prune: false (the stack's first PR) when
   this merges, so it drops shelly-manager and the mDNS relay from its
   inventory without deleting them and home-assistant-iot adopts them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:15:03 +02:00
claude-botandClaude Opus 5.5 469abc9305 fix(authentik): its ServiceMonitor lives in monitoring-rules; the radius one is the operator's
The authentik chart renders server/servicemonitor.yaml whenever
server.metrics.serviceMonitor.enabled is set, without checking
.Capabilities for monitoring.coreos.com (verified in chart 2026.8.3). The
authentik Kustomization does not depend on monitoring, so on an empty
cluster the Helm install fails until kube-prometheus-stack has installed the
CRDs. The chart's ServiceMonitor is switched off and declared standalone in
monitoring-rules, as cilium and velero already do. The selector matches the
live Service authentik-server-metrics (port metrics, 9300); the new name
differs from the chart's authentik-server, so the two never contend for one
object while production rolls over.

authentik-outpost-servicemonitors.yaml hand-wrote a headless Service
ak-outpost-local-radius-metrics and a ServiceMonitor for it, because the
radius outpost controller used not to expose its metrics port. authentik
2026.8.3 does: the operator creates that Service (http-metrics 9300) and its
own ServiceMonitor. On a cold cluster the operator created the Service first,
with a ClusterIP; clusterIP is immutable, so the dry-run of the hand-written
clusterIP: None copy failed and took the whole monitoring-rules
Kustomization with it (restore test run 29). The file goes; the operator
owns both. No alert rule refers to the radius job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:13:24 +02:00
claude-botandClaude Opus 5.5 7ada3540b5 chore(paperclip): remove Paperclip and everything that only served it
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Removes the Paperclip deployment (namespace paperclip, CNPG cluster
pclp-pg, ingresses, image automation, server image build) and the
infrastructure that existed only for its agents:

- agent sandbox: paperclip-renner namespace and CNPs, the
  kubernetes-sigs/agent-sandbox controller with its namespace, CNP and
  Renovate rules
- agent-runtime-{go,ops} images and the build-agent-runtime pipeline
- matrix bridge (Element <-> Paperclip agents) and synapse's appservice
  registration mount
- monitoring loki-read-gateway (only called by the paperclip Role)
- MQTT account agents-read and the controller's RBAC in ns paperclip
- the OpenBao secret-exists-check policy file
- agent admits in the wiki, lidarr, shelly-manager, mosquitto and
  hubble-relay CNPs; the paperclip-api Gitea webhook host; the wiki API
  key; the ClusterSecretStore namespace entry; the pclp-pg RECOVERY row
- the Paperclip design spec and plan

Authentik group "Paperclip Users" is switched to state: absent so the
blueprint deletes it. package-retention's EXCLUDE_PACKAGES becomes
optional, since its only value was the agent-runtime prefix.

The shelly-manager api-proxy sidecar stays: mqtt-account-controller
calls shelly-manager-api too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 07:50:12 +02:00
claude-botandClaude Opus 5.5 a5845f6b60 chore(authentik): remove the passkey-only flows (stage 3)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Stage 3 of unifying the auth flows. After #1285/#1286 no provider, brand
or flow references the passkey-only objects any more (checked live:
0 providers on either flow; passkey-webauthn-validation and
passkey-passwordless-identification are bound only inside the two
retired flows). They are now declared absent: both flows (their stage
bindings and the policy bindings on those cascade), the
passkey-passwordless-identification stage, the passkey-webauthn-validation
stage and the passkey-user-has-webauthn policy. The shared default
stages they bound (password, MFA validation, login) stay.

The passkey-stepup.yaml key keeps its name, so the blueprint instance
stays the same; it now only carries the OAuth2 providers and the
removals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:14:07 +02:00
security-bot a81be6a53d chore(image-automation): bump gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-10-15-0857
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-10-15 08:57:56 +00:00
claude-botandClaude Opus 5.5 67a1eb2ba5 feat(authentik): move every app onto the SSO login (stage 2)
Stage 2 of unifying the auth flows, after the llama and paperless trial
of stage 1 was confirmed:

- Brand authentik.srv.renner.uno: flow_authentication =
  sso-authentication (was NULL, i.e. the slug-first
  default-authentication-flow with password + MFA). Every provider
  without its own authentication_flow logs in through it, PVE and PBS
  included.
- The 7 blueprint OIDC providers (Home Assistant, NetBird,
  Audiobookshelf, Immich, Gitea, Grafana, Matrix MAS): authentication
  flow unset (brand), authorization implicit consent.
- authentik-gate-sync: default authorization flow implicit consent, via
  DEFAULT_AUTHORIZATION_FLOW on the Deployment and in the code; the
  per-gate login/authorization annotations are dropped. Paperless keeps
  paperless-authorization.

Every flow referenced here already exists, so the gate controller does
not hit the missing-flow removal seen in stage 1. Stage 3 removes the
passkey-only flows once nothing references them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 10:41:17 +02:00
claude-botandClaude Opus 5.5 5e495d300a feat(authentik): add SSO login and paperless two-factor flows, trial on two gates
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Stage 1 of unifying the auth flows into two security levels on one SSO
login:

- sso-authentication: one identification stage that takes the password
  inline or a passkey via autofill (webauthn_stage); either factor alone
  logs in.
- paperless-authorization: password and passkey on every authorize.
  Authorization flows are planned without a pending user (oauth2
  authorize.py plan context), so a bare PasswordStage there fails with
  "No pending user"; the password is therefore checked inline by an
  identification stage, a deny stage rejects an identified user that
  differs from the session user, and a passkey validation with
  last_auth_threshold 0 follows (no authentik_mfa cookie, so it always
  asks).

Trial targets: llama-server (normal level: sso-authentication +
implicit consent) and paperless (high level). The brand, the OIDC
providers and the remaining gates follow once both are confirmed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 10:17:33 +02:00
claude-botandClaude Opus 5.5 201990af6d feat(authentik): bootstrap ak-pg from its barman backup
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ak-pg declared bootstrap.initdb, a leftover of the retired seed-job design,
so a cold rebuild came up with an empty Authentik database: no users,
passkeys, groups or gate membership. It now declares bootstrap.recovery
from ak-pg-store (serverName ak-pg), the stanza RECOVERY.md already
documented and the pattern wp-pg uses.

bootstrap is honoured only when PGDATA is absent, so the live cluster is
unaffected. An empty object store makes bootstrap hang instead of coming
up empty. RECOVERY.md now limits the manual initdb->recovery swap to im-pg
and pprl-pg.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:24:54 +02:00
security-bot 34b5708f96 chore(image-automation): bump gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-10-15-0527
Updates managed by image-automation-controller.
2026-10-15 05:27:55 +00:00
security-bot 8df9cbab5f chore(image-automation): bump gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-10-14-1955
ci/woodpecker/cron/wiki-maintenance Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-10-14 19:57:54 +00:00
claude-botandClaude Opus 5.5 50b0aafdf2 feat(authentik-gate-sync): publish the route's address as the source Ingress status
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Like any ingress controller, authentik-gate-sync now owns the load-balancer
status of the Ingresses of its class: each pass it copies the address Traefik
reports for the generated route onto the source Ingress, and clears it while
the gate has no route. Previously the migrated Ingresses kept the address
Traefik wrote before their class changed, which Traefik never clears.

Status publishing is non-fatal (logged), so it never holds or removes a gate.
RBAC gains ingresses/status; the change is additive, so the order in which
Flux applies RBAC and the new image does not matter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:54:09 +02:00
claude-botandClaude Opus 5.5 01213d6ee0 feat(authentik-gate): migrate the remaining 12 gates to the authentik-gate class
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
paperless, shelly-manager, zone-configurator, esphome, zigbee2mqtt,
alertmanager, prometheus, wiki, paperclip, navidrome, lidarr and wordpress
now gate through authentik-gate-sync: each source Ingress names its real
backend and carries the old provider's settings as annotations (group,
login-flow, authorization-flow where it was implicit consent, paperless'
skip-path).

Removed per gate: the ExternalName Service, the egress CNP in authentik,
the namespace CNP rule admitting the old outpost, and the blueprint
provider/application/binding/outpost (state: absent, outpost ->
application -> provider). Groups stay declared without users lists;
membership is managed in Authentik.

Per-gate specifics:
- paperless: the docs.renner.uno alias backends the generated
  paperless-gate Service (its middleware redirects before the backend); the
  auth.renner.uno/outpost.goauthentik.io route is removed, proxy outposts
  take their callback on the app host.
- alertmanager / prometheus: source Ingresses renamed to alertmanager /
  prometheus; the controller generates the *-gate route Ingresses.
- wordpress-admin: gains a tls block for www.renner.uno (wordpress-tls,
  certificate still owned by the public Ingress), so the gate's external
  host stays https.
- wiki, paperclip: only the gate peer/rule leaves their shared CNPs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:37:26 +02:00
security-bot a39efcee68 chore(image-automation): bump gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-10-14-1925
Updates managed by image-automation-controller.
2026-10-14 19:27:54 +00:00
claude-botandClaude Opus 5.5 fb282baf6d feat(home-assistant): gate vscode.home.renner.uno through the authentik-gate class
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
First group gate on authentik-gate-sync. The Ingress switches to
ingressClassName authentik-gate with its real backend
(home-assistant-vscode:80) and annotations for the group (Home Assistant
Config Editors) and the code-server skip path; the default
passkey-stepup-authorization flow matches the old provider.

Removed: the ExternalName Service, the vscode-gate egress CNP, the
home-assistant-ns ingress rule for the old outpost, and the blueprint
provider/application/binding/outpost (state: absent, outpost ->
application -> provider). The group stays declared in the blueprint but
without a users list: membership is now managed in Authentik, and current
members are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:21:52 +02:00
claude-botandClaude Opus 5.5 d549f9234f fix(authentik-gate-sync): keep named targetPorts named in the gate CNPs
A named targetPort was resolved to a number through the Service's
EndpointSlices, so with no ready endpoints (app restarting, crashlooping)
the gate counted as invalid: its route was removed and its Authentik
objects torn down, to be rebuilt once the pods came back. Cilium accepts
named ports in toPorts and resolves them against the selected pods'
container ports, so the name now goes into both CNPs as-is and the gate no
longer depends on the backend being up. The EndpointSlice read (and its
RBAC) is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:20:47 +02:00
claude-botandClaude Opus 5.5 406b7b61f7 feat(llama-server): gate llm.renner.uno through the authentik-gate class
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
First gate on authentik-gate-sync. The Ingress keeps its host, TLS, public
label and cert-manager annotation, switches to ingressClassName
authentik-gate and names its real backend (llama-server:8080); the
passwordless login flow and the manifest/service-worker skip path move to
auth.renner.uno/* annotations. The controller now owns the outpost, the
Traefik route and both CNPs.

Removed: the hand-written ExternalName Service, the llama-gate egress CNP,
the ai-ns ingress rule for the old outpost, and the llama-gate blueprint
objects (state: absent, outpost -> application -> provider).

Expect a short gap on llm.renner.uno at switch-over: Traefik drops the route
when the class changes and serves the new one once the controller's next pass
has started the new outpost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:08:30 +02:00
security-bot 5d45ce47bc chore(image-automation): bump gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-10-14-1906
Updates managed by image-automation-controller.
2026-10-14 19:07:09 +00:00
claude-botandClaude Opus 5.5 38cd002385 fix(authentik-gate-sync): address review — lookups, ownership, fail-closed access
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
- Proxy providers are filtered by name__iexact (plain name= is ignored and
  returned the first provider alphabetically).
- Applications are retrieved by slug, never listed: the list endpoint is
  policy-filtered for the token's user and cached per user regardless of
  filters. Cleanup finds owned apps through the proxy provider list and
  confirms meta_publisher + provider one by one.
- Group gates bind an expression policy (ak_is_group_member by name, the name
  as a repr() literal) instead of the group: deleting a group cascades its
  bindings away and would open the app, the policy keeps denying. New bindings
  are created before stale ones are removed.
- Per-gate errors no longer abort the pass; a gate whose Ingress is invalid
  loses its route immediately. Network errors become ApiError.
- Gate names carry a hash of namespace/name: unambiguous and bounded to fit
  label values and ak-outpost-<name>.
- Teardown removes only an outpost serving exactly the gate's provider; a
  provider orphaned by an interrupted teardown is adopted on recreation.
- Role gains the expression-policy permissions; tests cover the Authentik
  reconcile against an in-memory API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:51:15 +02:00
claude-botandClaude Opus 5.5 d1ed8685a9 fix(authentik-gate-sync): start on :latest until image automation pins a tag
Branch pushes do not build in-house images, so no dated tag exists before merge; the master build pushes :latest alongside it and the setter marker replaces it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:40:24 +02:00
claude-botandClaude Opus 5.5 bbb1582bcf feat(authentik): authentik-gate ingress class controller
Adds authentik-gate-sync, an in-house controller (ci/images/authentik-gate-sync,
stdlib Python like netbird-service-sync) that implements an `authentik-gate`
IngressClass. An Ingress of that class names its real backend; the controller
turns it into an Authentik proxy gate and wires the upstream ingress provider:

- Authentik: proxy provider, application (meta_publisher marks ownership),
  outpost on the local Kubernetes connection, optional group binding.
- App namespace: ExternalName Service <ingress>-gate -> outpost, Traefik
  Ingress <ingress>-gate (source hosts/paths/TLS/labels/annotations, backend
  swapped, cert-manager annotations dropped so only the source issues), and a
  CNP admitting only this outpost to the backend pods.
- authentik namespace: CNP letting the outpost reach only those pods.

Annotations: auth.renner.uno/group (members managed in Authentik; none = any
user), /login-flow, /authorization-flow (default passkey-stepup-authorization),
/skip-path. Fail closed: the Traefik route is applied last and removed when the
gate's config is invalid (missing group/flow/backend); transient API errors
leave the gate as the last good pass left it.

The API token is zero-touch: an ESO Password generator (CreatedOnce) feeds
both the controller and AUTHENTIK_GATE_SYNC_TOKEN on server/worker, which the
new gate-sync blueprint sets as the key of a non-expiring token for a
service account whose role covers only gate objects.

No existing gate moves yet; llama is migrated in a follow-up once this runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:39:50 +02:00
claude-botandClaude Opus 5.5 7518022b59 chore(openproject): decommission OpenProject
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
OpenProject Community hides every OIDC provider without an Enterprise
token (auth_plugin.rb, EnterpriseToken.allows_to?(:sso_auth_providers)),
so it cannot sign in through Authentik.

Removes the app, its Flux Kustomization, namespace, network policy,
op-pg PodMonitor, blackbox probe, Velero schedule and ClusterSecretStore
entry. Authentik drops the OIDC secret env and marks the OpenProject
application, provider and group absent. Gitea no longer allows
openproject.renner.uno as a webhook host.

Refs: REN-1363

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:04:56 +02:00
claude-botandClaude Sonnet 5.5 37c4287931 feat(openproject): sign in through Authentik OIDC
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Authentik gets an OpenProject confidential OAuth2 provider, application and
OpenProject Users group in the blueprint; the client secret is read from
OpenBao apps/openproject through an ExternalSecret and !Env. OpenProject
seeds the matching provider from env, and its egress policy allows
Authentik via Traefik.

Refs: REN-1363

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-09 13:10:16 +02:00
atlas-botandClaude Opus 5.5 024eb97292 feat(authentik): samba-bridge images 2026.8.3-872f754 (REN-723)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Server and LDAP outpost images built from k.renner/authentik-samba-bridge
872f7546 (Authentik 2026.8.3), in lockstep with the 2026.8.3 chart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 08:37:50 +00:00
atlas-botandClaude Opus 5.5 ca003e9cd8 feat(authentik): 2026.8.3 chart + trusted proxy CIDRs (REN-723)
2026.8 honours forwarded headers only from AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS.
Set it to the pod CIDRs Traefik connects from. Bridge image tags follow
once authentik-samba-bridge#4 is merged and built (lockstep pin).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 08:36:33 +00:00
atlas-botandClaude Opus 5.5 e2e45b2b86 fix(authentik): allow authorization_code for matrix-mas (REN-567)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Authentik 2026.5 checks each authorize against the provider's
grant_types, and a provider created by a blueprint starts with an empty
list. Every Matrix login through Authentik failed with invalid_request
(log: "Invalid grant_type for provider", grant_type=authorization_code).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:19:26 +00:00
atlas-botandClaude Opus 5.5 b17001d6da feat(matrix): MAS with Authentik passkey login and QR sign-in (REN-567)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was canceled
ci/woodpecker/pr/ci-manifests-test Pipeline was canceled
Element X signs in only with a password or through MAS. Put MAS in front
of Synapse with Authentik (passkey) as the upstream provider, and enable
the QR rendezvous (MSC4108/MSC4388).

- apps/matrix/mas.yaml: MAS 1.26.0, `mas` database in mx-pg (CNPG
  Database), account.matrix.renner.uno.
- synapse.yaml: matrix_authentication_service, password_config off; a
  syn2mas init container moves the existing accounts and sessions once
  (marker on synapse-data).
- Drop the owner-account Job; MAS owns accounts now.
- Authentik: public PKCE client `matrix-mas`, passkey flows, only the
  Matrix Users group (k.renner).
- matrix-ns: MAS egress to Traefik :8443 (auth.renner.uno) only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:08:04 +00:00
claude-botandClaude Opus 5.5 378e433086 feat(lidarr): missing-album view over the Navidrome library
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Lidarr in the navidrome namespace as a "what is missing" UI: artist
discographies (Wanted -> Missing) and Spotify followed artists / saved
albums / playlists as import lists.

- Mounts navidrome-music-bulk read-write (Lidarr has no read-only root
  folder mode); config on ceph-block.
- A reconcile sidecar (same image, curl+jq) reads the API key from
  /config/config.xml and on every start sets renameTracks=false,
  writeAudioTags=no, no recycle bin, no empty-folder deletes, no
  permission changes -- then adds /music as root folder only if absent,
  so the automatic library import never runs with file-mutating
  settings.
- No download client or indexer.
- AuthenticationMethod=External behind a dedicated lidarr-gate outpost
  restricted to a new "Music Librarians" group (k.renner).
- Image automation on the linuxserver build number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 19:33:54 +02:00
claude-botandClaude Opus 5.5 cb81157216 feat(authentik): paperclip-gate outpost and Paperclip Users group
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:07:14 +02:00
claude-botandClaude Opus 5.5 222d0263a4 feat(navidrome): self-hosted music streaming on the bulk pool
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Navidrome (Subsonic API) as the Spotify replacement for phone apps.

- Library PVC on ceph-filesystem-bulk (RWX, 300Gi, mounted read-only);
  SQLite data on ceph-block (RWO).
- Web UI behind a dedicated navidrome-gate Authentik proxy outpost;
  Navidrome trusts X-Authentik-Username (ExtAuth) and auto-creates the
  user on first SSO login (first user = admin).
- /rest and /share routed straight to Navidrome with the identity header
  stripped, so Subsonic clients use their Navidrome password.
- Namespace baseline CNP, gate egress CNP, Flux image automation,
  Velero daily schedule (09:15), bootstrap wait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:12:08 +02:00
claude-botandClaude Fable 5.1 ae5de9bc84 feat(wiki): Authentik wiki-gate in front of wiki.renner.uno
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Proxy provider, application and outpost blueprint; outpost egress CNP;
ExternalName mirror; the ingress backends the outpost and the BasicAuth
middleware is gone. The ai CNP admits the outpost and Gitea only, so the
X-Authentik-Username header the service trusts from pod addresses cannot
arrive from the LAN.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 12:48:22 +02:00
claude-botandClaude Opus 5 9a521568c0 fix(dns): migrate external-dns hostname annotations off the alpha prefix
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
external-dns v0.22.0 changed the default --annotation-prefix from
external-dns.alpha.kubernetes.io/ to external-dns.kubernetes.io/ with no
fallback, and its release notes lead with the consequence: "This change can
delete all your DNS records."

The chart floated onto it at 2026-09-11 23:47:56 (appVersion 0.21.0 ->
0.22.0). Two hours later, at 2026-09-12 01:47:59, the unifi provider logged
`client deleted record` for every name that depended on the annotation:

  ha.renner.uno       mqtt.renner.uno     ldap.renner.uno
  syslog.renner.uno   ftp.paperless.renner.uno
  syncthing.paperless.renner.uno

A, AAAA and the TXT ownership records, and no `client created record` since.
Every one of those is a LoadBalancer Service, whose hostname external-dns
can only learn from the annotation — so the source went silent and
--policy=sync removed the records. Ingress-sourced names take their host
from spec.rules[].host and were untouched, which is exactly the split
observed: source_endpoints_total is 36 with source_errors_total 0.

These six annotations were the only alpha-prefixed external-dns annotations
in the repo, so moving them to the current prefix is the whole fix. Pinning
the prefix back with --annotation-prefix would keep us on a deprecated
default that upstream intends to drop.

Scanning has been broken since: the Brother resolves ftp.paperless.renner.uno
rather than using the address.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 10:00:30 +02:00
claude-botandClaude Opus 5 939028fadd refactor(flux): CRD consumers move behind the CRDs they need
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
One unknown kind fails a server-side dry-run for the WHOLE apply set,
including the very HelmRelease that would have installed the missing
CRDs. On a running cluster every CRD exists, so this never showed; a
cold rebuild deadlocks on it immediately.

Three groups move, content unchanged:

ExternalSecrets and Traefik Middlewares out of infrastructure into
post-crds, which reconciles after the operators. The flux-git
ExternalSecret also changes creationPolicy Merge -> Orphan with
deletionPolicy Retain: Merge never CREATES the target secret, so on a
fresh cluster the Gitea credential handover silently did not happen.

ServiceMonitors/PodMonitors out of their charts and app sets into
apps/monitoring-rules, which applies after kube-prometheus-stack owns
the CRDs. Rook additionally stops creating its own two monitors
(spec.monitoring.enabled: false) — creating them is fatal to its
node-daemon reconcile when the CRD is missing, which held the
CephCluster in Progressing and with it the whole apps layer. Both
objects are reproduced field for field off the live cluster; the
ceph-exporter DaemonSet and the metrics endpoints are untouched.
Cilium is the worst case: its chart aborts rendering outright on the
missing CRD, and cilium is one of the seven infrastructure
healthChecks — the lowest layer in the cluster waiting on the highest.

Steady-state effect: object ownership moves between Kustomizations;
specs are identical. Verified on local vmtest runs and the Hetzner
throwaway cluster.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 20:47:01 +02:00
issue-fix-bot e0a00f6b05 fix: [alert] KubeHpaMaxedOut (authentik) (#761)
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
2026-09-02 13:45:55 +00:00
claude-botandClaude Opus 5 3f014a0020 fix(llama-gate): name the authentication flow, as Audiobookshelf does
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The gate set only authorization_flow, so authentication fell back to
Authentik's default flow and the step-up ran on top of a password prompt
instead of replacing it — a clumsy sign-in for a UI meant to be opened often.

Audiobookshelf and immich set both halves, and that is the pattern to copy for
a user-facing app:

  authentication_flow: passkey-passwordless-authentication
  authorization_flow:  passkey-stepup-authorization

Both are referenced with !Find rather than !KeyOf because they are defined in
the passkey-stepup document of this ConfigMap, and !KeyOf resolves only within
a single document.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 21:52:54 +02:00
claude-botandClaude Opus 5 aa53335c2b feat(llama-server): public WebUI behind an Authentik gate
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
llm.renner.uno becomes public and is fronted by a dedicated Authentik proxy
outpost. The gate is the entire access control: llama-server has no
authentication of its own and serves the OpenAI API on the same port as the
chat UI, which is exactly why the hostname was LAN-only until now.

Follows the vscode-gate pattern, including its passkey step-up, for the same
reason — what sits behind the gate is unprotected, so the gate is not a second
factor on top of an app login, it is the only one.

The API stays in-cluster. Home Assistant, Paperless and CI address
llama-server.ai.svc.cluster.local:8080 directly, never traverse Traefik or the
outpost, and so need no credentials. Nothing about them changes.

Traefik is deliberately removed from the ai namespace's ingress policy. It
routes the hostname to the outpost and never to llama-server, and dropping the
rule makes that structural: an Ingress pointing straight at the Service, by
accident or otherwise, would simply not connect.

Two details taken from prior gate failures rather than rediscovered:

  - No /outpost.goauthentik.io Ingress on auth.renner.uno. Only paperless-gate
    has one; the other gates serve those paths on their own host. Adding a
    second would have claimed a host+path paperless already owns and broken its
    login.
  - skip_path_regex covers manifest.webmanifest and sw.js, both of which the
    WebUI serves. A manifest is fetched with credentials omitted per the W3C
    spec, so it carries no cookie and the gate answers with a redirect, turning
    every page load into a round-trip. That is the failure that forced
    paperless-gate's carve-out and retired music-assistant-gate outright.

No group and no policy binding: any user who can authenticate gets in, as with
Immich, Audiobookshelf, Home Assistant and Music Assistant. The gates that pin
membership to a named user in git are admin interfaces — code-server over HA
secrets, esphome, zigbee2mqtt, prometheus — where the allowlist is the point. A
chat UI is not one of those, and its membership belongs in the user directory
rather than in a manifest.

The e2e checks move to the Service. They used to drive the public hostname,
which now answers with an Authentik redirect; going in-cluster is also how every
real consumer reaches it. A new check asserts the public hostname bounces an
anonymous request, so a regression that let Traefik reach llama-server directly
would fail rather than pass quietly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 21:41:08 +02:00
claude-bot 15ec3ce2f0 fix(monitoring): stop double-scraping two authentik outposts
Chasing a stale comment turned up a live duplicate. Since serviceMonitorSelector
was loosened to {}, Prometheus discovers the goauthentik.io operator's own
ServiceMonitors — which is why it was loosened. The hand-written copies for ldap
and paperless-gate, added back when the strict selector was dropping the
operator's, now scrape the same endpoints a second time.

Measured, not assumed: 10.244.4.113:9300 and 10.244.4.76:9300 were each
producing 40 samples per 10 minutes against 20 for every other outpost, and
ldap picked up a third target because the hand-written selector matched the
plain Service as well as the -metrics one. No duplicate-timestamp errors,
because the two pools scrape at different offsets — the cost is doubled
ingestion and a target list that lies about how many outposts there are.

Radius stays, and the comment now says why it is not the same case: the
operator's ServiceMonitor for it selects on managed-by=goauthentik.io together
with service-type=metrics, and no Service carries both — the operator's has the
first, the one defined here has the second. It is the only thing scraping
radius, so deleting it would have been a silent loss.

Also corrects the comments that claimed the release label is required for
discovery. It is not, for ServiceMonitors and PodMonitors: those two selectors
are empty. It very much is for Probes, PrometheusRules and ScrapeConfigs, whose
selectors still match on it, so probes-blackbox.yaml now says so explicitly —
otherwise the corrected comments elsewhere read as license to drop the label
there too.

Live selector state, for the record:
  serviceMonitorSelector {}   podMonitorSelector {}
  probeSelector / ruleSelector / scrapeConfigSelector  release=kube-prometheus-stack
2026-08-28 09:00:58 +02:00
claude-bot aac2599e5f docs(authentik): trim the blueprint comments, keep every ordering rule
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
559 -> 509 comment lines. A far smaller cut than the other files, and that is
the honest result: most of this file is per-entry rationale for a declarative
blueprint, where the comment is the only place the constraint is written down.

Everything load-bearing stays, and stays emphasised. Every ORDER MATTERS note
survives — Application before Provider, Application before Provider before
Outpost — because Application.provider is on_delete=PROTECT and getting the
order wrong leaves the blueprint failing to apply with no obvious cause. So do
both "git owns membership, add and remove people HERE not in the UI" notes,
since blueprint apply silently reverts UI edits to those groups. So does the
reason each gate exists at all: code-server runs --auth=none as root over
plaintext HA secrets, and shelly-manager has no authentication whatsoever.

Cut: the derivations. Why the dual-OAuth2 setup was collapsed into one
provider, the full W3C-spec account of credentials:'omit', a paragraph
reconstructing how the music-assistant gate failed. The instruction that came
out of that last one is kept and sharpened into "do not reintroduce a gate in
front of a PWA".

Two stale things went with it. The paperless proxy comment said the ingress
controller "only routes paperless.renner.uno to the outpost service" and named
ingress-nginx as that controller — there is no ingress-nginx left, Traefik is
the only IngressClass. And a leftover instruction to "delete via the Authentik
UI once everything's confirmed working" from a session long finished.

Verified beyond the usual: these comments live INSIDE the embedded blueprint
string, so a broken indent would render fine and only fail when Authentik
parses it. Extracted the rendered ConfigMap's k8s-outposts.yaml and parsed it
separately — valid, and still 74 entries.
2026-08-28 07:17:25 +02:00
claude-bot 2f3d952656 refactor(secrets): drop the unused barman-cloud-creds
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Three ExternalSecrets synced a Secret that nothing reads. Established rather
than assumed, from three directions: every live ObjectStore names
hetzner-s3-creds, no CNPG Cluster uses the legacy inline barmanObjectStore
(all seven run the barman-cloud plugin against an ObjectStore CR), and no pod
in the cluster mounts barman-cloud-creds.

They are almost certainly left from before the plugin migration, when the
inline configuration read a Secret of that name.

Worth stating plainly because it is not merely untidy: seed/{authentik,immich,
paperless}/barman-creds hold a Hetzner S3 credential that is NOT the one in
use. Fingerprint c2c5cf3e against 87954b64 for the live hetzner-s3 path, with
the three copies identical to each other. A valid key with no consumer is
exposure without benefit, so if it is a distinct S3 key it wants revoking at
Hetzner, which only a human can do — deleting our copies does not.

Removing the manifests does NOT remove the objects. All three carry
kustomize.toolkit.fluxcd.io/prune: disabled from the seed->app-KS handoffs, so
Flux skips them during garbage collection by design. The ExternalSecrets, their
Secrets and the three OpenBao paths have to be deleted explicitly after this
merges; git alone leaves them live and orphaned, which is worse than leaving
them declared.

The OpenBao paths deliberately stay until then. Deleting them first would leave
three ExternalSecrets failing to sync against a key that no longer exists, for
no gain.
2026-08-27 21:18:08 +02:00
claude-bot c59f0050b3 refactor(secrets): one OpenBao path for the one Hetzner S3 credential
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
#397 asked for the opposite: give woodpecker its own path so a rotation of
paperless's credentials would stop silently changing woodpecker's. The premise
does not hold. There is one Hetzner S3 key, and rotating it at Hetzner changes
it for every consumer no matter how many paths hold a copy. A fifth copy would
have bought nothing and made the separation look real.

Seven ExternalSecrets were reading it from four paths — seed/paperless (four
namespaces), seed/authentik, seed/immich, seed/trmnl. They now all read
infrastructure/hetzner-s3, which sits beside the existing
infrastructure/velero-hetzner.

That the four paths are byte-identical was verified rather than assumed, and
without reading them: SHA-256 over the three fields of each synced Secret
returns the same fingerprint in all seven namespaces. The same check says
infrastructure/velero-hetzner carries the same access key id, in a different
shape — a single `cloud` field holding an AWS credentials file.

Velero is deliberately not folded in. Moving it means templating that INI from
these fields, which is worth doing and not worth bundling with a path swap that
touches every database backup at once.

The reasoning lives once, in the paperless manifest, with the others pointing at
it. Repeating it six times is how six copies drift.

Kept from the note this replaces, because it was right and cost someone an
afternoon: the 403 recorded in #397 was a wrong-path error, not missing
permissions. The mount is `homelab`, so the path is homelab/seed/... —
`bao kv list homelab/seed` works with the identity available here.

homelab/infrastructure/hetzner-s3 now exists, copied from the paperless path
inside OpenBao so the value never left it, and fingerprints identical to its
source. Writes turned out to be possible all along: they fail through
openbao.renner.uno with a response the client cannot parse, and succeed against
the Service. That is worth knowing before the next person concludes, as I first
did, that the identity is read-only.

Closes #397
2026-08-27 21:02:28 +02:00
claude-bot 119b2c77ef perf(cnpg): roll archive_timeout out to the remaining six clusters
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
A WAL segment is a fixed 16 MiB file. archive_timeout forces a switch on a
timer, and the switched-out segment is written and archived at its full size
no matter how little real WAL it holds. CNPG defaults to 5min, permitting
288 segments/day per cluster of largely padding.

gf-pg went first as a staged rollout and is now verified: 211 -> 41 MB/hour,
an 81% cut, with archived segments falling from ~12/hour to ~1.5/hour in
step. Two independent signals moving together is what confirms the volume
was padding rather than data, and the measured result matched the ~35
MB/hour predicted beforehand.

Fleet-wide this is worth roughly 18 GB/day of logical writes, which at
size=2 with ~1.27x BlueStore overhead is ~45 GB/day at the drives -- about
14% of the 328 GB/day the three OSD NVMes currently take.

ak-pg gets 15min rather than 30min. It holds credentials, sessions and
enrolment state, and the extra 15min would save only a further 0.3 GB/day,
which does not justify doubling the worst-case loss window on that data.

ha-pg saves least in relative terms: its segments are already 63% full of
real WAL (4.9 GB/day), so the timer is not what drives most of its switches.

Also corrects two comments in ha-pg-cluster.yaml that this work falsified.

wal_compression: the claim that these clusters "were not getting compression"
was wrong -- "on" is pglz, which was already compressing. Switching to zstd
moved the WAL rate not at all (4911/4976 -> 5002 MB/day, by LSN advance over
20min windows). Full-page images genuinely are ~62% of WAL here (61.9%
measured as a steady-state delta, against 62.4% lifetime -- so the migration
did not skew it, which was my first guess and it was wrong). There was simply
no headroom between pglz and zstd on this data. zstd is kept because it is
not worse and costs nothing, but it is not a lever.

checkpoint_timeout: 15min -> 30min likewise did not move WAL. Probable reason
is that a recorder is append-mostly -- a page is dirtied while it fills and
then not touched again, so it pays its full-page image once whatever the
checkpoint interval. Kept, but flagged as unjustified pending a data-file
measurement: this volume takes ~11 GB/day against 4.9 GB/day of WAL, so most
of its traffic is outside what a WAL measurement can see.

Context: drive-endurance work on three QLC NVMe OSDs at 81-87% wear.
2026-08-20 15:36:26 +02:00
claude-bot 591c76fb86 perf(cnpg): compress WAL with zstd instead of pglz
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
`wal_compression: "on"` resolves to pglz, PostgreSQL's original 2016-era
algorithm. Every CNPG cluster here was set that way, so none of them were
getting the compression actually available to them: all seven builds accept
lz4 and zstd (verified via pg_settings.enumvals on each, including immich's
custom pgvector image and trmnl's PostgreSQL 18).

This matters because of what dominates recorder-style write volume. A page
is written to WAL in full -- all 8 KiB -- on its first modification after a
checkpoint, for torn-page protection. Measured on ha-pg, those full-page
images are 62.4% of WAL bytes. Compressing them is therefore the single
largest lever available on WAL volume, and pglz was the weakest option for
the job.

ha-pg additionally moves checkpoint_timeout 15min -> 30min. Full-page images
are emitted once per page per checkpoint interval, so doubling the interval
roughly halves how often each hot page pays the 8 KiB toll. max_wal_size
stays at 2GB deliberately: it must remain well above what one interval
generates, or checkpoints would trigger on size and the interval change
would do nothing. Recovery replays at most one interval of WAL -- under
200MB at the measured rate, i.e. seconds.

The other six clusters keep checkpoint_timeout at 15min. Their measured
write rates do not justify trading recovery time: gf-pg generates 4.5 MB/day
of WAL and wp-pg 62 MB/day, against ak-pg's 604 MB/day.

full_page_writes stays on everywhere. Disabling it would remove the
overhead entirely but RBD can tear a page across an object boundary, and
the trade is write volume against an unrecoverable database.

Both parameters are sighup-reloadable, so this does not restart any cluster.

Context: drive-endurance work on three QLC NVMe OSDs at 81-87% wear.
2026-08-20 14:03:18 +02:00
claude-botandClaude Opus 5 68c8c86046 fix(cnpg): checkpoint tuning across all Postgres clusters
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Ten Postgres instances across seven CNPG clusters are ~48 GB/day of the
cluster's ~114 GB/day of client writes, and only ha-pg carried any
write-reduction tuning. The rest ran Postgres defaults, which favour throughput
over storage that wears out.

MEASURED, not assumed. ak-pg since 2026-05-17:

  checkpoints_timed   27,624      = 291/day, i.e. one every 5 min (the default)
  buffers_checkpoint  25,801,628  = ~206 GB flushed
  database size       1,046 MB

That is the whole database rewritten roughly twice a day. The pathology is
specific to idle databases: sessions, tokens and job state re-dirty the same few
pages constantly, and checkpoint_timeout=5min forces each of them to disk 288
times a day instead of letting them coalesce. At 15min they are written a third
as often.

max_wal_size is raised alongside it. Without that, checkpoints trigger on size
instead and the timeout change does nothing. It also bounds crash recovery to at
most 2GB of WAL replay.

Applied to ak-pg, pprl-pg, tr-pg, wp-pg, im-pg, gf-pg. ha-pg already had it.

synchronous_commit is deliberately NOT changed here. It is off for ha-pg because
a state recorder is telemetry; these clusters hold credentials, documents and job
state, where losing the last few hundred ms of commits on an unclean shutdown is
not an acceptable trade for write volume.

Separately, ha-pg shared_buffers 256MB -> 512MB. After 15h on Postgres its
buffers_backend was 629,704 of 939,025 total page writes -- 67% written by
backends rather than the checkpointer or bgwriter, which means the working set
does not fit in the buffer pool and pages are written out repeatedly instead of
coalescing. Memory limit raised 1Gi -> 1536Mi to keep the same headroom (measured
383Mi in use, of which 256MB was shared_buffers).

Verified before commit by rendering every cluster and asserting the tuning landed
under spec.postgresql.parameters with the barman plugin block untouched. That
check exists because the first attempt at this matched the wrong `parameters:`
key -- the barman plugin's, at 6-space indent under plugins: -- and corrupted
wp-pg's barmanObjectName. Reverted and redone with an indent-aware match.

Refs #434

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 06:55:42 +02:00
claude-botandClaude Opus 5 b784ce3d38 feat(wordpress): host www.renner.uno in the cluster behind an Authentik gate
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Migrates the Hetzner VM 'wordpress-hochzeit' into the cluster. The site is not
the frozen wedding archive it looks like: it carries an unpublished, fully
composed birth announcement from 2026-05-14, so wp-admin has to keep working
for a human author rather than being replaced by a Git workflow.

Split routing on one host: '/' goes straight to WordPress so the blog stays
publicly readable, while /wp-admin and /wp-login.php route through a dedicated
wordpress-gate outpost. The inverse -- gate the host and skip_path_regex the
public paths, as paperless-gate does -- would need a negative lookahead, and
the outpost's RE2 has none.

Gating all of /wp-admin is only safe because the dead wpgiftregistry plugin is
dropped in the migration: it was the sole emitter of ajaxurl on this site, and
of 121 admin-ajax hits in the current log window not one returned 200.

Core lives in the pinned image, not on the volume, so upgrading is a tag bump
in Git. Only wp-content is persisted.

Backups are split the same way CNPG/Barman and Velero are elsewhere: the
operator takes a logical dump to Hetzner Object Storage at 02:45, Velero takes
the wp-content PVC at 08:45 and skips the database volume -- a crash-consistent
snapshot of a running database is not a backup.

Group membership for the gate is owned by Git, as with the Home Assistant,
Shelly and zone-configurator gates. s.renner is the Authentik account for the
site's WordPress Editor, whose login there is still the pre-2019 s.birzer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014giKN9N6e5J2DF8QZmVh98
2026-08-16 21:48:49 +02:00
claude-botandClaude Opus 5 803cdb3db8 feat(authentik): fall back to password when no passkey is enrolled
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The passkey apps were reachable only with a WebAuthn credential:
passkey-passwordless-authentication denies at its validation stage, and
passkey-stepup-authorization demands a passkey on every authorize. An
account with no passkey had no way in.

Branch both flows on enrollment with an expression policy,
passkey-user-has-webauthn. Enrolled accounts keep the mandatory passkey
at login and at every authorize. Accounts without one skip the WebAuthn
stage and get default-authentication-password followed by
default-authentication-mfa-validation — the same pair, in the same
order, that default-authentication-flow already runs, so the fallback is
not a weaker way in than auth.renner.uno's own login.

The policy runs against pending_user, so the branch follows the
identified account's enrollment and is not selectable by the requester.
Bindings keep evaluate_on_plan false and re_evaluate_policies true;
on-plan evaluation would run before a user is resolved.

The negated bindings carry failure_result: true. UserLoginStage defaults
a missing authentication backend to BACKEND_INBUILT rather than
refusing, so a flow that dropped both branches would log the user in on
a username alone. A PolicyException resolves to failure_result before
negate is applied, so a broken expression degrades to password + MFA
instead of to no factor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 09:45:24 +02:00
claude-botandClaude Opus 5 9e0ffae358 fix(authentik): share one WebAuthn stage across login and step-up
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Every fresh login to a passkey app costs two touches: one in
passkey-passwordless-authentication, a second immediately after in
passkey-stepup-authorization.

last_auth_threshold is enforced with a single browser cookie
(authentik_mfa) whose JWT is signed with, and carries, the validation
stage's PK. A stage only honours a cookie it wrote itself, and both
stages write to the same cookie name — so two stage objects each demand
their own touch and clobber each other's window.

Collapse them into one passkey-webauthn-validation stage bound into both
flows. not_configured_action is deny, since the authentication flow
reaches the stage with the user only identified and no password stage in
front of it. Passkey-less users enroll from user settings after a
password login; default-authenticator-webauthn-setup has a
configure_flow, so that path is live.

The 8h cookie now also covers the login flow, so a re-login in the same
browser inside the window is satisfied by the cookie alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 09:25:52 +02:00