A drifted proxy provider was PATCHed with only the drifted fields.
ProxyProviderSerializer.validate (providers/proxy/api.py, 2026.8.3)
checks internal_host against mode from the request attrs alone,
defaulting mode to proxy, so a flow-only PATCH fails with 400
"Internal host cannot be empty when forward auth is disabled." Seen
live after #1285: 8 gates could not move to the new flows and stayed on
their old ones.
The fake Authentik in the tests now applies the same validation, and a
test covers a flow change on an existing gate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
The remaining backed-up clusters still declared bootstrap.initdb, so a
cold rebuild came up with empty Home Assistant, Matrix, Paperclip and
TRMNL databases. Each now declares bootstrap.recovery from its
ObjectStore (serverName = cluster name). mx-pg drops its initdb
encoding/locale settings: the physical restore keeps the C collation.
RECOVERY.md lists all seven clusters; gf-pg has no ObjectStore and stays
on initdb.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Same change as ak-pg (#1281): im-pg and pprl-pg declared
bootstrap.initdb, so a cold rebuild came up with empty Immich and
Paperless databases despite 30 days of backups. Both now declare
bootstrap.recovery from their ObjectStore (serverName = cluster name).
bootstrap is honoured only when PGDATA is absent, so the live clusters
are unaffected.
RECOVERY.md drops the manual initdb->recovery swap and revert steps:
all three clusters it covers now restore on their own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bot has no cluster access, so any PR citing live-cluster evidence
gets a ⚪ Unverified section and could never be approved (e.g. #1281).
Approval now requires only no 🔴/🟡 section, matching the review banner
("approves only when it found nothing red or yellow"); the prompt's
verdict rule is aligned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
All wiki interaction now goes through the wiki service API; CI agents that
distill and consolidate by writing the notes repo directly are retired.
Removes .woodpecker/wiki-maintenance.yml and ci/agents/wiki-maintenance/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drops the per-step resources from the previous commit and removes every
depends_on: [], so Woodpecker runs the kaniko steps serially in file order.
A full rebuild then holds one step pod at a time and cannot exceed the
woodpecker-ci-quota.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A change to build-ci-images.yml starts all 14 kaniko steps at once. With
the LimitRange default of 2 CPU each they asked for ~28 CPU in limits
against the 24-CPU woodpecker-ci-quota, so whichever pods were created last
were rejected ("exceeded quota: woodpecker-ci-quota"): alpine-jq and
cni-plugins in pipeline 13723, vmtest, tofu and secret-rotator in 13726.
Every step now sets its resources from 30 days of Prometheus data (peak
working set / CPU per step):
- ansible, k8s, vmtest, tofu (peaks <=983Mi, <=0.54 cores): 1 CPU, 2Gi
- secret-rotator, review-bot (Go builds, ~2 cores): 2 CPU, 1Gi
- the remaining eight (<=271Mi, <=0.45 cores): 1 CPU, 1Gi
Requests stay at the LimitRange defaults (100m / 256Mi).
Total 16 CPU / 18Gi in limits, leaving room in the quota for a concurrent
PR check. The quota itself is unchanged; its comment no longer claims the
workflow runs 4 pods.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
build-authentik-gate-sync depended on a separate test step, so it only
started after the other steps had run, and Woodpecker skips not-yet-started
steps once the workflow has a failure: on the #1272 merge build-alpine-jq and
build-cni-plugins failed and the image was never built.
The unit tests now run as a stage of the image build (the final stage copies
the controller out of it), and the step starts with depends_on: [] like the
others.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
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>
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>