From f1b6751257ec8f846489d21ef1ff6f293ccbd844 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 9 Aug 2026 12:59:28 +1000 Subject: [PATCH 1/3] Register the netbox secrets plugin in the catalog Why: - The netbox secrets engine cannot be mounted until its plugin binary is registered in the OpenBao catalog, so the catalog entry must land before any engine mount or role config references it. How: - Add config/plugins/vault-plugin-secrets-netbox.yaml registering the plugin as a secret plugin, pinned to the released v0.1.0 binary sha256 that Puppet installs on the OpenBao nodes. Bump the sha in lockstep with any RPM upgrade. --- config/plugins/vault-plugin-secrets-netbox.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 config/plugins/vault-plugin-secrets-netbox.yaml diff --git a/config/plugins/vault-plugin-secrets-netbox.yaml b/config/plugins/vault-plugin-secrets-netbox.yaml new file mode 100644 index 0000000..9ff80c1 --- /dev/null +++ b/config/plugins/vault-plugin-secrets-netbox.yaml @@ -0,0 +1,11 @@ +# config/plugins/vault-plugin-secrets-netbox.yaml +# Imports (registers) the netbox secrets plugin in the catalog. Filename = +# catalog name = mount type. The binary is installed on the OpenBao nodes by +# Puppet (openbao-plugin-secrets-netbox RPM -> +# /opt/openbao-plugins/vault-plugin-secrets-netbox). +# +# sha256 pins the released v0.1.0 binary; bump it in lockstep with any RPM +# upgrade or OpenBao will refuse to launch the plugin. +type: secret +command: vault-plugin-secrets-netbox +sha256: "362b7f6c9e21179ad51d2d810684d9387fe50e3a1887f171700122a0b2a05cef" -- 2.47.3 From e2cd80e222f053451b190b4101aa1eb222ee6dd9 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 9 Aug 2026 13:00:54 +1000 Subject: [PATCH 2/3] Add the netbox secrets engine modules and wiring Why: - Managing NetBox from Vault needs three capabilities the repo does not yet have: mounting the netbox engine, minting scoped tokens through roles, and creating the NetBox service users those roles mint tokens for. Landing the modules and config scaffolding before any backend or role data lets each concrete identity be added as pure data later. How: - Add three modules under modules/vault_cluster/modules: netbox_secret_backend (mount + engine config, admin token read from KV), netbox_secret_backend_role (mint ephemeral scoped tokens for a filename-derived NetBox username), and netbox_user_management (mirror consul_acl_management: read the seeded admin token, drive one e-breuninger/netbox provider per backend, and synthesize the NetBox user + object permissions from the role map's inline permissions). - Derive the netbox_secret_backend and netbox_secret_backend_role maps in config.hcl, deriving each role's name and netbox_username from its filename so the engine role and NetBox username match by construction. - Wire the three module blocks and their variables through vault_cluster and the syd1 terragrunt inputs, reusing the sanitized backend-alias pattern the Consul providers use. - Leave the backend and role maps empty: the modules stand ready and create nothing until backend and role config data are added. --- config/config.hcl | 14 ++++ environments/au/syd1/terragrunt.hcl | 12 +++ modules/vault_cluster/main.tf | 50 ++++++++++++ .../modules/netbox_secret_backend/main.tf | 32 ++++++++ .../netbox_secret_backend/terraform.tf | 13 +++ .../netbox_secret_backend/variables.tf | 55 +++++++++++++ .../netbox_secret_backend_role/main.tf | 13 +++ .../netbox_secret_backend_role/terraform.tf | 9 +++ .../netbox_secret_backend_role/variables.tf | 45 +++++++++++ .../netbox_user_management/.tflint.hcl | 7 ++ .../modules/netbox_user_management/main.tf | 81 +++++++++++++++++++ .../modules/netbox_user_management/outputs.tf | 19 +++++ .../netbox_user_management/terraform.tf | 17 ++++ .../netbox_user_management/variables.tf | 43 ++++++++++ modules/vault_cluster/variables.tf | 46 +++++++++++ 15 files changed, 456 insertions(+) create mode 100644 modules/vault_cluster/modules/netbox_secret_backend/main.tf create mode 100644 modules/vault_cluster/modules/netbox_secret_backend/terraform.tf create mode 100644 modules/vault_cluster/modules/netbox_secret_backend/variables.tf create mode 100644 modules/vault_cluster/modules/netbox_secret_backend_role/main.tf create mode 100644 modules/vault_cluster/modules/netbox_secret_backend_role/terraform.tf create mode 100644 modules/vault_cluster/modules/netbox_secret_backend_role/variables.tf create mode 100644 modules/vault_cluster/modules/netbox_user_management/.tflint.hcl create mode 100644 modules/vault_cluster/modules/netbox_user_management/main.tf create mode 100644 modules/vault_cluster/modules/netbox_user_management/outputs.tf create mode 100644 modules/vault_cluster/modules/netbox_user_management/terraform.tf create mode 100644 modules/vault_cluster/modules/netbox_user_management/variables.tf diff --git a/config/config.hcl b/config/config.hcl index 19b655d..cb6193f 100644 --- a/config/config.hcl +++ b/config/config.hcl @@ -252,5 +252,19 @@ locals { }) if startswith(file_path, "gitea_secret_backend_role/") } + netbox_secret_backend = { + for file_path, content in local.all_configs : + trimsuffix(basename(file_path), ".yaml") => content + if startswith(file_path, "netbox_secret_backend/") + } + netbox_secret_backend_role = { + for file_path, content in local.all_configs : + trimsuffix(replace(file_path, "netbox_secret_backend_role/", ""), ".yaml") => merge(content, { + name = trimsuffix(basename(file_path), ".yaml") + netbox_username = trimsuffix(basename(file_path), ".yaml") + backend = dirname(replace(file_path, "netbox_secret_backend_role/", "")) + }) + if startswith(file_path, "netbox_secret_backend_role/") + } } } diff --git a/environments/au/syd1/terragrunt.hcl b/environments/au/syd1/terragrunt.hcl index 98a9415..44b3338 100644 --- a/environments/au/syd1/terragrunt.hcl +++ b/environments/au/syd1/terragrunt.hcl @@ -39,6 +39,12 @@ locals { for backend_name, _ in local.config.consul_secret_backend : backend_name => replace(backend_name, "/", "_") } + + # Same sanitized alias mapping for the NetBox providers. + netbox_backend_aliases = { + for backend_name, _ in local.config.netbox_secret_backend : + backend_name => replace(backend_name, "/", "_") + } } terraform { @@ -81,10 +87,16 @@ inputs = { gitea_secret_backend = local.config.gitea_secret_backend gitea_secret_backend_role = local.config.gitea_secret_backend_role + netbox_secret_backend = local.config.netbox_secret_backend + netbox_secret_backend_role = local.config.netbox_secret_backend_role + # Pass policy maps to vault_cluster module policy_auth_map = local.policies.policy_auth_map policy_rules_map = local.policies.policy_rules_map # Pass sanitized consul backend aliases for provider configuration consul_backend_aliases = local.consul_backend_aliases + + # Pass sanitized netbox backend aliases for provider configuration + netbox_backend_aliases = local.netbox_backend_aliases } diff --git a/modules/vault_cluster/main.tf b/modules/vault_cluster/main.tf index e7851bb..bfd7e0d 100644 --- a/modules/vault_cluster/main.tf +++ b/modules/vault_cluster/main.tf @@ -456,6 +456,56 @@ module "gitea_secret_backend_role" { depends_on = [module.gitea_secret_backend] } +module "netbox_secret_backend" { + source = "./modules/netbox_secret_backend" + + for_each = var.netbox_secret_backend + + path = each.key + plugin = each.value.plugin + description = each.value.description + netbox_url = each.value.netbox_url + token_version = each.value.token_version + country = var.country + region = var.region + ca_cert = each.value.ca_cert + tls_skip_verify = each.value.tls_skip_verify + request_timeout_seconds = each.value.request_timeout_seconds + + depends_on = [module.plugin] +} + +# Declaratively manage the NetBox service users + object permissions the engine +# roles mint tokens for, using the seeded admin token (mirrors consul_acl_management). +# Consumes the SAME role config as netbox_secret_backend_role: one file per identity, +# filename-derived username, inline permissions. +module "netbox_user_management" { + source = "./modules/netbox_user_management" + + country = var.country + region = var.region + netbox_backends = var.netbox_secret_backend + netbox_roles = var.netbox_secret_backend_role + netbox_backend_aliases = var.netbox_backend_aliases +} + +module "netbox_secret_backend_role" { + source = "./modules/netbox_secret_backend_role" + + for_each = var.netbox_secret_backend_role + + backend = each.value.backend + name = each.value.name + netbox_username = each.value.netbox_username + netbox_user_id = each.value.netbox_user_id + write_enabled = each.value.write_enabled + description = each.value.description + ttl = each.value.ttl + max_ttl = each.value.max_ttl + + depends_on = [module.netbox_secret_backend, module.netbox_user_management] +} + module "vault_policy" { source = "./modules/vault_policy" diff --git a/modules/vault_cluster/modules/netbox_secret_backend/main.tf b/modules/vault_cluster/modules/netbox_secret_backend/main.tf new file mode 100644 index 0000000..19c99b0 --- /dev/null +++ b/modules/vault_cluster/modules/netbox_secret_backend/main.tf @@ -0,0 +1,32 @@ +# Mounts the netbox secrets engine and writes its connection config via the +# vault-secrets-netbox provider. The plugin is registered ("imported") in the +# catalog separately (config/plugins/vault-plugin-secrets-netbox.yaml). The +# seeded NetBox admin token is sensitive and read from KV, not stored in git: +# kv/service/vault///secret_backend//config +# Expected key: admin_token (a NetBox token with add_token + grant_token, i.e. +# able to provision and delegate per-user API tokens). +data "vault_kv_secret_v2" "config" { + mount = "kv" + name = "service/vault/${var.country}/${var.region}/secret_backend/${var.path}/config" +} + +resource "netbox_secret_backend" "this" { + path = var.path + plugin = var.plugin + description = var.description + netbox_url = var.netbox_url + token = data.vault_kv_secret_v2.config.data["admin_token"] + token_version = var.token_version + ca_cert = var.ca_cert + tls_skip_verify = var.tls_skip_verify + request_timeout_seconds = var.request_timeout_seconds + + lifecycle { + # The KV seed is a bootstrap credential: it is consumed only when the engine + # config is first created. After creation the live admin token is rotated in + # place (vault write -f netbox/config/rotate) and diverges from the seed, so + # re-reading the (possibly stale) KV value must never push it back. Ignoring + # the token makes this module create-only for it (mirrors gitea/config). + ignore_changes = [token] + } +} diff --git a/modules/vault_cluster/modules/netbox_secret_backend/terraform.tf b/modules/vault_cluster/modules/netbox_secret_backend/terraform.tf new file mode 100644 index 0000000..4f336fd --- /dev/null +++ b/modules/vault_cluster/modules/netbox_secret_backend/terraform.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.10" + required_providers { + vault = { + source = "hashicorp/vault" + version = "5.6.0" + } + netbox = { + source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/vault-secrets-netbox" + version = "0.1.0" + } + } +} diff --git a/modules/vault_cluster/modules/netbox_secret_backend/variables.tf b/modules/vault_cluster/modules/netbox_secret_backend/variables.tf new file mode 100644 index 0000000..c830f68 --- /dev/null +++ b/modules/vault_cluster/modules/netbox_secret_backend/variables.tf @@ -0,0 +1,55 @@ +variable "path" { + description = "Mount path of the netbox secrets engine (e.g. \"netbox\")" + type = string +} + +variable "plugin" { + description = "Registered plugin name to mount (the catalog name = mount type)" + type = string + default = "vault-plugin-secrets-netbox" +} + +variable "description" { + description = "Human-friendly description of the mount" + type = string + default = null +} + +variable "netbox_url" { + description = "Base URL of the NetBox server (e.g. https://netbox.k8s.syd1.au.unkin.net)" + type = string +} + +variable "token_version" { + description = "NetBox API token format: 2 (default, requires API_TOKEN_PEPPERS on the NetBox server) or 1 (legacy plaintext-key)." + type = number + default = 2 +} + +variable "country" { + description = "Country segment of the KV path holding the seeded admin token" + type = string +} + +variable "region" { + description = "Region segment of the KV path holding the seeded admin token" + type = string +} + +variable "ca_cert" { + description = "PEM CA certificate that signed the NetBox server's TLS cert (optional; omit to use the system trust store)" + type = string + default = null +} + +variable "tls_skip_verify" { + description = "Skip TLS verification of the NetBox server (not recommended)" + type = bool + default = false +} + +variable "request_timeout_seconds" { + description = "HTTP timeout in seconds for calls from the plugin to NetBox" + type = number + default = 30 +} diff --git a/modules/vault_cluster/modules/netbox_secret_backend_role/main.tf b/modules/vault_cluster/modules/netbox_secret_backend_role/main.tf new file mode 100644 index 0000000..59246f3 --- /dev/null +++ b/modules/vault_cluster/modules/netbox_secret_backend_role/main.tf @@ -0,0 +1,13 @@ +# A role that mints short-lived, scoped NetBox tokens for a pre-existing NetBox +# service user. Reading netbox/creds/ produces a lease-bound token that is +# deleted from NetBox when the lease is revoked or reaches max_ttl. +resource "netbox_secret_backend_role" "this" { + backend = var.backend + name = var.name + netbox_username = var.netbox_username + netbox_user_id = var.netbox_user_id + write_enabled = var.write_enabled + description = var.description + ttl = var.ttl + max_ttl = var.max_ttl +} diff --git a/modules/vault_cluster/modules/netbox_secret_backend_role/terraform.tf b/modules/vault_cluster/modules/netbox_secret_backend_role/terraform.tf new file mode 100644 index 0000000..d86a21f --- /dev/null +++ b/modules/vault_cluster/modules/netbox_secret_backend_role/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + netbox = { + source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/vault-secrets-netbox" + version = "0.1.0" + } + } +} diff --git a/modules/vault_cluster/modules/netbox_secret_backend_role/variables.tf b/modules/vault_cluster/modules/netbox_secret_backend_role/variables.tf new file mode 100644 index 0000000..da03cbd --- /dev/null +++ b/modules/vault_cluster/modules/netbox_secret_backend_role/variables.tf @@ -0,0 +1,45 @@ +variable "backend" { + description = "Mount path of the netbox secrets engine this role belongs to" + type = string +} + +variable "name" { + description = "Role name (read netbox/creds/ to mint a token)" + type = string +} + +variable "netbox_username" { + description = "NetBox service username the minted tokens belong to (set this or netbox_user_id)" + type = string + default = null +} + +variable "netbox_user_id" { + description = "NetBox service user id the minted tokens belong to (set this or netbox_username)" + type = number + default = null +} + +variable "write_enabled" { + description = "Whether minted tokens carry NetBox write access (default read-only)" + type = bool + default = false +} + +variable "description" { + description = "Human-friendly description of the role" + type = string + default = null +} + +variable "ttl" { + description = "Default lease TTL in seconds for minted tokens (the token's NetBox expiry is aligned to the lease)" + type = number + default = null +} + +variable "max_ttl" { + description = "Maximum lease TTL in seconds for minted tokens" + type = number + default = null +} diff --git a/modules/vault_cluster/modules/netbox_user_management/.tflint.hcl b/modules/vault_cluster/modules/netbox_user_management/.tflint.hcl new file mode 100644 index 0000000..3657e1d --- /dev/null +++ b/modules/vault_cluster/modules/netbox_user_management/.tflint.hcl @@ -0,0 +1,7 @@ +rule "terraform_required_providers" { + enabled = false +} + +rule "terraform_required_version" { + enabled = false +} diff --git a/modules/vault_cluster/modules/netbox_user_management/main.tf b/modules/vault_cluster/modules/netbox_user_management/main.tf new file mode 100644 index 0000000..a5692d0 --- /dev/null +++ b/modules/vault_cluster/modules/netbox_user_management/main.tf @@ -0,0 +1,81 @@ +# Read the seeded NetBox admin token for each backend from KV. This is the same +# token the netbox secrets engine is configured with (key admin_token), and it +# must carry add_token + grant_token (or superuser) to create users/permissions. +data "vault_kv_secret_v2" "netbox_backend_configs" { + for_each = var.netbox_backends + + mount = "kv" + name = "service/vault/${var.country}/${var.region}/secret_backend/${each.key}/config" +} + +# One NetBox provider instance per backend, authenticated with its admin token. +provider "netbox" { + alias = "by_backend" + for_each = var.netbox_backend_aliases + + server_url = var.netbox_backends[each.key].netbox_url + api_token = data.vault_kv_secret_v2.netbox_backend_configs[each.key].data["admin_token"] + allow_insecure_https = var.netbox_backends[each.key].tls_skip_verify + # NetBox is internal and not always reachable at plan time; the resource CRUD + # calls surface any real incompatibility, so skip the startup version probe. + skip_version_check = true +} + +# NetBox users authenticate only via Vault-minted API tokens, never the web UI, +# so give each a random unknown password (required by the API) that no one holds. +resource "random_password" "user" { + for_each = var.netbox_roles + + length = 32 + special = true +} + +# Declarative NetBox service users, one per engine role. The role's filename- +# derived name is the username, so the engine role and its user match 1:1. +resource "netbox_user" "users" { + for_each = var.netbox_roles + + provider = netbox.by_backend[each.value.backend] + + username = each.value.name + password = random_password.user[each.key].result + active = each.value.active + staff = each.value.staff + email = each.value.email +} + +locals { + # Flatten roles x permissions into one map keyed by ":". A + # permission's name defaults to the role name (the username) so a single- + # permission identity repeats nothing already encoded by the filename. + netbox_permissions = merge([ + for role_key, role in var.netbox_roles : { + for idx, perm in role.permissions : + "${role_key}:${idx}" => { + backend = role.backend + user = role_key + name = coalesce(perm.name, length(role.permissions) == 1 ? role.name : "${role.name}-${idx}") + object_types = perm.object_types + actions = perm.actions + constraints = perm.constraints + description = perm.description + enabled = perm.enabled + } + } + ]...) +} + +# Object permissions granting each user its object-type/action scope. +resource "netbox_permission" "perms" { + for_each = local.netbox_permissions + + provider = netbox.by_backend[each.value.backend] + + name = each.value.name + object_types = each.value.object_types + actions = each.value.actions + enabled = each.value.enabled + description = each.value.description + constraints = each.value.constraints + users = [tonumber(netbox_user.users[each.value.user].id)] +} diff --git a/modules/vault_cluster/modules/netbox_user_management/outputs.tf b/modules/vault_cluster/modules/netbox_user_management/outputs.tf new file mode 100644 index 0000000..d7f4472 --- /dev/null +++ b/modules/vault_cluster/modules/netbox_user_management/outputs.tf @@ -0,0 +1,19 @@ +output "netbox_users" { + description = "Map of created NetBox users (id + username; password is intentionally omitted)" + value = { + for k, u in netbox_user.users : k => { + id = u.id + username = u.username + } + } +} + +output "netbox_permissions" { + description = "Map of created NetBox object permissions" + value = { + for k, p in netbox_permission.perms : k => { + id = p.id + name = p.name + } + } +} diff --git a/modules/vault_cluster/modules/netbox_user_management/terraform.tf b/modules/vault_cluster/modules/netbox_user_management/terraform.tf new file mode 100644 index 0000000..ee20fc0 --- /dev/null +++ b/modules/vault_cluster/modules/netbox_user_management/terraform.tf @@ -0,0 +1,17 @@ +terraform { + required_version = ">= 1.10" + required_providers { + vault = { + source = "hashicorp/vault" + version = "5.6.0" + } + netbox = { + source = "e-breuninger/netbox" + version = "4.3.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.5" + } + } +} diff --git a/modules/vault_cluster/modules/netbox_user_management/variables.tf b/modules/vault_cluster/modules/netbox_user_management/variables.tf new file mode 100644 index 0000000..051907c --- /dev/null +++ b/modules/vault_cluster/modules/netbox_user_management/variables.tf @@ -0,0 +1,43 @@ +variable "netbox_backends" { + description = "Map of netbox secret backends (keyed by mount path); only the URL and TLS mode are needed to reach NetBox" + type = map(object({ + netbox_url = string + tls_skip_verify = optional(bool, false) + })) +} + +variable "netbox_roles" { + description = "Map of netbox engine roles (the netbox_secret_backend_role config). Each role's filename-derived name is the NetBox username to create, and its permissions block is the user's object-permission set. Keyed by the role's config path." + type = map(object({ + name = string + backend = string + active = optional(bool, true) + staff = optional(bool, false) + email = optional(string) + permissions = optional(list(object({ + name = optional(string) + object_types = list(string) + actions = optional(list(string), ["view", "add", "change", "delete"]) + constraints = optional(string) + description = optional(string) + enabled = optional(bool, true) + })), []) + })) + default = {} +} + +variable "netbox_backend_aliases" { + description = "Map of netbox backend names to sanitized provider aliases" + type = map(string) + default = {} +} + +variable "country" { + description = "Country identifier" + type = string +} + +variable "region" { + description = "Region identifier" + type = string +} diff --git a/modules/vault_cluster/variables.tf b/modules/vault_cluster/variables.tf index 87787f5..be7b04d 100644 --- a/modules/vault_cluster/variables.tf +++ b/modules/vault_cluster/variables.tf @@ -416,6 +416,52 @@ variable "gitea_secret_backend_role" { default = {} } +variable "netbox_secret_backend" { + description = "Map of netbox token secret engines to create (mount + config; seeded admin token read from KV)" + type = map(object({ + plugin = optional(string, "vault-plugin-secrets-netbox") + description = optional(string) + netbox_url = string + token_version = optional(number, 2) + ca_cert = optional(string) + tls_skip_verify = optional(bool, false) + request_timeout_seconds = optional(number, 30) + })) + default = {} +} + +variable "netbox_secret_backend_role" { + description = "Map of netbox engine roles; each role's filename-derived name is both the engine role and the NetBox username it mints tokens for, and its permissions block is the user's object-permission set" + type = map(object({ + name = string + backend = string + netbox_username = optional(string) + netbox_user_id = optional(number) + write_enabled = optional(bool, false) + description = optional(string) + ttl = optional(number) + max_ttl = optional(number) + active = optional(bool, true) + staff = optional(bool, false) + email = optional(string) + permissions = optional(list(object({ + name = optional(string) + object_types = list(string) + actions = optional(list(string), ["view", "add", "change", "delete"]) + constraints = optional(string) + description = optional(string) + enabled = optional(bool, true) + })), []) + })) + default = {} +} + +variable "netbox_backend_aliases" { + description = "Map of netbox backend names to sanitized provider aliases" + type = map(string) + default = {} +} + variable "policy_auth_map" { description = "Map of auth mounts -> auth roles -> policy names" type = map(map(list(string))) -- 2.47.3 From df3e8b017c2a5a67edcf4288a03b6a887c5f510b Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 9 Aug 2026 13:01:41 +1000 Subject: [PATCH 3/3] Add the netbox backend and terraform-infra role Why: - The netbox engine modules stand ready but mount nothing and create no identity until backend and role data exist, so terraform-infra still reads a static NetBox token instead of minting ephemeral scoped tokens. How: - Add config/netbox_secret_backend/netbox.yaml to mount the engine at netbox and point it at the syd1 NetBox URL; the admin token is read from KV, not stored here. - Add config/netbox_secret_backend_role/netbox/terraform-infra.yaml as the single declarative source for the terraform-infra identity: filename-derived role name and NetBox username, write access, short TTLs, and an inline permissions block. Nothing in the file repeats the filename. - Scope terraform-infra to view/add/change/delete on the IPAM/DCIM objects it manages: prefixes, ip-addresses, ip-ranges, devices, interfaces, mac addresses. - Add policies/netbox/creds/terraform-infra.yaml letting the terraform-infra AppRole and its Woodpecker k8s role read netbox/creds/terraform-infra; it attaches to nothing until the separate terraform-infra Vault onboarding lands. --- config/netbox_secret_backend/netbox.yaml | 16 ++++++++++++ .../netbox/terraform-infra.yaml | 25 +++++++++++++++++++ policies/netbox/creds/terraform-infra.yaml | 21 ++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 config/netbox_secret_backend/netbox.yaml create mode 100644 config/netbox_secret_backend_role/netbox/terraform-infra.yaml create mode 100644 policies/netbox/creds/terraform-infra.yaml diff --git a/config/netbox_secret_backend/netbox.yaml b/config/netbox_secret_backend/netbox.yaml new file mode 100644 index 0000000..4e22f42 --- /dev/null +++ b/config/netbox_secret_backend/netbox.yaml @@ -0,0 +1,16 @@ +# Mounts the netbox token secrets engine at "netbox" and writes its config. +# The seeded NetBox admin token is sensitive and read from KV, not stored here: +# kv/service/vault/au/syd1/secret_backend/netbox/config +# -> key: admin_token (required) +# Populate that KV path with a purpose-built NetBox service token that has +# add_token + grant_token (or superuser) BEFORE applying, then run +# `vault write -f netbox/config/rotate` after the first apply so only Vault +# holds the live admin token. +# +# token_version 2 is the NetBox 4.6.5 default and requires API_TOKEN_PEPPERS to +# be configured on the NetBox server; set token_version: 1 here if the server +# has no peppers. +description: "NetBox ephemeral scoped API token engine" +netbox_url: "https://netbox.k8s.syd1.au.unkin.net" +token_version: 2 +request_timeout_seconds: 30 diff --git a/config/netbox_secret_backend_role/netbox/terraform-infra.yaml b/config/netbox_secret_backend_role/netbox/terraform-infra.yaml new file mode 100644 index 0000000..ac390ac --- /dev/null +++ b/config/netbox_secret_backend_role/netbox/terraform-infra.yaml @@ -0,0 +1,25 @@ +# Single declarative source for the terraform-infra NetBox service identity. The +# filename stem is the engine role name AND the NetBox username (1:1); config.hcl +# derives both from it, so neither is repeated below. Creating this file creates +# the user: the netbox_user_management module synthesizes the NetBox user + object +# permissions from the permissions block, and the engine role mints ephemeral +# tokens for that same user. write_enabled true because terraform-infra manages +# NetBox IPAM/DCIM; very short TTLs because a token is minted per plan/apply and +# revoked when the run's lease ends. +--- +write_enabled: true +ttl: 120 # 2m +max_ttl: 300 # 5m +permissions: + - object_types: + - ipam.prefix + - ipam.ipaddress + - ipam.iprange + - dcim.device + - dcim.interface + - dcim.macaddress + actions: + - view + - add + - change + - delete diff --git a/policies/netbox/creds/terraform-infra.yaml b/policies/netbox/creds/terraform-infra.yaml new file mode 100644 index 0000000..d7ff814 --- /dev/null +++ b/policies/netbox/creds/terraform-infra.yaml @@ -0,0 +1,21 @@ +# Allow the terraform-infra runner to mint an ephemeral NetBox token from the +# terraform-infra role (netbox/creds/terraform-infra), replacing the static +# netbox_token it used to read from kv/service/terraform/*. The e-breuninger +# netbox provider authenticates with the minted token; the lease revokes it when +# the run ends. +# +# Bound to both the terraform-infra AppRole and its Woodpecker k8s auth role, +# mirroring the terraform-ipam pattern. Both principals are created by the +# terraform-infra Vault onboarding (separate from this netbox change); until +# that onboarding lands this policy exists but attaches to nothing. +--- +rules: + - path: "netbox/creds/terraform-infra" + capabilities: + - read + +auth: + approle: + - terraform_infra + k8s/au/syd1: + - woodpecker_terraform_infra -- 2.47.3