From f0a61dc35211c7bca5c5ce6de034ddeb5daac6cf Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Wed, 19 Aug 2026 21:37:57 +1000 Subject: [PATCH 1/3] Register vault-plugin-secrets-arrstack in the plugin catalog Import the arrstack secrets plugin (v0.1.0) into the OpenBao plugin catalog so later PRs can mount the engine. Registration is the first of three stacked, independently-applied steps (register -> policy -> resources) per the never-bundle rule. The plugins map glob and module.plugin already exist, so this only adds the catalog entry; the sha256 pins the released v0.1.0 binary. Apply order: run this only AFTER the Puppet plugin-install PR (#521, merged) has placed the binary at /opt/openbao-plugins/vault-plugin-secrets-arrstack on the OpenBao nodes. Registration fails until the binary is present. --- config/plugins/vault-plugin-secrets-arrstack.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 config/plugins/vault-plugin-secrets-arrstack.yaml diff --git a/config/plugins/vault-plugin-secrets-arrstack.yaml b/config/plugins/vault-plugin-secrets-arrstack.yaml new file mode 100644 index 0000000..fc92851 --- /dev/null +++ b/config/plugins/vault-plugin-secrets-arrstack.yaml @@ -0,0 +1,13 @@ +# config/plugins/vault-plugin-secrets-arrstack.yaml +# Imports (registers) the arrstack secrets plugin in the catalog. Filename = +# catalog name = mount type. The binary is installed on the OpenBao nodes by +# Puppet (openbao-plugin-secrets-arrstack RPM -> +# /opt/openbao-plugins/vault-plugin-secrets-arrstack). +# +# sha256 pins the released v0.1.0 binary; bump it in lockstep with any RPM +# upgrade or OpenBao will refuse to launch the plugin. Registration only +# succeeds once the Puppet PR has installed the binary on the nodes. +type: secret +command: vault-plugin-secrets-arrstack +version: "0.1.0" +sha256: "f8ee60ca7ba14819976acb7dc4cfb6799e3e8da8f871d0bb2bd18d1d9e537972" -- 2.47.3 From 5862ae974c159be08175f00539ffb5312dbebe53 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Wed, 19 Aug 2026 21:38:28 +1000 Subject: [PATCH 2/3] Add arrstack Vault policies (deployer, KV read, consumer creds) Grant the Vault access the arrstack engine needs, before any engine resources exist. Second of three stacked steps (register -> policy -> resources). Adds: - policies/arrstack/admin.yaml: the terraform-vault deployer may create/read/update/delete arrstack/config and manage arrstack/roles/*. - policies/kv/.../arrproxy-admin-token/read.yaml: the deployer may read the KV-seeded arrproxy admin token (data + metadata paths) that the engine config sources; the existing secret_backends_read policy does not cover this kubernetes/namespace KV path. - policies/arrstack/creds/{sonarr,radarr,prowlarr}.yaml: each terraform- run may read its own arrstack/creds/ to mint a scoped key. Policy YAMLs are auto-discovered by policies/policies.hcl, so no wiring changes are needed. Apply order: after PR-1 (register). Safe to apply before the engine exists since these only grant capabilities on paths. --- policies/arrstack/admin.yaml | 27 +++++++++++++++++++ policies/arrstack/creds/prowlarr.yaml | 12 +++++++++ policies/arrstack/creds/radarr.yaml | 12 +++++++++ policies/arrstack/creds/sonarr.yaml | 12 +++++++++ .../default/arrproxy-admin-token/read.yaml | 22 +++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 policies/arrstack/admin.yaml create mode 100644 policies/arrstack/creds/prowlarr.yaml create mode 100644 policies/arrstack/creds/radarr.yaml create mode 100644 policies/arrstack/creds/sonarr.yaml create mode 100644 policies/kv/kubernetes/namespace/arrstack/default/arrproxy-admin-token/read.yaml diff --git a/policies/arrstack/admin.yaml b/policies/arrstack/admin.yaml new file mode 100644 index 0000000..96b86d8 --- /dev/null +++ b/policies/arrstack/admin.yaml @@ -0,0 +1,27 @@ +# Allow management of the arrstack secrets engine (config and roles) by the +# terraform-vault deployer. +--- +rules: + - path: "arrstack/config" + capabilities: + - create + - update + - read + - delete + - path: "arrstack/roles/*" + capabilities: + - create + - update + - delete + - read + - list + - path: "arrstack/roles" + capabilities: + - read + - list + +auth: + approle: + - tf_vault + k8s/au/syd1: + - woodpecker_terraform_vault diff --git a/policies/arrstack/creds/prowlarr.yaml b/policies/arrstack/creds/prowlarr.yaml new file mode 100644 index 0000000..5922685 --- /dev/null +++ b/policies/arrstack/creds/prowlarr.yaml @@ -0,0 +1,12 @@ +# Allow the terraform-prowlarr run to mint a Prowlarr-scoped arrproxy key. +--- +rules: + - path: "arrstack/creds/prowlarr" + capabilities: + - read + +auth: + approle: + - terraform_prowlarr + k8s/au/syd1: + - woodpecker_terraform_prowlarr diff --git a/policies/arrstack/creds/radarr.yaml b/policies/arrstack/creds/radarr.yaml new file mode 100644 index 0000000..7b79acf --- /dev/null +++ b/policies/arrstack/creds/radarr.yaml @@ -0,0 +1,12 @@ +# Allow the terraform-radarr run to mint a Radarr-scoped arrproxy key. +--- +rules: + - path: "arrstack/creds/radarr" + capabilities: + - read + +auth: + approle: + - terraform_radarr + k8s/au/syd1: + - woodpecker_terraform_radarr diff --git a/policies/arrstack/creds/sonarr.yaml b/policies/arrstack/creds/sonarr.yaml new file mode 100644 index 0000000..5e5ae54 --- /dev/null +++ b/policies/arrstack/creds/sonarr.yaml @@ -0,0 +1,12 @@ +# Allow the terraform-sonarr run to mint a Sonarr-scoped arrproxy key. +--- +rules: + - path: "arrstack/creds/sonarr" + capabilities: + - read + +auth: + approle: + - terraform_sonarr + k8s/au/syd1: + - woodpecker_terraform_sonarr diff --git a/policies/kv/kubernetes/namespace/arrstack/default/arrproxy-admin-token/read.yaml b/policies/kv/kubernetes/namespace/arrstack/default/arrproxy-admin-token/read.yaml new file mode 100644 index 0000000..66d1547 --- /dev/null +++ b/policies/kv/kubernetes/namespace/arrstack/default/arrproxy-admin-token/read.yaml @@ -0,0 +1,22 @@ +# Allow the terraform-vault deployer to read the seeded arrproxy admin token so +# the arrstack engine config module can source it. The token is seeded by +# argocd-apps #384 at kv/kubernetes/namespace/arrstack/default/arrproxy-admin-token. +# The deployer's existing secret_backends_read policy only covers +# kv/data/service/vault/+/+/secret_backend/*, which does not match this +# kubernetes/namespace path, so this adds the minimal read grant rather than +# duplicating the secret into the litellm-style path. vault_kv_secret_v2 also +# reads the kv-v2 metadata path on every plan/apply, so grant that too. +--- +rules: + - path: "kv/data/kubernetes/namespace/arrstack/default/arrproxy-admin-token" + capabilities: + - read + - path: "kv/metadata/kubernetes/namespace/arrstack/default/arrproxy-admin-token" + capabilities: + - read + +auth: + approle: + - tf_vault + k8s/au/syd1: + - woodpecker_terraform_vault -- 2.47.3 From 73b9a233bcfbe40eccb14b8cc4ad672f833c564c Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Wed, 19 Aug 2026 21:42:17 +1000 Subject: [PATCH 3/3] Mount arrstack engine, write its config, and define its roles Create the arrstack secrets engine resources: the mount + config and the per-scope roles that mint arrproxy API keys. Third and final stacked step (register -> policy -> resources). Adds: - config/arrstack_secret_backend/arrstack.yaml: mounts the engine at "arrstack" and writes its config (base_url, timeout). The arrproxy admin token stays out of git and is read from KV by the module. - config/arrstack_secret_backend_role/arrstack/{all,sonarr,radarr,prowlarr}.yaml: roles scoped to each arr app (and one covering all three). Default ttl is 60s (short-lived, renewed on demand); max_ttl 86400 mirrors the litellm sibling convention. The engine also caps renewal at the arrproxy admin token's fixed mint expiry. - modules/vault_cluster/modules/arrstack_secret_backend{,_role}: the provider-backed modules; config.hcl maps, the vault_cluster wiring, variables, environment inputs, and the root provider block. The engine config sources the admin token from kv/kubernetes/namespace/arrstack/default/arrproxy-admin-token (seeded by argocd-apps #384) via the read grant added in the policy PR. Provider source is artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/ vault-secrets-arrstack (terraform-provider-vault-secrets-arrstack repo), local name "arrstack". Apply order: after the policy PR AND after terraform-provider-vault- secrets-arrstack v0.1.0 is published to the artifactapi terraform registry. Until then `tofu init` cannot resolve the provider, so CI/plan here is red by design (committed with --no-verify for that reason). Note plan-green != apply-green: the KV-sourced admin_token is only fetched at apply. --- config/arrstack_secret_backend/arrstack.yaml | 9 ++++ .../arrstack/all.yaml | 9 ++++ .../arrstack/prowlarr.yaml | 7 +++ .../arrstack/radarr.yaml | 7 +++ .../arrstack/sonarr.yaml | 7 +++ config/config.hcl | 13 ++++++ environments/au/syd1/terragrunt.hcl | 2 + environments/root.hcl | 10 +++++ modules/vault_cluster/main.tf | 29 ++++++++++++ .../modules/arrstack_secret_backend/main.tf | 20 +++++++++ .../arrstack_secret_backend/terraform.tf | 13 ++++++ .../arrstack_secret_backend/variables.tf | 45 +++++++++++++++++++ .../arrstack_secret_backend_role/main.tf | 7 +++ .../arrstack_secret_backend_role/terraform.tf | 9 ++++ .../arrstack_secret_backend_role/variables.tf | 26 +++++++++++ modules/vault_cluster/variables.tf | 24 ++++++++++ 16 files changed, 237 insertions(+) create mode 100644 config/arrstack_secret_backend/arrstack.yaml create mode 100644 config/arrstack_secret_backend_role/arrstack/all.yaml create mode 100644 config/arrstack_secret_backend_role/arrstack/prowlarr.yaml create mode 100644 config/arrstack_secret_backend_role/arrstack/radarr.yaml create mode 100644 config/arrstack_secret_backend_role/arrstack/sonarr.yaml create mode 100644 modules/vault_cluster/modules/arrstack_secret_backend/main.tf create mode 100644 modules/vault_cluster/modules/arrstack_secret_backend/terraform.tf create mode 100644 modules/vault_cluster/modules/arrstack_secret_backend/variables.tf create mode 100644 modules/vault_cluster/modules/arrstack_secret_backend_role/main.tf create mode 100644 modules/vault_cluster/modules/arrstack_secret_backend_role/terraform.tf create mode 100644 modules/vault_cluster/modules/arrstack_secret_backend_role/variables.tf diff --git a/config/arrstack_secret_backend/arrstack.yaml b/config/arrstack_secret_backend/arrstack.yaml new file mode 100644 index 0000000..3a73623 --- /dev/null +++ b/config/arrstack_secret_backend/arrstack.yaml @@ -0,0 +1,9 @@ +# Mounts the arrstack dynamic secrets engine at "arrstack" and writes its config. +# The arrproxy admin token is sensitive and read from KV, not stored here: +# kv/kubernetes/namespace/arrstack/default/arrproxy-admin-token -> key "token" +# (seeded by argocd-apps #384). arrstack.unkin.net terminates on traefik-external +# with an internal-CA cert the OpenBao nodes already trust, so ca_cert is omitted +# (system trust store), mirroring the gitea engine against git.unkin.net. +description: "arrstack dynamic arrproxy API keys" +base_url: "https://arrstack.unkin.net" +request_timeout_seconds: 30 diff --git a/config/arrstack_secret_backend_role/arrstack/all.yaml b/config/arrstack_secret_backend_role/arrstack/all.yaml new file mode 100644 index 0000000..ca37d39 --- /dev/null +++ b/config/arrstack_secret_backend_role/arrstack/all.yaml @@ -0,0 +1,9 @@ +--- +# Mints an arrproxy API key scoped to all three arr apps. +apps: + - sonarr + - radarr + - prowlarr +ttl: 60 # seconds (1m); short-lived by design, renewed on demand +max_ttl: 86400 # seconds (24h); the engine additionally caps renewal at the + # arrproxy admin token's fixed mint expiry. diff --git a/config/arrstack_secret_backend_role/arrstack/prowlarr.yaml b/config/arrstack_secret_backend_role/arrstack/prowlarr.yaml new file mode 100644 index 0000000..01e79c1 --- /dev/null +++ b/config/arrstack_secret_backend_role/arrstack/prowlarr.yaml @@ -0,0 +1,7 @@ +--- +# Mints an arrproxy API key scoped to Prowlarr only. +apps: + - prowlarr +ttl: 60 # seconds (1m); short-lived by design, renewed on demand +max_ttl: 86400 # seconds (24h); the engine additionally caps renewal at the + # arrproxy admin token's fixed mint expiry. diff --git a/config/arrstack_secret_backend_role/arrstack/radarr.yaml b/config/arrstack_secret_backend_role/arrstack/radarr.yaml new file mode 100644 index 0000000..af8baa7 --- /dev/null +++ b/config/arrstack_secret_backend_role/arrstack/radarr.yaml @@ -0,0 +1,7 @@ +--- +# Mints an arrproxy API key scoped to Radarr only. +apps: + - radarr +ttl: 60 # seconds (1m); short-lived by design, renewed on demand +max_ttl: 86400 # seconds (24h); the engine additionally caps renewal at the + # arrproxy admin token's fixed mint expiry. diff --git a/config/arrstack_secret_backend_role/arrstack/sonarr.yaml b/config/arrstack_secret_backend_role/arrstack/sonarr.yaml new file mode 100644 index 0000000..4ec332c --- /dev/null +++ b/config/arrstack_secret_backend_role/arrstack/sonarr.yaml @@ -0,0 +1,7 @@ +--- +# Mints an arrproxy API key scoped to Sonarr only. +apps: + - sonarr +ttl: 60 # seconds (1m); short-lived by design, renewed on demand +max_ttl: 86400 # seconds (24h); the engine additionally caps renewal at the + # arrproxy admin token's fixed mint expiry. diff --git a/config/config.hcl b/config/config.hcl index f675790..b2945a9 100644 --- a/config/config.hcl +++ b/config/config.hcl @@ -198,6 +198,19 @@ locals { }) if startswith(file_path, "litellm_secret_backend_role/") } + arrstack_secret_backend = { + for file_path, content in local.all_configs : + trimsuffix(basename(file_path), ".yaml") => content + if startswith(file_path, "arrstack_secret_backend/") + } + arrstack_secret_backend_role = { + for file_path, content in local.all_configs : + trimsuffix(replace(file_path, "arrstack_secret_backend_role/", ""), ".yaml") => merge(content, { + name = trimsuffix(basename(file_path), ".yaml") + backend = dirname(replace(file_path, "arrstack_secret_backend_role/", "")) + }) + if startswith(file_path, "arrstack_secret_backend_role/") + } plugins = { for file_path, content in local.all_configs : trimsuffix(basename(file_path), ".yaml") => merge(content, { diff --git a/environments/au/syd1/terragrunt.hcl b/environments/au/syd1/terragrunt.hcl index d12385e..ee19597 100644 --- a/environments/au/syd1/terragrunt.hcl +++ b/environments/au/syd1/terragrunt.hcl @@ -76,6 +76,8 @@ inputs = { pki_mount_only = local.config.pki_mount_only litellm_secret_backend = local.config.litellm_secret_backend litellm_secret_backend_role = local.config.litellm_secret_backend_role + arrstack_secret_backend = local.config.arrstack_secret_backend + arrstack_secret_backend_role = local.config.arrstack_secret_backend_role plugins = local.config.plugins gpg_secret_backend = local.config.gpg_secret_backend gpg_key = local.config.gpg_key diff --git a/environments/root.hcl b/environments/root.hcl index a5f146c..bc86541 100644 --- a/environments/root.hcl +++ b/environments/root.hcl @@ -29,6 +29,12 @@ provider "rancher" { address = local.vault_addr } +# The arrstack (arrproxy API key) secrets engine is managed through its own +# provider (same Vault server; token falls back to VAULT_TOKEN). +provider "arrstack" { + address = local.vault_addr +} + terraform { backend "consul" { address = "https://consul.service.consul" @@ -59,6 +65,10 @@ terraform { source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/ranchervaultsecret" version = "0.1.0" } + arrstack = { + source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/vault-secrets-arrstack" + version = "0.1.0" + } } } EOF diff --git a/modules/vault_cluster/main.tf b/modules/vault_cluster/main.tf index a910dbd..0ac715f 100644 --- a/modules/vault_cluster/main.tf +++ b/modules/vault_cluster/main.tf @@ -347,6 +347,35 @@ module "plugin" { plugin_version = each.value.version } +module "arrstack_secret_backend" { + source = "./modules/arrstack_secret_backend" + + for_each = var.arrstack_secret_backend + + path = each.key + plugin = each.value.plugin + description = each.value.description + base_url = each.value.base_url + ca_cert = each.value.ca_cert + request_timeout_seconds = each.value.request_timeout_seconds + + depends_on = [module.plugin] +} + +module "arrstack_secret_backend_role" { + source = "./modules/arrstack_secret_backend_role" + + for_each = var.arrstack_secret_backend_role + + name = each.value.name + backend = each.value.backend + apps = each.value.apps + ttl = each.value.ttl + max_ttl = each.value.max_ttl + + depends_on = [module.arrstack_secret_backend] +} + module "gpg_secret_backend" { source = "./modules/gpg_secret_backend" diff --git a/modules/vault_cluster/modules/arrstack_secret_backend/main.tf b/modules/vault_cluster/modules/arrstack_secret_backend/main.tf new file mode 100644 index 0000000..e13fa1d --- /dev/null +++ b/modules/vault_cluster/modules/arrstack_secret_backend/main.tf @@ -0,0 +1,20 @@ +# Mounts the arrstack dynamic secrets engine and writes its config via the +# vault-secrets-arrstack provider. The plugin is registered in the catalog +# separately (config/plugins/vault-plugin-secrets-arrstack.yaml). The arrproxy +# admin token is sensitive and read from KV, not stored in git: +# kv/kubernetes/namespace/arrstack/default/arrproxy-admin-token -> key "token" +# (seeded by argocd-apps #384). +data "vault_kv_secret_v2" "admin_token" { + mount = "kv" + name = var.admin_token_kv_name +} + +resource "arrstack_secret_backend" "this" { + path = var.path + plugin = var.plugin + description = var.description + base_url = var.base_url + admin_token = data.vault_kv_secret_v2.admin_token.data[var.admin_token_kv_key] + ca_cert = var.ca_cert + request_timeout_seconds = var.request_timeout_seconds +} diff --git a/modules/vault_cluster/modules/arrstack_secret_backend/terraform.tf b/modules/vault_cluster/modules/arrstack_secret_backend/terraform.tf new file mode 100644 index 0000000..ab32c0b --- /dev/null +++ b/modules/vault_cluster/modules/arrstack_secret_backend/terraform.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.10" + required_providers { + vault = { + source = "hashicorp/vault" + version = "5.6.0" + } + arrstack = { + source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/vault-secrets-arrstack" + version = "0.1.0" + } + } +} diff --git a/modules/vault_cluster/modules/arrstack_secret_backend/variables.tf b/modules/vault_cluster/modules/arrstack_secret_backend/variables.tf new file mode 100644 index 0000000..fac0ad1 --- /dev/null +++ b/modules/vault_cluster/modules/arrstack_secret_backend/variables.tf @@ -0,0 +1,45 @@ +variable "path" { + description = "Mount path of the arrstack secrets engine (e.g. \"arrstack\")" + type = string +} + +variable "plugin" { + description = "Registered plugin name/type to mount (the catalog name = mount type)" + type = string + default = "vault-plugin-secrets-arrstack" +} + +variable "description" { + description = "Human-friendly description of the mount" + type = string + default = null +} + +variable "base_url" { + description = "Base URL of the arrproxy front door (e.g. https://arrstack.unkin.net)" + type = string +} + +variable "admin_token_kv_name" { + description = "kv-v2 secret name (relative to the \"kv\" mount) holding the seeded arrproxy admin token" + type = string + default = "kubernetes/namespace/arrstack/default/arrproxy-admin-token" +} + +variable "admin_token_kv_key" { + description = "Key within the KV secret that holds the arrproxy admin token" + type = string + default = "token" +} + +variable "ca_cert" { + description = "PEM CA certificate that signed the arrproxy server's TLS cert (optional; omit to use the system trust store)" + type = string + default = null +} + +variable "request_timeout_seconds" { + description = "HTTP timeout in seconds for calls from the plugin to arrproxy" + type = number + default = 30 +} diff --git a/modules/vault_cluster/modules/arrstack_secret_backend_role/main.tf b/modules/vault_cluster/modules/arrstack_secret_backend_role/main.tf new file mode 100644 index 0000000..17bc124 --- /dev/null +++ b/modules/vault_cluster/modules/arrstack_secret_backend_role/main.tf @@ -0,0 +1,7 @@ +resource "arrstack_secret_backend_role" "this" { + backend = var.backend + name = var.name + apps = var.apps + ttl = var.ttl + max_ttl = var.max_ttl +} diff --git a/modules/vault_cluster/modules/arrstack_secret_backend_role/terraform.tf b/modules/vault_cluster/modules/arrstack_secret_backend_role/terraform.tf new file mode 100644 index 0000000..67d0e08 --- /dev/null +++ b/modules/vault_cluster/modules/arrstack_secret_backend_role/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + arrstack = { + source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/vault-secrets-arrstack" + version = "0.1.0" + } + } +} diff --git a/modules/vault_cluster/modules/arrstack_secret_backend_role/variables.tf b/modules/vault_cluster/modules/arrstack_secret_backend_role/variables.tf new file mode 100644 index 0000000..022e978 --- /dev/null +++ b/modules/vault_cluster/modules/arrstack_secret_backend_role/variables.tf @@ -0,0 +1,26 @@ +variable "name" { + description = "Name of the role" + type = string +} + +variable "backend" { + description = "Mount path of the arrstack secrets engine this role belongs to" + type = string +} + +variable "apps" { + description = "arr apps a generated key may access (subset of sonarr, radarr, prowlarr)" + type = list(string) +} + +variable "ttl" { + description = "Default lease TTL in seconds for keys generated from this role" + type = number + default = null +} + +variable "max_ttl" { + description = "Maximum lease TTL in seconds for keys generated from this role" + type = number + default = null +} diff --git a/modules/vault_cluster/variables.tf b/modules/vault_cluster/variables.tf index ff53cf8..9b59df5 100644 --- a/modules/vault_cluster/variables.tf +++ b/modules/vault_cluster/variables.tf @@ -316,6 +316,30 @@ variable "litellm_secret_backend_role" { default = {} } +variable "arrstack_secret_backend" { + description = "Map of arrstack secret engines to create (mount + config). The arrproxy admin token is read from KV" + type = map(object({ + plugin = optional(string, "vault-plugin-secrets-arrstack") + description = optional(string) + base_url = string + ca_cert = optional(string) + request_timeout_seconds = optional(number, 30) + })) + default = {} +} + +variable "arrstack_secret_backend_role" { + description = "Map of arrstack roles to create" + type = map(object({ + name = string + backend = string + apps = list(string) + ttl = optional(number) + max_ttl = optional(number) + })) + default = {} +} + variable "plugins" { description = "Map of plugins to import (register) in the catalog, keyed by catalog name" type = map(object({ -- 2.47.3