vault: mount arrstack engine + config + roles (3/3) #127

Merged
benvin merged 5 commits from benvin/arrstack-resources into master 2026-08-21 00:09:47 +10:00
16 changed files with 237 additions and 0 deletions
@@ -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
@@ -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.
@@ -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.
@@ -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.
@@ -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.
+13
View File
@@ -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, {
+2
View File
@@ -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
+10
View File
@@ -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
+29
View File
@@ -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"
@@ -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
}
@@ -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"
}
}
}
@@ -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
}
@@ -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
}
@@ -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"
}
}
}
@@ -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
}
+24
View File
@@ -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({