From d387301d6b560f78f8ef602caae8ab41ec870730 Mon Sep 17 00:00:00 2001 From: Unkin Agent Date: Sat, 15 Aug 2026 16:43:02 +1000 Subject: [PATCH] arrstack: manage sonarr/radarr/prowlarr API keys via Vault (#369) ## Why The sonarr/radarr/prowlarr images self-generate an API key into /config/config.xml on first boot, so the key is unmanaged and differs per volume reset. This makes Vault the source of truth for those keys (override bootstrap, chosen by Ben): the key is minted in Vault and enforced into config.xml before each app starts. ## Changes - Add a `VaultAuth` `default` in the `arrstack` namespace (kubernetes auth, mount `k8s/au/syd1`, role `default`, SA `default`), mirroring jellyfin. - Add a per-app `VaultStaticSecret` that syncs `kv/kubernetes/namespace/arrstack/default/` (key `apitoken`) into the `-apikey` Secret. The `default` k8s role's templated policy already grants read on that path for the `arrstack/default` SA, so no terraform-vault change is needed. - Add an `apikey-init` initContainer to each of the three deployments that reads `API_KEY` from the VSO-created Secret, fails closed on a missing or non-hex value, and writes/updates only the `` element in `/config/config.xml` (then fixes ownership 1000:1000, mode 600). Image is a pinned busybox via artifactapi to keep this PR atomic (no new image dependency). - Wire the new manifests into the base and per-app kustomizations. ## Notes - Keys already seeded in Vault at `kv/kubernetes/namespace/arrstack/default/`. - nzbget is out of scope: it has no config.xml `` (uses ControlPassword), a separate follow-up. - Downstream consumers (proxy, terraform) currently read `kv/service/media-apps/`; the authoritative key now lives at the path above. Reconciliation is deferred. --------- Co-authored-by: unkin-agent Reviewed-on: https://git.unkin.net/unkin/argocd-apps/pulls/369 Co-authored-by: Unkin Agent Co-committed-by: Unkin Agent --- apps/base/arrstack/kustomization.yaml | 1 + apps/base/arrstack/prowlarr/deployment.yaml | 48 +++++++++++++++++++ .../base/arrstack/prowlarr/kustomization.yaml | 1 + .../arrstack/prowlarr/vaultstaticsecret.yaml | 25 ++++++++++ apps/base/arrstack/radarr/deployment.yaml | 48 +++++++++++++++++++ apps/base/arrstack/radarr/kustomization.yaml | 1 + .../arrstack/radarr/vaultstaticsecret.yaml | 25 ++++++++++ apps/base/arrstack/sonarr/deployment.yaml | 48 +++++++++++++++++++ apps/base/arrstack/sonarr/kustomization.yaml | 1 + .../arrstack/sonarr/vaultstaticsecret.yaml | 25 ++++++++++ apps/base/arrstack/vaultauth.yaml | 20 ++++++++ 11 files changed, 243 insertions(+) create mode 100644 apps/base/arrstack/prowlarr/vaultstaticsecret.yaml create mode 100644 apps/base/arrstack/radarr/vaultstaticsecret.yaml create mode 100644 apps/base/arrstack/sonarr/vaultstaticsecret.yaml create mode 100644 apps/base/arrstack/vaultauth.yaml diff --git a/apps/base/arrstack/kustomization.yaml b/apps/base/arrstack/kustomization.yaml index c68c8c5..4ab4a90 100644 --- a/apps/base/arrstack/kustomization.yaml +++ b/apps/base/arrstack/kustomization.yaml @@ -4,6 +4,7 @@ kind: Kustomization resources: - namespace.yaml + - vaultauth.yaml - pv-media-tv.yaml - pv-media-movies.yaml - pvc-media-tv.yaml diff --git a/apps/base/arrstack/prowlarr/deployment.yaml b/apps/base/arrstack/prowlarr/deployment.yaml index df9efde..a6a95cc 100644 --- a/apps/base/arrstack/prowlarr/deployment.yaml +++ b/apps/base/arrstack/prowlarr/deployment.yaml @@ -20,6 +20,54 @@ spec: securityContext: fsGroup: 1000 fsGroupChangePolicy: OnRootMismatch + initContainers: + # Enforce the Vault-sourced API key in /config/config.xml before the app + # starts. Vault is source of truth (override bootstrap): the key is minted + # in Vault, synced by VSO into the prowlarr-apikey Secret, and written here. + # Runs as root to fix ownership; touches only the element. + - name: apikey-init + image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/library/busybox:1.37.0 + imagePullPolicy: IfNotPresent + securityContext: + runAsUser: 0 + env: + - name: API_KEY + valueFrom: + secretKeyRef: + name: prowlarr-apikey + key: apitoken + command: + - sh + - -c + - | + set -eu + case "$API_KEY" in + "" | *[!0-9a-fA-F]*) + echo "apikey-init: API_KEY missing or not hex; refusing" >&2 + exit 1 + ;; + esac + CFG=/config/config.xml + if [ ! -f "$CFG" ]; then + printf '\n %s\n\n' "$API_KEY" > "$CFG" + elif grep -q '' "$CFG"; then + sed -i "s|[^<]*|${API_KEY}|" "$CFG" + else + sed -i "s||\n ${API_KEY}|" "$CFG" + fi + chown 1000:1000 "$CFG" + chmod 600 "$CFG" + echo "apikey-init: enforced from Vault" + resources: + requests: + cpu: 50m + memory: 32Mi + limits: + cpu: 200m + memory: 64Mi + volumeMounts: + - name: config + mountPath: /config containers: - name: prowlarr image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/linuxserver/prowlarr:2.5.2 diff --git a/apps/base/arrstack/prowlarr/kustomization.yaml b/apps/base/arrstack/prowlarr/kustomization.yaml index a7f5633..887f44d 100644 --- a/apps/base/arrstack/prowlarr/kustomization.yaml +++ b/apps/base/arrstack/prowlarr/kustomization.yaml @@ -4,6 +4,7 @@ kind: Kustomization resources: - pvc-config.yaml + - vaultstaticsecret.yaml - deployment.yaml - service.yaml - gateway.yaml diff --git a/apps/base/arrstack/prowlarr/vaultstaticsecret.yaml b/apps/base/arrstack/prowlarr/vaultstaticsecret.yaml new file mode 100644 index 0000000..356201e --- /dev/null +++ b/apps/base/arrstack/prowlarr/vaultstaticsecret.yaml @@ -0,0 +1,25 @@ +--- +# prowlarr API key. Seeded at kv/kubernetes/namespace/arrstack/default/prowlarr +# (key: apitoken); 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 prowlarr-apikey Secret that the apikey-init initContainer reads +# to enforce in /config/config.xml (Vault is source of truth). +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: prowlarr-apikey + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + destination: + create: true + name: prowlarr-apikey + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/arrstack/default/prowlarr + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default diff --git a/apps/base/arrstack/radarr/deployment.yaml b/apps/base/arrstack/radarr/deployment.yaml index 9de78e5..6ed1f9d 100644 --- a/apps/base/arrstack/radarr/deployment.yaml +++ b/apps/base/arrstack/radarr/deployment.yaml @@ -20,6 +20,54 @@ spec: securityContext: fsGroup: 1000 fsGroupChangePolicy: OnRootMismatch + initContainers: + # Enforce the Vault-sourced API key in /config/config.xml before the app + # starts. Vault is source of truth (override bootstrap): the key is minted + # in Vault, synced by VSO into the radarr-apikey Secret, and written here. + # Runs as root to fix ownership; touches only the element. + - name: apikey-init + image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/library/busybox:1.37.0 + imagePullPolicy: IfNotPresent + securityContext: + runAsUser: 0 + env: + - name: API_KEY + valueFrom: + secretKeyRef: + name: radarr-apikey + key: apitoken + command: + - sh + - -c + - | + set -eu + case "$API_KEY" in + "" | *[!0-9a-fA-F]*) + echo "apikey-init: API_KEY missing or not hex; refusing" >&2 + exit 1 + ;; + esac + CFG=/config/config.xml + if [ ! -f "$CFG" ]; then + printf '\n %s\n\n' "$API_KEY" > "$CFG" + elif grep -q '' "$CFG"; then + sed -i "s|[^<]*|${API_KEY}|" "$CFG" + else + sed -i "s||\n ${API_KEY}|" "$CFG" + fi + chown 1000:1000 "$CFG" + chmod 600 "$CFG" + echo "apikey-init: enforced from Vault" + resources: + requests: + cpu: 50m + memory: 32Mi + limits: + cpu: 200m + memory: 64Mi + volumeMounts: + - name: config + mountPath: /config containers: - name: radarr image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/linuxserver/radarr:6.3.0 diff --git a/apps/base/arrstack/radarr/kustomization.yaml b/apps/base/arrstack/radarr/kustomization.yaml index a7f5633..887f44d 100644 --- a/apps/base/arrstack/radarr/kustomization.yaml +++ b/apps/base/arrstack/radarr/kustomization.yaml @@ -4,6 +4,7 @@ kind: Kustomization resources: - pvc-config.yaml + - vaultstaticsecret.yaml - deployment.yaml - service.yaml - gateway.yaml diff --git a/apps/base/arrstack/radarr/vaultstaticsecret.yaml b/apps/base/arrstack/radarr/vaultstaticsecret.yaml new file mode 100644 index 0000000..3601f9f --- /dev/null +++ b/apps/base/arrstack/radarr/vaultstaticsecret.yaml @@ -0,0 +1,25 @@ +--- +# radarr API key. Seeded at kv/kubernetes/namespace/arrstack/default/radarr +# (key: apitoken); 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 radarr-apikey Secret that the apikey-init initContainer reads +# to enforce in /config/config.xml (Vault is source of truth). +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: radarr-apikey + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + destination: + create: true + name: radarr-apikey + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/arrstack/default/radarr + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default diff --git a/apps/base/arrstack/sonarr/deployment.yaml b/apps/base/arrstack/sonarr/deployment.yaml index 5352c71..6e9be13 100644 --- a/apps/base/arrstack/sonarr/deployment.yaml +++ b/apps/base/arrstack/sonarr/deployment.yaml @@ -23,6 +23,54 @@ spec: # OnRootMismatch avoids a recursive chown of the whole media tree. fsGroup: 1000 fsGroupChangePolicy: OnRootMismatch + initContainers: + # Enforce the Vault-sourced API key in /config/config.xml before the app + # starts. Vault is source of truth (override bootstrap): the key is minted + # in Vault, synced by VSO into the sonarr-apikey Secret, and written here. + # Runs as root to fix ownership; touches only the element. + - name: apikey-init + image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/library/busybox:1.37.0 + imagePullPolicy: IfNotPresent + securityContext: + runAsUser: 0 + env: + - name: API_KEY + valueFrom: + secretKeyRef: + name: sonarr-apikey + key: apitoken + command: + - sh + - -c + - | + set -eu + case "$API_KEY" in + "" | *[!0-9a-fA-F]*) + echo "apikey-init: API_KEY missing or not hex; refusing" >&2 + exit 1 + ;; + esac + CFG=/config/config.xml + if [ ! -f "$CFG" ]; then + printf '\n %s\n\n' "$API_KEY" > "$CFG" + elif grep -q '' "$CFG"; then + sed -i "s|[^<]*|${API_KEY}|" "$CFG" + else + sed -i "s||\n ${API_KEY}|" "$CFG" + fi + chown 1000:1000 "$CFG" + chmod 600 "$CFG" + echo "apikey-init: enforced from Vault" + resources: + requests: + cpu: 50m + memory: 32Mi + limits: + cpu: 200m + memory: 64Mi + volumeMounts: + - name: config + mountPath: /config containers: - name: sonarr image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/linuxserver/sonarr:4.0.19 diff --git a/apps/base/arrstack/sonarr/kustomization.yaml b/apps/base/arrstack/sonarr/kustomization.yaml index a7f5633..887f44d 100644 --- a/apps/base/arrstack/sonarr/kustomization.yaml +++ b/apps/base/arrstack/sonarr/kustomization.yaml @@ -4,6 +4,7 @@ kind: Kustomization resources: - pvc-config.yaml + - vaultstaticsecret.yaml - deployment.yaml - service.yaml - gateway.yaml diff --git a/apps/base/arrstack/sonarr/vaultstaticsecret.yaml b/apps/base/arrstack/sonarr/vaultstaticsecret.yaml new file mode 100644 index 0000000..93e9798 --- /dev/null +++ b/apps/base/arrstack/sonarr/vaultstaticsecret.yaml @@ -0,0 +1,25 @@ +--- +# sonarr API key. Seeded at kv/kubernetes/namespace/arrstack/default/sonarr +# (key: apitoken); 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 sonarr-apikey Secret that the apikey-init initContainer reads +# to enforce in /config/config.xml (Vault is source of truth). +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: sonarr-apikey + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + destination: + create: true + name: sonarr-apikey + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/arrstack/default/sonarr + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default diff --git a/apps/base/arrstack/vaultauth.yaml b/apps/base/arrstack/vaultauth.yaml new file mode 100644 index 0000000..3eb27aa --- /dev/null +++ b/apps/base/arrstack/vaultauth.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultAuth +metadata: + name: default + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + allowedNamespaces: + - arrstack + kubernetes: + audiences: + - vault + role: default + serviceAccount: default + tokenExpirationSeconds: 600 + method: kubernetes + mount: k8s/au/syd1 + vaultConnectionRef: vso-system/default