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