Files
unkinben 07bd94c55a Wire API_TOKEN_PEPPERS into NetBox config (#346)
## Why

NetBox 4.6.5 refuses to save v2 API tokens without `API_TOKEN_PEPPERS` ("Unable to save v2 tokens: API_TOKEN_PEPPERS is not defined"), which blocks creating the superuser token the NetBox Vault engine needs (it defaults to v2 tokens). The chart only auto-generates a pepper when it creates the config secret itself; it does not do that while `existingSecret` (`netbox-secret-key`) is set, so the config secret carries no `api_token_peppers` key.

## Changes

- Document `api_token_peppers` on the `netbox-secret-key` VaultStaticSecret: a JSON pepper map `{"1": "<random>"}` seeded once into Vault alongside `secret_key`. VSO syncs every key at the path into the config secret, which the chart already mounts as an optional file into `API_TOKEN_PEPPERS`.
- Add a reloader annotation via `commonAnnotations` so the `netbox` and `netbox-worker` Deployments roll when `netbox-secret-key` changes, picking up the seeded pepper (and any rotated `secret_key`) without a manual restart.

## Follow-up (out of band)

seed the pepper once (rotating it invalidates existing v2 tokens):

```
PEP=$(openssl rand -base64 48 | tr -d '\n')
vault kv patch kv/kubernetes/namespace/netbox/default/netbox-secret-key \
  api_token_peppers="{\"1\": \"$PEP\"}"
```

---------

Co-authored-by: Ben Vincent <neotheo@gmail.com>
Reviewed-on: #346
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-09 12:33:30 +10:00

151 lines
4.7 KiB
YAML

---
# NetBox (netbox-community chart 8.3.40, appVersion v4.6.5).
# Postgres = external CNPG cluster, Valkey = standalone Deployment, both defined
# in apps/base/netbox. Secrets come from Vault via VSO (see base/vaultstaticsecret.yaml).
image:
# ghcr.io is mirrored through artifactapi at the containerd level; the pull is
# gated by the ^netbox-community/ allowlist added in terraform-artifactapi.
# tag defaults to the chart appVersion (v4.6.5).
registry: ghcr.io
repository: netbox-community/netbox
# Two web replicas for zero-downtime rollouts (media is RWX, see below).
replicaCount: 2
# Config secret. Must contain key: secret_key (Django SECRET_KEY). Also carries
# api_token_peppers — a JSON object {"1": "<50+ char random>"} the chart mounts
# (optional file) into API_TOKEN_PEPPERS; NetBox 4.6 refuses to save v2 tokens
# without it. The chart only auto-generates a pepper when it creates this secret
# itself, which it does NOT do while existingSecret is set — so the pepper is
# seeded into Vault alongside secret_key (see base/vaultstaticsecret.yaml).
existingSecret: netbox-secret-key
# Bootstrap superuser — existingSecret keys: username, password, email, api_token.
superuser:
existingSecret: netbox-superuser
# Roll NetBox (web + worker) when the config secret changes, so a Vault-seeded
# api_token_peppers (or a rotated secret_key) is picked up without a manual
# restart. commonAnnotations lands on Deployment metadata, where reloader reads it.
commonAnnotations:
secret.reloader.stakater.com/reload: netbox-secret-key
# Disable the bundled Bitnami subcharts; we bring our own Postgres and Valkey.
postgresql:
enabled: false
valkey:
enabled: false
# External Postgres = the CNPG cluster's pgbouncer pooler (session mode).
externalDatabase:
host: netbox-postgres-pooler-rw
port: 5432
database: netbox
username: netbox
existingSecretName: postgres-credentials
existingSecretKey: password
disableServerSideCursors: false
# External Valkey — one instance, DB 0 = RQ task queue, DB 1 = cache. No auth
# (in-cluster, namespace-isolated).
tasksDatabase:
host: netbox-valkey
port: 6379
database: 0
cachingDatabase:
host: netbox-valkey
port: 6379
database: 1
# Uploaded media/attachments, shared across web replicas (RWX CephFS).
persistence:
enabled: true
storageClass: cephfs-raid6-delete
accessMode: ReadWriteMany
size: 5Gi
allowedHosts:
- netbox.k8s.syd1.au.unkin.net
serviceAccount:
create: true
automountServiceAccountToken: false
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: "1"
memory: 2Gi
worker:
replicaCount: 1
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
# --- Authentik OIDC SSO via python-social-auth ---
# remoteAuth wires REMOTE_AUTH_ENABLED + REMOTE_AUTH_BACKEND; the SOCIAL_AUTH_*
# settings are supplied via extraConfig (YAML fragments the chart loads into the
# NetBox config). New OIDC users are auto-provisioned on first login.
remoteAuth:
enabled: true
backends:
- social_core.backends.open_id_connect.OpenIdConnectAuth
autoCreateUser: true
extraConfig:
# index 0 -> /run/config/extra/0/extra-0.yaml (non-secret OIDC config).
# Canonical Authentik host identity.unkin.net (served by the internal unkin.net
# CA; trusted via the combined bundle mounted below).
- values:
SOCIAL_AUTH_OIDC_OIDC_ENDPOINT: https://identity.unkin.net/application/o/netbox/
SOCIAL_AUTH_OIDC_KEY: netbox
# index 1 -> /run/config/extra/1/oidc.yaml (client secret, from Vault via VSO)
- secret:
secretName: oauth-credentials
items:
- key: oidc.yaml
path: oidc.yaml
# python-social-auth uses `requests` to reach identity.unkin.net, whose cert is
# signed by the internal unkin.net CA. Combine the image's public roots with the
# reflected vault-ca-cert into one bundle and point requests/OpenSSL at it, so
# both internal (OIDC) and public HTTPS keep working.
initContainers:
- name: combine-certs
image: alpine:3
command:
- sh
- -c
- cat /etc/ssl/certs/ca-certificates.crt /custom-ca/ca.crt > /combined-certs/ca-certificates.crt
volumeMounts:
- name: vault-ca-cert
mountPath: /custom-ca
readOnly: true
- name: combined-certs
mountPath: /combined-certs
extraVolumes:
- name: vault-ca-cert
secret:
secretName: vault-ca-cert
items:
- key: ca.crt
path: ca.crt
- name: combined-certs
emptyDir: {}
extraVolumeMounts:
- name: combined-certs
mountPath: /etc/ssl/combined
readOnly: true
extraEnvs:
- name: REQUESTS_CA_BUNDLE
value: /etc/ssl/combined/ca-certificates.crt
- name: SSL_CERT_FILE
value: /etc/ssl/combined/ca-certificates.crt