From 1ecba543aa01fecb0d24da2ee2af2581afe4e270 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Tue, 25 Aug 2026 20:49:04 +1000 Subject: [PATCH] arrstack: wire arrproxy v0.4.0 tier routing (fafflix/cheeztv) (#416) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why The old bare `sonarr`/`radarr` Services are gone, but arrproxy still ran the v0.3.x built-in default topology pointing at non-existent `sonarr`/`radarr` upstreams and mounted dead `sonarr-apikey`/`radarr-apikey` Secrets. This cuts the front door over to arrproxy v0.4.0 tier routing so the adult (fafflix, `/3aa168`) and kids (cheeztv, `/3df803`) *arr instances are reachable again, with per-tier group authorization. ## Changes - Bump `arrproxy-api` and `arrproxy-ui` images to `v0.4.0`. - Add `arrproxy-tiers` ConfigMap (`ARRPROXY_TIERS_FILE`): `fafflix` + `cheeztv` tiers, each with sonarr/radarr upstreams, route hash, key subdir and group grants. Adults (`akP-media-fafflix`) reach both tiers all methods; kids (`akP-media-cheeztv`) reach only cheeztv, GET/HEAD. `legacyRoutes:false` retires the unprefixed routes. - Because arrproxy strips `/` and proxies `//...`, each upstream URL carries the `/` path segment so the joined upstream path (`/3aa168/sonarr/...`) matches the *arr's own UrlBase. - Mount the tiers file and rework the projected keys volume to the real per-instance `{sonarr,radarr}-{adult,kids}-apikey` Secrets under `/` (`adult/*`, `kids/*`); refresh reloader annotations. - oauth2-proxy: route `/3aa168` and `/3df803` to arrproxy-api (drop dead `/sonarr` `/radarr` `/prowlarr`; prowlarr is served directly), and widen `SKIP_AUTH_REGEX` to `^/[^/]+/[^/]+/api` for the hash-prefixed tier api paths. Group headers keep flowing via the existing Authentik `ak_groups` claim -> oauth2-proxy `X-Forwarded-Groups` mechanism. Validated with `kustomize build` + repo kubeconform (88/88 valid). No new Vault seeds required — the per-instance apikey Secrets already exist. Reviewed-on: https://git.unkin.net/unkin/argocd-apps/pulls/416 Co-authored-by: unkin-agent Co-committed-by: unkin-agent --- .../arrstack/arrproxy/api-deployment.yaml | 49 ++++++++++++----- .../base/arrstack/arrproxy/kustomization.yaml | 1 + .../arrproxy/oauth2-proxy-configmap.yaml | 26 +++++---- .../arrstack/arrproxy/tiers-configmap.yaml | 53 +++++++++++++++++++ .../base/arrstack/arrproxy/ui-deployment.yaml | 2 +- 5 files changed, 105 insertions(+), 26 deletions(-) create mode 100644 apps/base/arrstack/arrproxy/tiers-configmap.yaml diff --git a/apps/base/arrstack/arrproxy/api-deployment.yaml b/apps/base/arrstack/arrproxy/api-deployment.yaml index bf3ba82..3125c94 100644 --- a/apps/base/arrstack/arrproxy/api-deployment.yaml +++ b/apps/base/arrstack/arrproxy/api-deployment.yaml @@ -7,7 +7,8 @@ metadata: annotations: # Wave 2: serve only after the wave-1 migrate Job completes. argocd.argoproj.io/sync-wave: "2" - secret.reloader.stakater.com/reload: "arrproxy-pepper,arrproxy-admin-token,arrproxy-db-app,sonarr-apikey,radarr-apikey,prowlarr-apikey" + secret.reloader.stakater.com/reload: "arrproxy-pepper,arrproxy-admin-token,arrproxy-db-app,sonarr-adult-apikey,radarr-adult-apikey,sonarr-kids-apikey,radarr-kids-apikey" + configmap.reloader.stakater.com/reload: "arrproxy-tiers" spec: replicas: 2 selector: @@ -33,7 +34,7 @@ spec: type: RuntimeDefault containers: - name: api - image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/arrproxy-api:v0.3.1 + image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/arrproxy-api:v0.4.0 imagePullPolicy: IfNotPresent ports: - containerPort: 8080 @@ -49,11 +50,18 @@ spec: # reaches this upstream) so group-based authorization works. - name: ARRPROXY_GROUPS_HEADER value: X-Forwarded-Groups - # Real per-app *arr keys, projected one file per app under this dir - # (sourced from the existing -apikey Secrets). The api injects - # them server-side and redacts them from every proxied response. + # Real per-app *arr keys, projected one file per app under a per-tier + # subdir (/) matching the tiers file: adult/{sonarr,radarr} + # and kids/{sonarr,radarr}. Sourced from the same -apikey + # Secrets the *arr Deployments consume. The api injects them server-side + # and redacts them from every proxied response. - name: ARRPROXY_KEYS_DIR value: /etc/arrproxy/keys + # Tier topology (fafflix/cheeztv): upstreams, route hashes, key subdirs + # and group grants. Legacy unprefixed routes are retired in the file + # (legacyRoutes:false); see arrproxy-tiers ConfigMap. + - name: ARRPROXY_TIERS_FILE + value: /etc/arrproxy/tiers/tiers.json - name: ARRPROXY_PEPPER valueFrom: secretKeyRef: @@ -86,6 +94,9 @@ spec: - name: arr-keys mountPath: /etc/arrproxy/keys readOnly: true + - name: tiers + mountPath: /etc/arrproxy/tiers + readOnly: true - name: tmp mountPath: /tmp livenessProbe: @@ -118,27 +129,37 @@ spec: cpu: "1" memory: 512Mi volumes: - # Real *arr API keys, one file per app named exactly so the api - # reads /etc/arrproxy/keys/{sonarr,radarr,prowlarr}. Reuses the same - # -apikey Secrets the *arr Deployments already consume. + # Real per-tier *arr API keys, projected as / so the api + # reads /etc/arrproxy/keys/{adult,kids}/{sonarr,radarr} (matching the + # tiers file keyDir). Reuses the same -apikey Secrets the *arr + # Deployments already consume (seeded by their VaultStaticSecrets). - name: arr-keys projected: sources: - secret: - name: sonarr-apikey + name: sonarr-adult-apikey items: - key: apitoken - path: sonarr + path: adult/sonarr - secret: - name: radarr-apikey + name: radarr-adult-apikey items: - key: apitoken - path: radarr + path: adult/radarr - secret: - name: prowlarr-apikey + name: sonarr-kids-apikey items: - key: apitoken - path: prowlarr + path: kids/sonarr + - secret: + name: radarr-kids-apikey + items: + - key: apitoken + path: kids/radarr + # Tier topology JSON (ARRPROXY_TIERS_FILE). + - name: tiers + configMap: + name: arrproxy-tiers - name: tmp emptyDir: sizeLimit: 64Mi diff --git a/apps/base/arrstack/arrproxy/kustomization.yaml b/apps/base/arrstack/arrproxy/kustomization.yaml index 76b6cd4..778f2d2 100644 --- a/apps/base/arrstack/arrproxy/kustomization.yaml +++ b/apps/base/arrstack/arrproxy/kustomization.yaml @@ -8,6 +8,7 @@ resources: - migrations-configmap.yaml - migrate-job.yaml - vaultstaticsecret.yaml + - tiers-configmap.yaml - oauth2-proxy-configmap.yaml - oauth2-proxy-deployment.yaml - api-deployment.yaml diff --git a/apps/base/arrstack/arrproxy/oauth2-proxy-configmap.yaml b/apps/base/arrstack/arrproxy/oauth2-proxy-configmap.yaml index 8cae392..d9dca5b 100644 --- a/apps/base/arrstack/arrproxy/oauth2-proxy-configmap.yaml +++ b/apps/base/arrstack/arrproxy/oauth2-proxy-configmap.yaml @@ -21,9 +21,11 @@ data: # application (terraform-authentik PR #18). OAUTH2_PROXY_OIDC_ISSUER_URL: "https://identity.unkin.net/application/o/arrstack/" OAUTH2_PROXY_REDIRECT_URL: "https://arrstack.unkin.net/oauth2/callback" - # Longest-prefix wins: /api and / go to arrproxy-api, everything else - # (the SPA + static assets) to arrproxy-ui. - OAUTH2_PROXY_UPSTREAMS: "http://arrproxy-ui.arrstack.svc.cluster.local:8080/,http://arrproxy-api.arrstack.svc.cluster.local:8080/api/,http://arrproxy-api.arrstack.svc.cluster.local:8080/sonarr/,http://arrproxy-api.arrstack.svc.cluster.local:8080/radarr/,http://arrproxy-api.arrstack.svc.cluster.local:8080/prowlarr/" + # Longest-prefix wins: /api and the per-tier hash routes (/3aa168, /3df803) go + # to arrproxy-api, everything else (the SPA + static assets) to arrproxy-ui. + # Tier routes carry ///...; the bare /sonarr /radarr /prowlarr + # upstreams are retired (legacy routes off; prowlarr is served directly). + OAUTH2_PROXY_UPSTREAMS: "http://arrproxy-ui.arrstack.svc.cluster.local:8080/,http://arrproxy-api.arrstack.svc.cluster.local:8080/api/,http://arrproxy-api.arrstack.svc.cluster.local:8080/3aa168/,http://arrproxy-api.arrstack.svc.cluster.local:8080/3df803/" OAUTH2_PROXY_SCOPE: "openid email profile ak_groups" # Populate session.Groups from the Authentik ak_groups claim; pass-user-headers # then emits it as a single comma-joined X-Forwarded-Groups header. @@ -33,14 +35,16 @@ data: # set-xauthrequest is intentionally NOT set -- it only populates auth_request # *response* headers, which never reach an --upstreams-proxied backend. OAUTH2_PROXY_PASS_USER_HEADERS: "true" - # Bypass auth for the *arr proxy API (//api...) and the machine-mint - # admin route (/api/admin/...). The first matches /sonarr/api; the second - # matches /api/admin/ only -- both routed to the arrproxy-api upstream by the - # catch-all /api/ prefix above. /api/admin/ is protected by arrproxy's OWN - # ARRPROXY_ADMIN_TOKEN bearer (OpenBao on the VMs reaches it via the ingress), - # so it is intentionally oauth-skipped. /api/tokens and /api/me are NOT - # matched and stay oauth-authenticated. - OAUTH2_PROXY_SKIP_AUTH_REGEX: "^/[^/]+/api,^/api/admin/" + # Bypass auth for the tier *arr proxy API (///api...) and the + # machine-mint admin route (/api/admin/...). The first matches + # /3aa168/sonarr/api (two path segments before /api, since tier routes are + # hash-prefixed); the second matches /api/admin/ only -- both routed to the + # arrproxy-api upstream by the tier and catch-all /api/ prefixes above. + # /api/admin/ is protected by arrproxy's OWN ARRPROXY_ADMIN_TOKEN bearer + # (OpenBao on the VMs reaches it via the ingress), so it is intentionally + # oauth-skipped. /api/tokens and /api/me are NOT matched and stay + # oauth-authenticated. + OAUTH2_PROXY_SKIP_AUTH_REGEX: "^/[^/]+/[^/]+/api,^/api/admin/" OAUTH2_PROXY_EMAIL_DOMAINS: "*" # Authentik hardcodes email_verified=false in the id_token; without this # oauth2-proxy rejects the session ("email ... isn't verified") -> 500 on diff --git a/apps/base/arrstack/arrproxy/tiers-configmap.yaml b/apps/base/arrstack/arrproxy/tiers-configmap.yaml new file mode 100644 index 0000000..96bf54d --- /dev/null +++ b/apps/base/arrstack/arrproxy/tiers-configmap.yaml @@ -0,0 +1,53 @@ +--- +# arrproxy v0.4.0 tier topology (ARRPROXY_TIERS_FILE). Two isolated instance +# tiers, each with its own *arr backends, real-key subdir, and group grants: +# fafflix (adult, /3aa168/*) and cheeztv (kids, /3df803/*). +# arrproxy strips the / route prefix and proxies "//..." upstream, so +# each app's upstream URL carries the "/" path segment: the joined upstream +# path (/3aa168/sonarr/...) then matches the *arr's own UrlBase (/3aa168/sonarr). +# Access: the adult group reaches BOTH tiers with any method; the kids group +# reaches ONLY the cheeztv tier and only GET/HEAD. Groups arrive on the +# hierarchical Authentik ak_groups claim, forwarded by oauth2-proxy as +# X-Forwarded-Groups. Legacy unprefixed routes are retired (legacyRoutes:false); +# the old bare sonarr/radarr Services no longer exist. +apiVersion: v1 +kind: ConfigMap +metadata: + name: arrproxy-tiers + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "2" +data: + tiers.json: | + { + "legacyTier": "fafflix", + "legacyRoutes": false, + "tiers": [ + { + "name": "fafflix", + "hash": "3aa168", + "keyDir": "adult", + "grants": [ + { "group": "akP-media-fafflix" } + ], + "apps": [ + { "name": "sonarr", "upstream": "http://sonarr-adult.arrstack.svc.cluster.local:8989/3aa168", "urlBase": "/3aa168/sonarr" }, + { "name": "radarr", "upstream": "http://radarr-adult.arrstack.svc.cluster.local:7878/3aa168", "urlBase": "/3aa168/radarr" } + ] + }, + { + "name": "cheeztv", + "hash": "3df803", + "keyDir": "kids", + "readOnly": true, + "grants": [ + { "group": "akP-media-fafflix" }, + { "group": "akP-media-cheeztv", "methods": ["GET", "HEAD"] } + ], + "apps": [ + { "name": "sonarr", "upstream": "http://sonarr-kids.arrstack.svc.cluster.local:8989/3df803", "urlBase": "/3df803/sonarr" }, + { "name": "radarr", "upstream": "http://radarr-kids.arrstack.svc.cluster.local:7878/3df803", "urlBase": "/3df803/radarr" } + ] + } + ] + } diff --git a/apps/base/arrstack/arrproxy/ui-deployment.yaml b/apps/base/arrstack/arrproxy/ui-deployment.yaml index 325ade5..04d32cb 100644 --- a/apps/base/arrstack/arrproxy/ui-deployment.yaml +++ b/apps/base/arrstack/arrproxy/ui-deployment.yaml @@ -31,7 +31,7 @@ spec: type: RuntimeDefault containers: - name: ui - image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/arrproxy-ui:v0.3.1 + image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/arrproxy-ui:v0.4.0 imagePullPolicy: IfNotPresent ports: - containerPort: 8080