Add NetBox secrets engine: mount, roles, policies
Why: - terraform-infra and the puppet netbox fact use static NetBox API tokens; the released vault-plugin-secrets-netbox mints per-consumer ephemeral tokens with lease-bound revocation, removing the long-lived secrets. How: - Register the vault-plugin-secrets-netbox v0.1.0 plugin in the catalog (config/plugins), pinning the released binary sha256. - Add netbox_secret_backend + netbox_secret_backend_role modules and their config/terragrunt wiring, mirroring the gitea engine. - Mount the engine at netbox with its admin token seeded from KV and rotated in place (lifecycle ignore_changes on the seed). - Add roles: puppet-facts (read-only, 24h/72h) and terraform-infra (write, 1h/4h), each mapped to its NetBox service user. - Add the netbox admin policy for the deployer and a netbox/creds/terraform-infra read policy bound to the terraform-infra AppRole + k8s auth role.
This commit is contained in:
@@ -252,5 +252,18 @@ 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")
|
||||
backend = dirname(replace(file_path, "netbox_secret_backend_role/", ""))
|
||||
})
|
||||
if startswith(file_path, "netbox_secret_backend_role/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,16 @@
|
||||
# Role minting ephemeral read-only NetBox tokens for the puppet netbox fact.
|
||||
# The fact (profiles::netbox::facts) only GETs this node's device/VM + interface
|
||||
# data, so tokens are read-only (write_enabled false). Longer TTLs than the
|
||||
# terraform-infra role because the token feeds a persistent, cache-backed fact.
|
||||
# Reading netbox/creds/puppet-facts mints a lease-bound token deleted from
|
||||
# NetBox on revoke/expiry.
|
||||
#
|
||||
# There is no puppet-host -> Vault auth in the estate, so no consumer policy
|
||||
# binds netbox/creds/puppet-facts: an operator mints from this role by hand and
|
||||
# seeds the token into hieradata (profiles::netbox::facts::api_token). See the
|
||||
# PR body for the flagged design choice.
|
||||
---
|
||||
netbox_username: svc-puppet-facts
|
||||
write_enabled: false
|
||||
ttl: 86400 # 24h
|
||||
max_ttl: 259200 # 72h
|
||||
@@ -0,0 +1,10 @@
|
||||
# Role minting ephemeral NetBox tokens for the terraform-infra CI runner.
|
||||
# terraform-infra manages NetBox IPAM/devices, so tokens carry write access
|
||||
# (write_enabled true). Short TTLs because a token is minted per plan/apply and
|
||||
# revoked when the run's lease ends. Reading netbox/creds/terraform-infra mints
|
||||
# a lease-bound token deleted from NetBox on revoke/expiry.
|
||||
---
|
||||
netbox_username: svc-terraform-infra
|
||||
write_enabled: true
|
||||
ttl: 3600 # 1h
|
||||
max_ttl: 14400 # 4h
|
||||
@@ -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"
|
||||
@@ -81,6 +81,9 @@ 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
|
||||
|
||||
@@ -456,6 +456,42 @@ 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]
|
||||
}
|
||||
|
||||
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 "vault_policy" {
|
||||
source = "./modules/vault_policy"
|
||||
|
||||
|
||||
@@ -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/<country>/<region>/secret_backend/<path>/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]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# A role that mints short-lived, scoped NetBox tokens for a pre-existing NetBox
|
||||
# service user. Reading netbox/creds/<name> 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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/<name> 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
|
||||
}
|
||||
@@ -416,6 +416,35 @@ 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 token-minting roles to create"
|
||||
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)
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "policy_auth_map" {
|
||||
description = "Map of auth mounts -> auth roles -> policy names"
|
||||
type = map(map(list(string)))
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Allow the vault deployer to manage the netbox token secrets engine: its
|
||||
# connection config (seeded admin token), in-place token rotation, and
|
||||
# token-minting roles.
|
||||
#
|
||||
# Scoped to netbox/* only, and deliberately excludes netbox/creds/* - minting
|
||||
# tokens is for consumers, not the deployer. The plugin-catalog grant needed to
|
||||
# import the plugin is the shared, sudo-protected wildcard in
|
||||
# policies/sys/plugins/catalog/admin.yaml (already covers this plugin), and
|
||||
# mounting the engine uses the deployer's existing sys/mounts/* access, so no
|
||||
# new catalog/mount grant is added here (mirrors the gitea/rancher engines).
|
||||
---
|
||||
rules:
|
||||
# Engine connection config (NetBox URL, TLS, token_version, seeded admin token).
|
||||
- path: "netbox/config"
|
||||
capabilities:
|
||||
- create
|
||||
- read
|
||||
- update
|
||||
- delete
|
||||
# In-place rotation of the seeded admin token (write-only trigger).
|
||||
- path: "netbox/config/rotate"
|
||||
capabilities:
|
||||
- create
|
||||
- update
|
||||
# Token-minting roles.
|
||||
- path: "netbox/roles/*"
|
||||
capabilities:
|
||||
- create
|
||||
- read
|
||||
- update
|
||||
- delete
|
||||
- list
|
||||
- path: "netbox/roles"
|
||||
capabilities:
|
||||
- read
|
||||
- list
|
||||
|
||||
auth:
|
||||
approle:
|
||||
- tf_vault
|
||||
k8s/au/syd1:
|
||||
- woodpecker_terraform_vault
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user