fire-planner / k8s-portal / insta2spotify: revert auth=public to auth=none

The Phase 4 audit promoted three "smoke-test candidates" from `protected = false`
to `auth = "public"`, but all three are XHR / curl-driven endpoints (fetch()
calls, automation scripts) that don't survive the 302+cookie redirect dance
that the public-auto-login flow requires on first visit. fire-planner's SPA
broke immediately — every fetch() to /api/* hit a cross-origin redirect and
CORS preflight rejected it.

Important learning for the `auth = "public"` design:

  `auth = "public"` is functionally equivalent to a normal Authentik forward-auth
  for the FIRST request — it issues a 302 to authentik to set a guest session
  cookie, then 302s back. This is invisible for top-level browser navigation
  but BREAKS:
    - XHR/fetch() under CORS preflight (preflight rejects redirects)
    - curl/automation scripts that don't preserve cookies across requests
    - Mobile / native clients that can't follow OAuth-style redirects

  Use `auth = "public"` only for top-level HTML pages where the user navigates
  via the browser address bar (or links). For XHR APIs, native-client surfaces,
  webhooks, OAuth callbacks — use `auth = "none"`.

The plan's "smoke test 3 candidates" were misjudged on this front. Reverting
all three to `auth = "none"` (their previous behaviour). The end-to-end public
flow IS verified working via curl + flow API — the design is sound, just the
test targets were wrong.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-05-10 18:59:31 +00:00
parent faad99cff3
commit 09f83b4e83
3 changed files with 17 additions and 10 deletions

View file

@ -444,11 +444,14 @@ module "ingress" {
# Second ingress at the same host for the /api/ prefix WITHOUT Authentik # Second ingress at the same host for the /api/ prefix WITHOUT Authentik
# forward-auth. The SPA loads under Authentik (main ingress at /), then its # forward-auth. The SPA loads under Authentik (main ingress at /), then its
# fetch() XHRs hit /api/* directly forward-auth on /api/* would 302 the # fetch() XHRs hit /api/* directly ANY forward-auth here (required OR
# XHR to a cross-origin Authentik login page, which fetch().json() can't # public-tier auto-bind) would 302 the XHR to a cross-origin Authentik
# parse. App-layer bearer auth still gates writes (POST/PATCH/DELETE on # login page, which fetch() rejects under CORS preflight rules. Even the
# scenarios, /recompute, /simulate); read endpoints are open. Acceptable # `auth = "public"` flow needs a 302+cookie dance on first visit to set
# for a personal tool whose only data is anonymous numeric projections. # the guest session cookie, so it doesn't help XHR APIs. App-layer bearer
# auth still gates writes (POST/PATCH/DELETE on scenarios, /recompute,
# /simulate); read endpoints are open. Acceptable for a personal tool
# whose only data is anonymous numeric projections.
module "ingress_api" { module "ingress_api" {
source = "../../modules/kubernetes/ingress_factory" source = "../../modules/kubernetes/ingress_factory"
dns_type = "none" dns_type = "none"
@ -459,7 +462,7 @@ module "ingress_api" {
port = 8080 port = 8080
ingress_path = ["/api/"] ingress_path = ["/api/"]
tls_secret_name = var.tls_secret_name tls_secret_name = var.tls_secret_name
auth = "public" auth = "none"
} }
# Plan-time read of the ESO-created K8s Secret for Grafana datasource # Plan-time read of the ESO-created K8s Secret for Grafana datasource

View file

@ -248,7 +248,9 @@ module "ingress" {
} }
} }
# API ingress unprotected (API key auth handled by backend) # API ingress unprotected (API key auth handled by backend). XHR-based
# endpoints; `auth = "public"` would 302+cookie-dance and break CORS
# preflight, so we stay at `auth = "none"`.
module "ingress_api" { module "ingress_api" {
source = "../../modules/kubernetes/ingress_factory" source = "../../modules/kubernetes/ingress_factory"
namespace = kubernetes_namespace.insta2spotify.metadata[0].name namespace = kubernetes_namespace.insta2spotify.metadata[0].name
@ -256,7 +258,7 @@ module "ingress_api" {
host = "insta2spotify" host = "insta2spotify"
service_name = "insta2spotify" service_name = "insta2spotify"
tls_secret_name = var.tls_secret_name tls_secret_name = var.tls_secret_name
auth = "public" auth = "none"
ingress_path = ["/api/identify", "/api/auth", "/api/health", "/api/history"] ingress_path = ["/api/identify", "/api/auth", "/api/health", "/api/history"]
max_body_size = "50m" max_body_size = "50m"
} }

View file

@ -159,7 +159,9 @@ module "ingress" {
} }
} }
# Unprotected ingress for the setup script and agent endpoint (needs to be curl-able without auth) # Unprotected ingress for the setup script and agent endpoint (needs to be
# curl-able without auth). `auth = "public"` would 302+cookie-dance on
# first visit, breaking automation that doesn't preserve cookies.
module "ingress_setup_script" { module "ingress_setup_script" {
source = "../../../../modules/kubernetes/ingress_factory" source = "../../../../modules/kubernetes/ingress_factory"
namespace = kubernetes_namespace.k8s_portal.metadata[0].name namespace = kubernetes_namespace.k8s_portal.metadata[0].name
@ -168,5 +170,5 @@ module "ingress_setup_script" {
service_name = "k8s-portal" service_name = "k8s-portal"
ingress_path = ["/setup/script", "/agent"] ingress_path = ["/setup/script", "/agent"]
tls_secret_name = var.tls_secret_name tls_secret_name = var.tls_secret_name
auth = "public" auth = "none"
} }