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({