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

50 lines
2.7 KiB
Markdown

# 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/netbox``model.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](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.