From 95f3ff86b9124ed8a7d31c4546586d0e87e8e1fd Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Fri, 23 Oct 2026 10:19:49 +0200 Subject: [PATCH 1/2] fix(authentik-gate-sync): refresh the embedded outpost every minute Each authentik-server replica's embedded outpost loses its websocket whenever gunicorn recycles the worker holding it (AUTHENTIK_WEB__MAX_REQUESTS). The reconnect backoff never resets (goauthentik/authentik#26463), so a replica then misses gate changes for up to 5 minutes and catches up only on the refresh interval. A 1-minute interval bounds that gap; worker recycling stays as is. Co-Authored-By: Claude Opus 5.5 --- ci/images/authentik-gate-sync/controller.py | 9 +++++++-- ci/images/authentik-gate-sync/test_controller.py | 5 +++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/ci/images/authentik-gate-sync/controller.py b/ci/images/authentik-gate-sync/controller.py index feddb0b7..4702075a 100644 --- a/ci/images/authentik-gate-sync/controller.py +++ b/ci/images/authentik-gate-sync/controller.py @@ -54,6 +54,7 @@ MANAGER = "authentik-gate-sync" MANAGED_BY = {"app.kubernetes.io/managed-by": MANAGER} GATE_LABEL = "auth.renner.uno/gate" EMBEDDED_OUTPOST = "goauthentik.io/outposts/embedded" +REFRESH_INTERVAL = "minutes=1" TRAEFIK = {"k8s:io.kubernetes.pod.namespace": INGRESS_NS, "app.kubernetes.io/name": "traefik"} MIDDLEWARES_ANN = "traefik.ingress.kubernetes.io/router.middlewares" @@ -212,9 +213,13 @@ def resolve_backend(gate, service): def embedded_config(config): """The embedded outpost's config with the host the gates depend on. The embedded outpost builds the sign-in redirect from authentik_host, so it is - the public URL, which is also Authentik's default for it.""" + the public URL, which is also Authentik's default for it. + + A replica whose outpost websocket is down picks up gate changes only on + the refresh interval; the reconnect backoff never resets + (goauthentik/authentik#26463), so that gap reaches 5 minutes.""" return {**(config or {}), "authentik_host": AK_BROWSER_URL, "authentik_host_insecure": False, - "authentik_host_browser": AK_BROWSER_URL} + "authentik_host_browser": AK_BROWSER_URL, "refresh_interval": REFRESH_INTERVAL} def provider_body(gate, flows): diff --git a/ci/images/authentik-gate-sync/test_controller.py b/ci/images/authentik-gate-sync/test_controller.py index 038db4f6..fda5fa25 100644 --- a/ci/images/authentik-gate-sync/test_controller.py +++ b/ci/images/authentik-gate-sync/test_controller.py @@ -167,9 +167,10 @@ class Objects(unittest.TestCase): self.assertEqual(c.drift({"a": 1, "b": 2}, {"a": 1, "b": 3}), {"b": 3}) def test_embedded_config_keeps_other_keys(self): - cfg = c.embedded_config({"log_level": "debug", "authentik_host": c.AK_URL}) + cfg = c.embedded_config({"log_level": "debug", "authentik_host": c.AK_URL, "refresh_interval": "minutes=5"}) self.assertEqual(cfg, {"log_level": "debug", "authentik_host": c.AK_BROWSER_URL, - "authentik_host_insecure": False, "authentik_host_browser": c.AK_BROWSER_URL}) + "authentik_host_insecure": False, "authentik_host_browser": c.AK_BROWSER_URL, + "refresh_interval": "minutes=1"}) class FakeAuthentik: -- 2.54.0 From 58196acebc0c319f36efc7e20485f0635fb0c1b1 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Fri, 23 Oct 2026 19:00:21 +0200 Subject: [PATCH 2/2] fix(authentik): embedded outpost config lives in the blueprint The embedded outpost's config is Authentik's own and does not depend on authentik-gate-sync. A blueprint update of an existing outpost is partial (importer: partial=True unless MUST_CREATED), so the entry sets config without providers and the gates the controller attached stay. config is replaced whole; the entry mirrors the live row (authentik_outposts_outpost, 2026-10-23) with refresh_interval minutes=1. kubernetes_namespace stays default: the outpost has the Local Kubernetes Cluster connection, and the authentik namespace would let the worker create an ak-outpost Service for it. The controller drops embedded_config/ensure_embedded_config and only attaches providers. Co-Authored-By: Claude Opus 5.5 --- ci/images/authentik-gate-sync/controller.py | 23 ------------ .../authentik-gate-sync/test_controller.py | 15 -------- .../apps/authentik/blueprints-configmap.yaml | 37 +++++++++++++++++-- kubernetes/apps/authentik/gate-sync.yaml | 2 - 4 files changed, 33 insertions(+), 44 deletions(-) diff --git a/ci/images/authentik-gate-sync/controller.py b/ci/images/authentik-gate-sync/controller.py index 4702075a..f81e50a0 100644 --- a/ci/images/authentik-gate-sync/controller.py +++ b/ci/images/authentik-gate-sync/controller.py @@ -45,7 +45,6 @@ INGRESS_NS = os.environ.get("INGRESS_NAMESPACE", "ingress") DEFAULT_AUTHZ_FLOW = os.environ.get("DEFAULT_AUTHORIZATION_FLOW", "default-provider-authorization-implicit-consent") INVALIDATION_FLOW = os.environ.get("INVALIDATION_FLOW", "default-provider-invalidation-flow") COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN", "renner.uno") -AK_BROWSER_URL = os.environ.get("AUTHENTIK_HOST_BROWSER", "https://auth.renner.uno/") INTERVAL = int(os.environ.get("INTERVAL_SECONDS", "60")) HTTP_TIMEOUT = 20 @@ -54,7 +53,6 @@ MANAGER = "authentik-gate-sync" MANAGED_BY = {"app.kubernetes.io/managed-by": MANAGER} GATE_LABEL = "auth.renner.uno/gate" EMBEDDED_OUTPOST = "goauthentik.io/outposts/embedded" -REFRESH_INTERVAL = "minutes=1" TRAEFIK = {"k8s:io.kubernetes.pod.namespace": INGRESS_NS, "app.kubernetes.io/name": "traefik"} MIDDLEWARES_ANN = "traefik.ingress.kubernetes.io/router.middlewares" @@ -210,18 +208,6 @@ def resolve_backend(gate, service): return sp["port"], selector, target -def embedded_config(config): - """The embedded outpost's config with the host the gates depend on. The - embedded outpost builds the sign-in redirect from authentik_host, so it is - the public URL, which is also Authentik's default for it. - - A replica whose outpost websocket is down picks up gate changes only on - the refresh interval; the reconnect backoff never resets - (goauthentik/authentik#26463), so that gap reaches 5 minutes.""" - return {**(config or {}), "authentik_host": AK_BROWSER_URL, "authentik_host_insecure": False, - "authentik_host_browser": AK_BROWSER_URL, "refresh_interval": REFRESH_INTERVAL} - - def provider_body(gate, flows): return { "name": gate["name"], @@ -354,14 +340,6 @@ def embedded_outpost(ak): return found[0] -def ensure_embedded_config(ak): - outpost = embedded_outpost(ak) - want = embedded_config(outpost.get("config")) - if outpost.get("config") != want: - ak.req("PATCH", f"outposts/instances/{outpost['pk']}/", {"config": want}) - LOG.info("embedded outpost: updated config") - - def ensure_gate(ak, gate, flows): name = gate["name"] if gate["group"] and ak.one("core/groups/", name=gate["group"]) is None: @@ -490,7 +468,6 @@ def reconcile(): sources = [i for i in k8s.get("/apis/networking.k8s.io/v1/ingresses").get("items", []) if is_gated(i)] shared = shared_hosts(sources) flows = {f["slug"]: f["pk"] for f in ak.list("flows/instances/")} - ensure_embedded_config(ak) desired = set() # (kind, ns, name) of every Kubernetes object some gate wants gates = set() # gate names whose Authentik side should exist diff --git a/ci/images/authentik-gate-sync/test_controller.py b/ci/images/authentik-gate-sync/test_controller.py index fda5fa25..3c70a520 100644 --- a/ci/images/authentik-gate-sync/test_controller.py +++ b/ci/images/authentik-gate-sync/test_controller.py @@ -166,12 +166,6 @@ class Objects(unittest.TestCase): def test_drift(self): self.assertEqual(c.drift({"a": 1, "b": 2}, {"a": 1, "b": 3}), {"b": 3}) - def test_embedded_config_keeps_other_keys(self): - cfg = c.embedded_config({"log_level": "debug", "authentik_host": c.AK_URL, "refresh_interval": "minutes=5"}) - self.assertEqual(cfg, {"log_level": "debug", "authentik_host": c.AK_BROWSER_URL, - "authentik_host_insecure": False, "authentik_host_browser": c.AK_BROWSER_URL, - "refresh_interval": "minutes=1"}) - class FakeAuthentik: """In-memory stand-in for the endpoints the controller uses; records calls.""" @@ -300,15 +294,6 @@ class Reconcile(unittest.TestCase): c.embedded_outpost(self.ak) self.assertEqual(self.ak.get(f"outposts/instances/{ldap['pk']}/")["providers"], [7]) - def test_embedded_config(self): - c.ensure_embedded_config(self.ak) - cfg = self.ak.get(f"outposts/instances/{self.embedded['pk']}/")["config"] - self.assertEqual((cfg["log_level"], cfg["authentik_host"], cfg["authentik_host_browser"]), - ("info", c.AK_BROWSER_URL, c.AK_BROWSER_URL)) - before = len(self.ak.calls) - c.ensure_embedded_config(self.ak) - self.assertEqual(self.ak.calls[before:], []) - def test_provider_moves_from_proxy_mode(self): self.ak.add("providers/proxy/", {"name": self.g["name"], "mode": "proxy", "internal_host": "http://llama-server.ai.svc.cluster.local:8080", diff --git a/kubernetes/apps/authentik/blueprints-configmap.yaml b/kubernetes/apps/authentik/blueprints-configmap.yaml index 54b3a8a0..0ec6ba93 100644 --- a/kubernetes/apps/authentik/blueprints-configmap.yaml +++ b/kubernetes/apps/authentik/blueprints-configmap.yaml @@ -374,10 +374,39 @@ data: name: lidarr-gate state: absent - # The embedded outpost is not declared here: authentik-gate-sync owns its - # providers list (every gate) and the hosts in its config, and a - # blueprint entry must carry `providers`, which would detach the gates - # on every apply. + # No `providers`: authentik-gate-sync attaches the gates, and an update + # of an existing outpost is partial. `config` is replaced whole, so it + # carries every key, kubernetes_namespace included. + - model: authentik_outposts.outpost + identifiers: + name: authentik Embedded Outpost + state: present + attrs: + config: + log_level: info + # The embedded outpost builds the sign-in redirect from + # authentik_host, so it is the public URL. + authentik_host: "https://auth.renner.uno/" + authentik_host_insecure: false + authentik_host_browser: "https://auth.renner.uno/" + # A replica whose outpost websocket is down picks up gate changes + # only on this interval; the reconnect backoff never resets + # (goauthentik/authentik#26463). + refresh_interval: "minutes=1" + object_naming_template: "ak-outpost-%(name)s" + container_image: null + docker_labels: null + docker_network: ladmin_default + docker_map_ports: true + kubernetes_namespace: default + kubernetes_replicas: 1 + kubernetes_service_type: ClusterIP + kubernetes_disabled_components: [] + kubernetes_image_pull_secrets: [] + kubernetes_ingress_annotations: {} + kubernetes_ingress_class_name: null + kubernetes_ingress_secret_name: "authentik-outpost-tls" + kubernetes_json_patches: null # Shared LDAPS cert for the LDAP Outpost. Authentik's data model # owns the cert per-LDAPProvider (no outpost-level cert field), so diff --git a/kubernetes/apps/authentik/gate-sync.yaml b/kubernetes/apps/authentik/gate-sync.yaml index ab6dc4e6..731466a6 100644 --- a/kubernetes/apps/authentik/gate-sync.yaml +++ b/kubernetes/apps/authentik/gate-sync.yaml @@ -114,8 +114,6 @@ spec: env: - name: AK_URL value: http://authentik-server.authentik.svc.cluster.local - - name: AUTHENTIK_HOST_BROWSER - value: https://auth.renner.uno/ - name: DEFAULT_AUTHORIZATION_FLOW value: default-provider-authorization-implicit-consent - name: INTERVAL_SECONDS -- 2.54.0