diff --git a/apps/base/arrstack/arrproxy/api-deployment.yaml b/apps/base/arrstack/arrproxy/api-deployment.yaml new file mode 100644 index 0000000..17228b8 --- /dev/null +++ b/apps/base/arrstack/arrproxy/api-deployment.yaml @@ -0,0 +1,136 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: arrproxy-api + namespace: arrstack + 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-db-app,sonarr-apikey,radarr-apikey,prowlarr-apikey" +spec: + replicas: 2 + selector: + matchLabels: + app: arrproxy-api + strategy: + rollingUpdate: + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: arrproxy-api + spec: + serviceAccountName: default + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: api + image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/arrproxy-api:v0.1.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 + name: http + protocol: TCP + env: + - name: ARRPROXY_ADDR + value: ":8080" + # oauth2-proxy --pass-user-headers forwards identity to the upstream as + # X-Forwarded-{User,Email,Groups} (a single comma-joined Groups value). + # Email/User already match the api defaults; override the groups header + # (default X-Auth-Request-Groups is auth_request-response-only and never + # 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. + - name: ARRPROXY_KEYS_DIR + value: /etc/arrproxy/keys + - name: ARRPROXY_PEPPER + valueFrom: + secretKeyRef: + name: arrproxy-pepper + key: pepper + # DSN assembled from the CNPG-generated arrproxy-db-app Secret; + # $(VAR) expansion resolves the two env entries defined above it. + - name: ARRPROXY_DB_USER + valueFrom: + secretKeyRef: + name: arrproxy-db-app + key: username + - name: ARRPROXY_DB_PASSWORD + valueFrom: + secretKeyRef: + name: arrproxy-db-app + key: password + - name: DATABASE_URL + value: "postgres://$(ARRPROXY_DB_USER):$(ARRPROXY_DB_PASSWORD)@arrproxy-db-rw.arrstack.svc.cluster.local:5432/arrproxy?sslmode=require" + volumeMounts: + - name: arr-keys + mountPath: /etc/arrproxy/keys + readOnly: true + - name: tmp + mountPath: /tmp + livenessProbe: + httpGet: + path: /livez + port: http + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /readyz + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + 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. + - name: arr-keys + projected: + sources: + - secret: + name: sonarr-apikey + items: + - key: apitoken + path: sonarr + - secret: + name: radarr-apikey + items: + - key: apitoken + path: radarr + - secret: + name: prowlarr-apikey + items: + - key: apitoken + path: prowlarr + - name: tmp + emptyDir: + sizeLimit: 64Mi + restartPolicy: Always diff --git a/apps/base/arrstack/arrproxy/cnpg_backup.yaml b/apps/base/arrstack/arrproxy/cnpg_backup.yaml new file mode 100644 index 0000000..624fa64 --- /dev/null +++ b/apps/base/arrstack/arrproxy/cnpg_backup.yaml @@ -0,0 +1,51 @@ +--- +# Ceph RGW (S3) backup target for the arrproxy token-store CNPG cluster, +# provisioned by the in-estate cephrgw-operator. One dedicated bucket + owner +# user per cluster. +apiVersion: ceph.unkin.net/v1alpha1 +kind: ObjectStoreUser +metadata: + name: cnpg-arrproxy-backup + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + displayName: "CNPG backup owner (arrproxy)" + uid: cnpg-arrproxy-backup + maxBuckets: 5 + secretName: cnpg-arrproxy-backup-s3 + retainOnDelete: true +--- +apiVersion: ceph.unkin.net/v1alpha1 +kind: Bucket +metadata: + name: cnpg-arrproxy + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + placementTarget: ec + bucketName: cnpg-arrproxy + ownerRef: cnpg-arrproxy-backup + versioning: false + tags: + app: arrproxy + purpose: cnpg-backup + retainOnDelete: true +--- +# Nightly base backup; continuous WAL archiving is always-on via the Cluster's +# spec.backup.barmanObjectStore. Staggered off other clusters' schedules. +apiVersion: postgresql.cnpg.io/v1 +kind: ScheduledBackup +metadata: + name: cnpg-arrproxy-nightly + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + schedule: "0 20 2 * * *" + immediate: false + backupOwnerReference: self + method: barmanObjectStore + cluster: + name: arrproxy-db diff --git a/apps/base/arrstack/arrproxy/cnpg_cluster.yaml b/apps/base/arrstack/arrproxy/cnpg_cluster.yaml new file mode 100644 index 0000000..0f9cc42 --- /dev/null +++ b/apps/base/arrstack/arrproxy/cnpg_cluster.yaml @@ -0,0 +1,117 @@ +--- +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: arrproxy-db + namespace: arrstack + annotations: + # Wave 0: DB (and the generated arrproxy-db-app Secret) must be Healthy before + # the wave-1 migrate Job runs. ArgoCD gates on the Cluster's health status. + argocd.argoproj.io/sync-wave: "0" +spec: + affinity: + podAntiAffinityType: preferred + backup: + retentionPolicy: 30d + barmanObjectStore: + destinationPath: s3://cnpg-arrproxy + endpointURL: https://s3.ceph.unkin.net + endpointCA: + name: vault-ca-cert + key: ca.crt + s3Credentials: + accessKeyId: + name: cnpg-arrproxy-backup-s3 + key: AWS_ACCESS_KEY_ID + secretAccessKey: + name: cnpg-arrproxy-backup-s3 + key: AWS_SECRET_ACCESS_KEY + serverName: arrproxy + data: + compression: bzip2 + jobs: 2 + wal: + compression: zstd + maxParallel: 2 + bootstrap: + initdb: + # No secret ref: CNPG mints the owner credentials and publishes them in the + # generated "arrproxy-db-app" Secret, which the api reads to build the DSN. + database: arrproxy + encoding: UTF8 + localeCType: C + localeCollate: C + owner: arrproxy + enablePDB: true + enableSuperuserAccess: false + failoverDelay: 0 + imageName: ghcr.io/cloudnative-pg/postgresql:18.1-system-trixie + instances: 2 + logLevel: info + maxSyncReplicas: 0 + minSyncReplicas: 0 + monitoring: + customQueriesConfigMap: + - key: queries + name: cnpg-default-monitoring + disableDefaultQueries: false + enablePodMonitor: false + postgresql: + parameters: + archive_mode: "on" + archive_timeout: 5min + dynamic_shared_memory_type: posix + effective_cache_size: 256MB + full_page_writes: "on" + log_destination: csvlog + log_directory: /controller/log + log_filename: postgres + log_rotation_age: "0" + log_rotation_size: "0" + log_truncate_on_rotation: "false" + logging_collector: "on" + max_connections: "200" + max_parallel_workers: "16" + max_replication_slots: "16" + max_worker_processes: "16" + shared_buffers: 128MB + shared_memory_type: mmap + ssl_max_protocol_version: TLSv1.3 + ssl_min_protocol_version: TLSv1.3 + wal_keep_size: 256MB + wal_level: logical + wal_log_hints: "on" + wal_receiver_timeout: 5s + wal_sender_timeout: 5s + syncReplicaElectionConstraint: + enabled: false + primaryUpdateMethod: restart + primaryUpdateStrategy: unsupervised + probes: + liveness: + isolationCheck: + connectionTimeout: 1000 + enabled: true + requestTimeout: 1000 + replicationSlots: + highAvailability: + enabled: true + slotPrefix: _cnpg_ + synchronizeReplicas: + enabled: true + updateInterval: 30 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 50m + memory: 256Mi + smartShutdownTimeout: 180 + startDelay: 3600 + stopDelay: 1800 + storage: + resizeInUseVolumes: true + size: 10Gi + storageClass: cephrbd-fast-delete + switchoverDelay: 3600 diff --git a/apps/base/arrstack/arrproxy/gateway.yaml b/apps/base/arrstack/arrproxy/gateway.yaml new file mode 100644 index 0000000..b223d2d --- /dev/null +++ b/apps/base/arrstack/arrproxy/gateway.yaml @@ -0,0 +1,41 @@ +--- +# External (DMZ) front for the arrstack, served on arrstack.unkin.net via the +# external Traefik (LB VIP 198.18.199.0). cert-manager mints arrproxy-gateway-tls +# (CN arrstack.unkin.net) off the internal Vault-PKI CA. The apex arrstack.unkin.net +# A record lives in the bind-operator unkin.net zone (bind-internal/authoritative), +# NOT external-dns, so no external-dns annotation here. +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + labels: + traefik.io/instance: external + annotations: + argocd.argoproj.io/sync-wave: "2" + cert-manager.io/cluster-issuer: vault-issuer + cert-manager.io/common-name: arrstack.unkin.net + cert-manager.io/private-key-size: "4096" + name: arrproxy + namespace: arrstack +spec: + gatewayClassName: traefik-external + listeners: + - name: http + port: 80 + protocol: HTTP + hostname: arrstack.unkin.net + allowedRoutes: + namespaces: + from: Same + - name: https + port: 443 + protocol: HTTPS + hostname: arrstack.unkin.net + allowedRoutes: + namespaces: + from: Same + tls: + mode: Terminate + certificateRefs: + - group: "" + kind: Secret + name: arrproxy-gateway-tls diff --git a/apps/base/arrstack/arrproxy/httproute.yaml b/apps/base/arrstack/arrproxy/httproute.yaml new file mode 100644 index 0000000..a31f74c --- /dev/null +++ b/apps/base/arrstack/arrproxy/httproute.yaml @@ -0,0 +1,58 @@ +--- +# Redirect plain HTTP to HTTPS. +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: arrproxy-http-redirect + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "2" +spec: + hostnames: + - arrstack.unkin.net + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: arrproxy + sectionName: http + rules: + - filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 301 + matches: + - path: + type: PathPrefix + value: / +--- +# All HTTPS traffic enters via oauth2-proxy (the arrproxy entry Service), which +# authenticates and path-routes to arrproxy-ui / arrproxy-api. The UI-vs-api and +# the //api oauth-bypass split is done inside oauth2-proxy (upstreams + +# skip-auth-regex), so a single backend here is sufficient. +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: arrproxy-route + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "2" +spec: + hostnames: + - arrstack.unkin.net + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: arrproxy + sectionName: https + rules: + - backendRefs: + - group: "" + kind: Service + name: arrproxy + port: 80 + weight: 1 + matches: + - path: + type: PathPrefix + value: / diff --git a/apps/base/arrstack/arrproxy/kustomization.yaml b/apps/base/arrstack/arrproxy/kustomization.yaml new file mode 100644 index 0000000..76b6cd4 --- /dev/null +++ b/apps/base/arrstack/arrproxy/kustomization.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - cnpg_cluster.yaml + - cnpg_backup.yaml + - migrations-configmap.yaml + - migrate-job.yaml + - vaultstaticsecret.yaml + - oauth2-proxy-configmap.yaml + - oauth2-proxy-deployment.yaml + - api-deployment.yaml + - ui-deployment.yaml + - services.yaml + - gateway.yaml + - httproute.yaml diff --git a/apps/base/arrstack/arrproxy/migrate-job.yaml b/apps/base/arrstack/arrproxy/migrate-job.yaml new file mode 100644 index 0000000..7f908f6 --- /dev/null +++ b/apps/base/arrstack/arrproxy/migrate-job.yaml @@ -0,0 +1,92 @@ +--- +# Applies the arrproxy schema once per sync, before the api rolls, so the serve +# replicas never race migrations (arrproxy-api does not self-migrate). Runs as the +# CNPG-minted app user so the tokens table is owned by that role. +# +# Sync-phase hook at wave 1 (NOT PreSync): the CNPG Cluster + generated +# arrproxy-db-app Secret apply at wave 0 and ArgoCD waits for the Cluster to be +# Healthy before starting wave 1, so Postgres exists before migrate connects. +apiVersion: batch/v1 +kind: Job +metadata: + name: arrproxy-migrate + namespace: arrstack + annotations: + argocd.argoproj.io/hook: Sync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation + argocd.argoproj.io/sync-wave: "1" +spec: + backoffLimit: 6 + ttlSecondsAfterFinished: 600 + template: + metadata: + labels: + app: arrproxy-migrate + spec: + serviceAccountName: default + automountServiceAccountToken: false + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: migrate + image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/library/postgres:18-alpine + imagePullPolicy: IfNotPresent + env: + - name: HOME + value: /tmp + - name: PGUSER + valueFrom: + secretKeyRef: + name: arrproxy-db-app + key: username + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: arrproxy-db-app + key: password + - name: PGHOST + value: arrproxy-db-rw.arrstack.svc.cluster.local + - name: PGPORT + value: "5432" + - name: PGDATABASE + value: arrproxy + - name: PGSSLMODE + value: require + command: + - psql + - -v + - ON_ERROR_STOP=1 + - -f + - /migrations/0001_init.sql + volumeMounts: + - name: migrations + mountPath: /migrations + readOnly: true + - name: tmp + mountPath: /tmp + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi + volumes: + - name: migrations + configMap: + name: arrproxy-migrations + - name: tmp + emptyDir: + sizeLimit: 64Mi diff --git a/apps/base/arrstack/arrproxy/migrations-configmap.yaml b/apps/base/arrstack/arrproxy/migrations-configmap.yaml new file mode 100644 index 0000000..ed92d2a --- /dev/null +++ b/apps/base/arrstack/arrproxy/migrations-configmap.yaml @@ -0,0 +1,29 @@ +--- +# arrproxy schema, mirrored from the arrproxy repo migrations/0001_init.sql +# (v0.1.0). arrproxy-api does NOT self-migrate, so the wave-1 migrate Job applies +# this once per sync as the app user. Keep in sync with the repo on schema bumps. +apiVersion: v1 +kind: ConfigMap +metadata: + name: arrproxy-migrations + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "0" +data: + 0001_init.sql: | + -- arrproxy token store. Only token hashes are persisted; plaintext is shown + -- once at mint time and never recoverable. + CREATE TABLE IF NOT EXISTS tokens ( + id TEXT PRIMARY KEY, + subject TEXT NOT NULL, + label TEXT NOT NULL DEFAULT '', + token_hash TEXT NOT NULL UNIQUE, + apps TEXT[] NOT NULL DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + expires_at TIMESTAMPTZ, + disabled BOOLEAN NOT NULL DEFAULT false, + last_used_at TIMESTAMPTZ + ); + + CREATE INDEX IF NOT EXISTS tokens_subject_idx ON tokens (subject); + CREATE INDEX IF NOT EXISTS tokens_token_hash_idx ON tokens (token_hash); diff --git a/apps/base/arrstack/arrproxy/oauth2-proxy-configmap.yaml b/apps/base/arrstack/arrproxy/oauth2-proxy-configmap.yaml new file mode 100644 index 0000000..e850f40 --- /dev/null +++ b/apps/base/arrstack/arrproxy/oauth2-proxy-configmap.yaml @@ -0,0 +1,46 @@ +--- +# Non-secret oauth2-proxy configuration (client_id/secret/cookie_secret come from +# the oauth-credentials Secret). oauth2-proxy is the single auth front for the +# arrstack: it authenticates the UI and the token API against Authentik, and path- +# routes to the arrproxy-ui / arrproxy-api upstreams. //api is exempted from +# auth (SKIP_AUTH_REGEX) so *arr clients presenting a per-user token reach the api +# directly; the api then validates the token. Everything else requires the oauth +# session and receives identity via X-Forwarded-* / X-Auth-Request-* headers. +apiVersion: v1 +kind: ConfigMap +metadata: + name: arrproxy-oauth2-env + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "2" +data: + OAUTH2_PROXY_HTTP_ADDRESS: "0.0.0.0:4180" + OAUTH2_PROXY_PROVIDER: "oidc" + # Authentik arrstack app discovery issuer (served by the internal unkin.net CA; + # trusted via PROVIDER_CA_FILES below). CONFIRM the slug matches the Authentik + # 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/" + 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. + OAUTH2_PROXY_OIDC_GROUPS_CLAIM: "ak_groups" + # Forward identity + groups to arrproxy-api as X-Forwarded-{User,Email,Groups} + # (the api reads these; ARRPROXY_GROUPS_HEADER=X-Forwarded-Groups). NOTE: + # 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 ONLY for the *arr proxy API (//api...): this matches + # /sonarr/api but NOT /api/tokens or /api/me (which stay authenticated). + OAUTH2_PROXY_SKIP_AUTH_REGEX: "^/[^/]+/api" + OAUTH2_PROXY_EMAIL_DOMAINS: "*" + OAUTH2_PROXY_COOKIE_SECURE: "true" + OAUTH2_PROXY_COOKIE_DOMAINS: "arrstack.unkin.net" + OAUTH2_PROXY_WHITELIST_DOMAINS: "arrstack.unkin.net" + OAUTH2_PROXY_REVERSE_PROXY: "true" + OAUTH2_PROXY_PROVIDER_CA_FILES: "/etc/ssl/combined/ca-certificates.crt" + OAUTH2_PROXY_CODE_CHALLENGE_METHOD: "S256" + OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "true" diff --git a/apps/base/arrstack/arrproxy/oauth2-proxy-deployment.yaml b/apps/base/arrstack/arrproxy/oauth2-proxy-deployment.yaml new file mode 100644 index 0000000..f3fb499 --- /dev/null +++ b/apps/base/arrstack/arrproxy/oauth2-proxy-deployment.yaml @@ -0,0 +1,133 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: arrproxy-oauth2 + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "2" + configmap.reloader.stakater.com/auto: "true" + secret.reloader.stakater.com/reload: "oauth-credentials,vault-ca-cert" +spec: + replicas: 2 + selector: + matchLabels: + app: arrproxy-oauth2 + strategy: + rollingUpdate: + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: arrproxy-oauth2 + spec: + serviceAccountName: default + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + initContainers: + # identity.unkin.net serves a Vault-PKI cert; combine the system roots + # with the internal CA so oauth2-proxy's OIDC HTTP client trusts it. + - name: combine-certs + image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/library/alpine:3 + imagePullPolicy: IfNotPresent + command: + - sh + - -c + - cat /etc/ssl/certs/ca-certificates.crt /custom-ca/ca.crt > /combined-certs/ca-certificates.crt + volumeMounts: + - name: vault-ca-cert + mountPath: /custom-ca + readOnly: true + - name: combined-certs + mountPath: /combined-certs + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + resources: + requests: + cpu: 50m + memory: 32Mi + limits: + cpu: 200m + memory: 64Mi + containers: + - name: oauth2-proxy + image: artifactapi.k8s.syd1.au.unkin.net/ghcr/oauth2-proxy/oauth2-proxy:v7.15.3 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 4180 + name: http + protocol: TCP + envFrom: + - configMapRef: + name: arrproxy-oauth2-env + optional: false + env: + - name: OAUTH2_PROXY_CLIENT_ID + valueFrom: + secretKeyRef: + name: oauth-credentials + key: client_id + - name: OAUTH2_PROXY_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: oauth-credentials + key: client_secret + - name: OAUTH2_PROXY_COOKIE_SECRET + valueFrom: + secretKeyRef: + name: oauth-credentials + key: cookie_secret + volumeMounts: + - name: combined-certs + mountPath: /etc/ssl/combined + readOnly: true + livenessProbe: + httpGet: + path: /ping + port: http + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /ready + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 256Mi + volumes: + - name: vault-ca-cert + secret: + secretName: vault-ca-cert + items: + - key: ca.crt + path: ca.crt + - name: combined-certs + emptyDir: {} + restartPolicy: Always diff --git a/apps/base/arrstack/arrproxy/services.yaml b/apps/base/arrstack/arrproxy/services.yaml new file mode 100644 index 0000000..eede4c9 --- /dev/null +++ b/apps/base/arrstack/arrproxy/services.yaml @@ -0,0 +1,59 @@ +--- +# Front-door entry Service: the HTTPRoute for arrstack.unkin.net targets this. +# All traffic (UI, token API, and the *arr proxy) enters via oauth2-proxy. +apiVersion: v1 +kind: Service +metadata: + name: arrproxy + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "2" +spec: + internalTrafficPolicy: Cluster + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + selector: + app: arrproxy-oauth2 + sessionAffinity: None + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: arrproxy-api + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "2" +spec: + internalTrafficPolicy: Cluster + ports: + - name: http + port: 8080 + protocol: TCP + targetPort: http + selector: + app: arrproxy-api + sessionAffinity: None + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: arrproxy-ui + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "2" +spec: + internalTrafficPolicy: Cluster + ports: + - name: http + port: 8080 + protocol: TCP + targetPort: http + selector: + app: arrproxy-ui + sessionAffinity: None + type: ClusterIP diff --git a/apps/base/arrstack/arrproxy/ui-deployment.yaml b/apps/base/arrstack/arrproxy/ui-deployment.yaml new file mode 100644 index 0000000..f216f1f --- /dev/null +++ b/apps/base/arrstack/arrproxy/ui-deployment.yaml @@ -0,0 +1,72 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: arrproxy-ui + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "2" +spec: + replicas: 2 + selector: + matchLabels: + app: arrproxy-ui + strategy: + rollingUpdate: + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: arrproxy-ui + spec: + serviceAccountName: default + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: ui + image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/arrproxy-ui:v0.1.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 + name: http + protocol: TCP + env: + - name: ARRPROXY_UI_ADDR + value: ":8080" + livenessProbe: + httpGet: + path: /livez + port: http + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /readyz + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + resources: + requests: + cpu: 50m + memory: 32Mi + limits: + cpu: 200m + memory: 128Mi + restartPolicy: Always diff --git a/apps/base/arrstack/arrproxy/vaultstaticsecret.yaml b/apps/base/arrstack/arrproxy/vaultstaticsecret.yaml new file mode 100644 index 0000000..8b24a28 --- /dev/null +++ b/apps/base/arrstack/arrproxy/vaultstaticsecret.yaml @@ -0,0 +1,49 @@ +--- +# Per-deployment token-hash pepper. Seeded (openssl rand) at +# kv/kubernetes/namespace/arrstack/default/arrproxy-pepper (key: pepper); the +# default k8s role's templated policy already grants read on +# kv/data/kubernetes/namespace/{{sa_namespace}}/{{sa_name}}/* for the +# arrstack/default ServiceAccount, so no terraform-vault change is needed. VSO +# syncs it into the arrproxy-pepper Secret consumed by arrproxy-api as +# ARRPROXY_PEPPER. +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: arrproxy-pepper + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + destination: + create: true + name: arrproxy-pepper + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/arrstack/default/arrproxy-pepper + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default +--- +# Authentik OIDC client for the arrstack front door (client_id, client_secret, +# cookie_secret), created by terraform-authentik at +# kv/kubernetes/namespace/arrstack/default/oauth-credentials. VSO syncs it into +# the oauth-credentials Secret consumed by the oauth2-proxy Deployment. +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: oauth-credentials + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + destination: + create: true + name: oauth-credentials + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/arrstack/default/oauth-credentials + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default diff --git a/apps/base/arrstack/kustomization.yaml b/apps/base/arrstack/kustomization.yaml index 4ab4a90..b60d6f2 100644 --- a/apps/base/arrstack/kustomization.yaml +++ b/apps/base/arrstack/kustomization.yaml @@ -13,3 +13,4 @@ resources: - radarr - prowlarr - nzbget + - arrproxy diff --git a/apps/base/bind-internal/authoritative/records.yaml b/apps/base/bind-internal/authoritative/records.yaml index 830e744..aa4f04b 100644 --- a/apps/base/bind-internal/authoritative/records.yaml +++ b/apps/base/bind-internal/authoritative/records.yaml @@ -102,3 +102,18 @@ spec: values: # traefik-internal gateway VIP; the ghp Gateway serves ghp.unkin.net there. - 198.18.200.4 +--- +apiVersion: bind.unkin.net/v1alpha1 +kind: DNSRecord +metadata: + name: arrstack-dns-internal + namespace: bind-internal +spec: + zoneRef: unkin-net + name: arrstack + type: A + ttl: 600 + values: + # traefik-EXTERNAL (DMZ) gateway VIP; the arrproxy Gateway serves the + # arrstack.unkin.net front door (oauth2-proxy) there. + - 198.18.199.0