From 922f91cb7d67bcddb446e7551f8d3746437073bb Mon Sep 17 00:00:00 2001 From: atlas Date: Thu, 27 Aug 2026 11:15:52 +0200 Subject: [PATCH] swarm-authelia: stop answering machine callers with a 200 error page The authelia vhost intercepts upstream errors and serves a friendly "SSO unavailable" page. The `=` form of `error_page` takes its status from the redirected location, and that location serves a file -- so the page is returned as **200**. That is right for a human typing the URL and wrong for every machine caller, all of which reach authelia through this same vhost by name: - `/api/authz/auth-request` -- nginx `auth_request` treats any 2xx as success, so a down authelia means access GRANTED - `/api/oidc/introspection` -- a token check answering 200 - `/api/oidc/token`, `/.well-known/openid-configuration` -- clients parsing an HTML error page as their JSON document Routes `/api/` and `/.well-known/` without the interception. A longer prefix wins over `/`, and the intercept directives live inside the `/` location rather than at server level, so they do not reach the new ones. Split by AUDIENCE rather than by an enumerated path list: a human still gets the page, and every machine caller -- including the login page's own XHR, and any endpoint added later -- gets the real status. Measured against a real nginx with a dead upstream, both arms: machine paths return 502 where they returned 200+HTML, a subrequest through the new prefix denies (matching a direct port dial) where through `/` it served the protected content, and the browser control confirms the friendly page survives. URI preservation checked separately against a live echo upstream -- `proxy_pass` with no URI part passes the full original path. --- nix/host-modules/swarm-authelia.nix | 35 +++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index 86d844f8..dfbb51ab 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -1073,6 +1073,41 @@ in default_type text/html; ''; }; + + # Authelia's MACHINE surface, routed without the error interception + # above. A longer prefix wins over `/` in nginx, and + # `proxy_intercept_errors` / `error_page` are set inside that + # location rather than at server level, so they do not reach here. + # + # ⚠️ THIS IS A SECURITY BOUNDARY, not a tidy-up. `error_page … = + # /__hive_sso_unavailable` takes its status from the redirected + # location, which serves a FILE — so it answers **200**. Every + # machine caller then receives a success carrying an HTML body + # instead of the 502 that actually happened: + # + # - `/api/authz/auth-request` — nginx `auth_request` treats any + # 2xx as success, so a down authelia would mean ACCESS GRANTED + # - `/api/oidc/introspection` — a token check answering 200 + # - `/api/oidc/token`, `/.well-known/openid-configuration` — a + # client parsing an error page as its JSON document + # + # Measured with both controls rather than reasoned: against a dead + # upstream a subrequest through `/` serves the protected content, + # and through this prefix it denies, matching a direct dial. The + # browser arm confirms `/` still serves the friendly page — a fix + # that silently deleted it would pass the deny check alone. + # + # The split is by AUDIENCE, not by path list: a human typing the + # URL gets a page explaining that SSO is down, and every `/api/` + # and `/.well-known/` caller — including the login page's own XHR — + # gets the status. Enumerating individual endpoints would leave the + # next one added silently intercepted. + locations."/api/" = { + proxyPass = "http://127.0.0.1:${toString cfg.port}"; + }; + locations."/.well-known/" = { + proxyPass = "http://127.0.0.1:${toString cfg.port}"; + }; }; # Order the container after the host CA generator, so the bind source