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
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/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). 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.