# 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` | | **provision token** | Vault → `BOOTAPI_PROVISION_TOKEN[_FILE]` | `.ProvisionToken` | | 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. ## The provisioned callback token `POST /provisioned/{ident}` (which flips `pxe_enabled` off in NetBox) is guarded by `BOOTAPI_PROVISION_TOKEN`. The default kickstart `%post` calls it with that token in an `Authorization: Bearer` header, so **the token is embedded in every rendered kickstart** — treat it as a provisioning secret (same exposure class as the root hash: visible to anything on the provisioning VLAN). It only authorizes clearing a boot gate, not reading data. Rotate it in Vault as normal; empty disables the callback (fail closed). The call runs over plain HTTP by default because `%post` has no internal-CA trust yet; the token — not TLS — is what authenticates it. ## NetBox write scope bootapi performs exactly one NetBox write: `PATCH /api/dcim/devices/{id}/` setting `custom_fields.pxe_enabled=false` from the provisioned callback. Its NetBox token therefore needs **write on the device `pxe_enabled` custom field** in addition to read on devices/interfaces/ip-addresses. Scope the `bootapi` NetBox role/permission to just that (a NetBox object-permission constrained to `dcim.device` with the `pxe_enabled` field) rather than granting broad write. This is a deliberate, minimal escalation from the read-only design; it is called out here and in the deployment doc so the token is provisioned with the right (and only the right) scope. ## 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.