Files
bootapi/docs/security.md
T
unkinben 274c480b09
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Initial bootapi: NetBox-driven PXE/kickstart boot service
bootapi replaces Cobbler's PXE/kickstart side. It resolves a PXE-booting host
from NetBox (by MAC or hostname), renders an iPXE boot script and a kickstart
from Go text/templates, and serves them over HTTP. The ENC half already moved to
encapi; this covers the provisioning/boot half.

What's here:
- cmd/bootapi + internal/{config,model,netbox,render,server}; embedded default
  templates under templates/ (AlmaLinux 9 + Fedora kickstarts, iPXE boot +
  unknown-MAC fallbacks) ported from Cobbler's boot/bootstrap contract.
- NetBox client (v4.x API) behind a Resolver interface with a short-TTL cache;
  tested against httptest fixtures using real NetBox JSON shapes.
- chi HTTP server: /ipxe/{mac}, /boot/ipxe?mac=, /ks/{ident}, healthz/readyz,
  Prometheus /metrics. Unknown MAC -> safe fallback iPXE (200), unknown KS -> 404.
- Secrets (root pw hash, ssh keys) injected at render time from env/Vault, never
  NetBox. Config is env-based per estate convention.
- Makefile (build/test/lint/docker + patch/minor/major), Dockerfile (distroless),
  .woodpecker (pre-commit, golangci-lint v2 + go test -race, docker build on PR;
  image push + Gitea binary release on v* tag), docs/ and example config.

go build/vet clean, go test -race green, golangci-lint v2 clean, pre-commit clean.

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-28 20:57:11 +10:00

2.7 KiB

Security: secrets in kickstarts

A kickstart is fetched over the network by an unauthenticated installer and can embed real secrets: the root password hash, SSH keys, bootstrap tokens, repo credentials. bootapi's rule is:

NetBox holds identity and topology, never secrets. Secrets are injected at render time from Vault/env.

What goes where

Value Where it lives How it reaches the template
hostname, domain, IPs, MACs, VLANs, gateway, platform, role NetBox internal/netboxmodel.Host
template selection knobs (provision_template, nameservers, gateway) NetBox custom fields .Custom / typed fields
root password hash Vault → BOOTAPI_ROOT_PASSWORD_HASH[_FILE] .RootPasswordHash
SSH authorized keys Vault → BOOTAPI_SSH_AUTHORIZED_KEYS .SSHAuthorizedKeys
puppet CA/server names env (not secret) .PuppetServer / .PuppetCAServer

BOOTAPI_ROOT_PASSWORD_HASH_FILE and BOOTAPI_NETBOX_TOKEN_FILE let the values arrive as Vault-mounted files rather than env, which is the k8s norm (see deployment.md). If no root hash is configured, the default templates emit rootpw --lock rather than a blank/guessable password.

This matches the pre-bootapi setup, where the root hash was Cobbler's eyaml default_password_crypted injected into settings.yaml — an operator-managed secret, never in the NetBox/inventory layer.

Exposure notes

  • Kickstarts are served over HTTP to the installer, so treat any embedded secret as visible to anything on the provisioning VLAN. Keep bootapi's kickstart endpoint on the trusted PXE network, exactly as Cobbler's was.
  • The root password hash is in the rendered kickstart by necessity (Anaconda needs it). Prefer SSH-key login + a locked or strong-random root password, and rotate the hash in Vault as normal.
  • The puppet bootstrap uses no long-lived token: the host generates a CSR and the puppetmaster autosigns it based on source subnet + *.main.unkin.net (unchanged from Cobbler). So the kickstart carries no puppet secret.

Follow-up: per-template Vault lookups

Today all render-time secrets are process-wide env/files (one root hash, one key set for the fleet), which covers the current estate. If per-host or per-role secrets are ever needed (e.g. a distinct bootstrap token per role), the seam is internal/render.dataFor: add a Vault kv fetch keyed by host/role there, behind an interface, the same way encapi's internal/distro resolver injects per-host params behind an interface. Tracked as a follow-up, not implemented, to avoid giving bootapi broad Vault read scope before it's needed.