From 09f83b4e831cacacdc4c43fba5467960c1bc759d Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sun, 10 May 2026 18:59:31 +0000 Subject: [PATCH] fire-planner / k8s-portal / insta2spotify: revert auth=public to auth=none MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- stacks/fire-planner/main.tf | 15 +++++++++------ stacks/insta2spotify/main.tf | 6 ++++-- stacks/k8s-portal/modules/k8s-portal/main.tf | 6 ++++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/stacks/fire-planner/main.tf b/stacks/fire-planner/main.tf index 9cd80219..93ec91f3 100644 --- a/stacks/fire-planner/main.tf +++ b/stacks/fire-planner/main.tf @@ -444,11 +444,14 @@ module "ingress" { # Second ingress at the same host for the /api/ prefix WITHOUT Authentik # forward-auth. The SPA loads under Authentik (main ingress at /), then its -# fetch() XHRs hit /api/* directly — forward-auth on /api/* would 302 the -# XHR to a cross-origin Authentik login page, which fetch().json() can't -# parse. 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. +# fetch() XHRs hit /api/* directly — ANY forward-auth here (required OR +# public-tier auto-bind) would 302 the XHR to a cross-origin Authentik +# login page, which fetch() rejects under CORS preflight rules. Even the +# `auth = "public"` flow needs a 302+cookie dance on first visit to set +# 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" { source = "../../modules/kubernetes/ingress_factory" dns_type = "none" @@ -459,7 +462,7 @@ module "ingress_api" { port = 8080 ingress_path = ["/api/"] tls_secret_name = var.tls_secret_name - auth = "public" + auth = "none" } # Plan-time read of the ESO-created K8s Secret for Grafana datasource diff --git a/stacks/insta2spotify/main.tf b/stacks/insta2spotify/main.tf index d671cbf0..a1bc3b0e 100644 --- a/stacks/insta2spotify/main.tf +++ b/stacks/insta2spotify/main.tf @@ -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" { source = "../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.insta2spotify.metadata[0].name @@ -256,7 +258,7 @@ module "ingress_api" { host = "insta2spotify" service_name = "insta2spotify" tls_secret_name = var.tls_secret_name - auth = "public" + auth = "none" ingress_path = ["/api/identify", "/api/auth", "/api/health", "/api/history"] max_body_size = "50m" } diff --git a/stacks/k8s-portal/modules/k8s-portal/main.tf b/stacks/k8s-portal/modules/k8s-portal/main.tf index 44ae778d..c40bfb8e 100644 --- a/stacks/k8s-portal/modules/k8s-portal/main.tf +++ b/stacks/k8s-portal/modules/k8s-portal/main.tf @@ -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" { source = "../../../../modules/kubernetes/ingress_factory" namespace = kubernetes_namespace.k8s_portal.metadata[0].name @@ -168,5 +170,5 @@ module "ingress_setup_script" { service_name = "k8s-portal" ingress_path = ["/setup/script", "/agent"] tls_secret_name = var.tls_secret_name - auth = "public" + auth = "none" }