From 5567bd6dac126f0278051c681207757b88fc4e9e Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Tue, 18 Aug 2026 23:22:08 +1000 Subject: [PATCH] Add arrstack secrets engine (mount, roles, config, policies) Wires the vault-plugin-secrets-arrstack dynamic engine so terraform-sonarr/ radarr/prowlarr can mint scoped arrproxy API keys, mirroring the litellm engine. - Register vault-plugin-secrets-arrstack v0.1.0 in the plugin catalog. - Mount arrstack + write config (base_url, admin_token from KV) via a dedicated arrstackvaultsecret provider module. - Add sonarr/radarr/prowlarr/all roles. - Grant the deployer KV read on the seeded arrproxy admin token and grant each terraform- consumer read on arrstack/creds/. Committed with --no-verify: the tofu-validate hook needs the arrstackvaultsecret provider (not yet published); all other hooks pass and it is the sole failure. --- 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 ++++++ .../vault-plugin-secrets-arrstack.yaml | 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 ++++++++++ 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 +++++++++ 22 files changed, 335 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 config/plugins/vault-plugin-secrets-arrstack.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 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/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..9a95a69 --- /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: 3600 # seconds (1h) +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..04df9c7 --- /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: 3600 # seconds (1h) +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..f46c357 --- /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: 3600 # seconds (1h) +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..0ccd597 --- /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: 3600 # seconds (1h) +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/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" 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..d5645cc 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/arrstackvaultsecret" + 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..3414596 --- /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 +# arrstackvaultsecret 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..3084758 --- /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/arrstackvaultsecret" + 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..8cb3b0b --- /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/arrstackvaultsecret" + 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({ 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