Declarative *arr wiring
The *arr apps split their config into two layers. config.xml (API key, auth, port) is
seeded declaratively; everything relational (download clients, root folders, indexers,
app links) lives in SQLite and is applied through the REST API by an idempotent Job.
Vault ── arr-apikeys (generated) ─▶ media-arr-apikeys Secret ─▶ initContainers seed config.xml └─ wiring (you write) ───────▶ media-wiring-secrets Secret ─▶ wiring Job ─▶ *arr REST APIsPieces
Section titled “Pieces”| Piece | What |
|---|---|
vault-credential-setup Job | Generates 32-char API keys for prowlarr/sonarr/radarr/bazarr into Vault (prevent_override → stable) |
media-arr-apikeys (VaultStaticSecret) | Projects those keys into the namespace |
seed-config initContainers | Write /config/config.xml with the pinned key on first boot (idempotent — skip if present) |
media-wiring Job | GET-before-POST against the APIs: NZBGet clients, root folders, Prowlarr→Sonarr/Radarr, indexers from indexers.json |
media-wiring-secrets (VaultStaticSecret) | NZBGet password + indexer API keys — you write these to Vault |
Everything is reconciled by Flux; the Jobs carry kustomize.toolkit.fluxcd.io/force: enabled
so they re-run and re-assert state each reconcile.
Generate the secrets
Section titled “Generate the secrets”1. Create the Vault policies + k8s-auth roles (must exist before the setup Job can authenticate). They’re Terraform-managed alongside the other apps:
cd tf/application-parameters/hl-cluster-vaultterraform init -upgradeterraform apply # adds the media-setup / media-secrets roles + policies2. Let Flux reconcile — the vault-credential-setup Job logs into Vault as
media-setup and writes the four API keys to secrets/cluster/internal/ns/media/arr-apikeys.
vault-secrets-operator then projects them into the media-arr-apikeys Secret, and the
apps’ initContainers seed config.xml. Force a run if you don’t want to wait:
flux -n tenant-0-k8s-services-homelab-muehlena-de reconcile kustomization applications-mediakubectl -n media get secret media-arr-apikeys # should have prowlarr/sonarr/radarr/bazarr keys3. Write the wiring secrets you own — the NZBGet control password (the same value as
the VM’s media_nas_nzbget_control_password) and any Usenet-indexer API keys. Use the
OpenBao CLI (bao, or vault):
bao kv put secrets/cluster/internal/ns/media/wiring \ nzbget-password='<same as media_nas_nzbget_control_password>' \ INDEXER_NZBGEEK_APIKEY='<your indexer key>' # optional, per indexerKey names for indexer secrets must be valid env-var names (INDEXER_*) — they’re exposed
to the wiring Job for envsubst into indexers.json. nzbget-password is read from a file
mount, so its hyphen is fine.
Declaring indexers
Section titled “Declaring indexers”k8s/manifests/base/appliations/media-wiring/config/indexers.json is a JSON array of full
Prowlarr /api/v1/indexer bodies (empty by default). Reference secret keys as ${INDEXER_*}:
[ { "name": "NZBgeek", "implementation": "Newznab", "configContract": "NewznabSettings", "enable": true, "protocol": "usenet", "fields": [ { "name": "baseUrl", "value": "https://api.nzbgeek.info" }, { "name": "apiKey", "value": "${INDEXER_NZBGEEK_APIKEY}" }, { "name": "categories", "value": [5000, 5040] } ] }]Once an indexer exists in Prowlarr, the Prowlarr→Sonarr/Radarr app links (set by the Job) auto-push it to the *arr apps — no per-app indexer config.
What the Job does (and doesn’t)
Section titled “What the Job does (and doesn’t)”Covered idempotently: Sonarr/Radarr NZBGet download client + root folders
(/data/media/tv, /data/media/movies), and Prowlarr applications for Sonarr + Radarr.
Tunables are env on the Job (SONARR_URL, NZBGET_HOST, SONARR_ROOT, …).
Not covered: Bazarr (its config isn’t RESTful — add Sonarr/Radarr in its UI with the
keys from media-arr-apikeys), and quality profiles / custom formats (use
Configarr / Recyclarr). Re-running the Job is always safe.