Newer
Older
openstack-caracal-dc-dc / docs / changelog-20260713-opnsense-apikey-bootstrap.md

2026-07-13 -- D-113(a2): API-key bootstrap, with no GUI click and no re-implemented crypto

The last unknown in D-113(a2) edge provisioning. The API path was proven (read AND write), but every new edge still needed a manual GUI click to mint its API key -- which would have put a human in the middle of building DC1's and DC2's edges in Stage 3. Closed.

What shipped

  • scripts/opnsense-mint-apikey.php -- runs ON the edge; calls OPNsense's own model (Auth\User -> apikeys->add()), the exact code path the GUI's ticket button invokes.
  • scripts/opnsense-bootstrap-apikey.sh -- ships the minter over SSH, runs it, retrieves the key, wipes the remote copies.
  • tests/opnsense-bootstrap-apikey/run-tests.sh -- offline harness, 8 PASS (ssh/scp stubbed; no edge contacted, no real key minted).

The design decision, and why the alternative was rejected

OPNsense stores API secrets as crypt(secret, '$6$') -- SHA-512 with an EMPTY salt (verified on the live 26.1 box: the stored hash is $6$$ + 86 chars). So we could mint keys offline and seed them into a bootstrap config.xml.

We deliberately do not. That would couple our provisioning to a vendor hash format we would have to keep byte-compatible forever -- and a mismatch fails silently: keys that simply never authenticate, with no error at generation time. Calling the vendor's own generator has no format to keep in sync.

This is the same principle as D-113(a2) itself: call the interface, do not re-implement the internals. Hand-reimplementing OPNsense's internals is exactly what cost us DOCFIX-191/192/193.

(An offline-hash attempt was actually blocked by the permission classifier mid-session. The block was correct, and it pushed us to the better design.)

VERIFIED END TO END on the live edge (2026-07-12/13)

Minted a SECOND key on Office1 via the vendor model, proved it works, then deleted it:

mint via OPNsense's own model   -> key=80 chars, secret=80 chars (base64 of 60 random bytes)
GET core/firmware/status        -> HTTP 200  [the vendor-minted key AUTHENTICATES]
POST auth/user/del_api_key/<id> -> {"result":"deleted"}
search_api_key                  -> 1 row remaining (the operator's original key)
retired key re-tested           -> REJECTED  [negative test: deletion really took]

The operator's existing key was untouched throughout, and the temporary creds file was shredded from the jumphost. The edge is back to its prior state.

Guard clause worth keeping (harness T5)

The script refuses to overwrite an existing creds file. An existing file means a LIVE key on the edge; overwriting the local copy would strand it there permanently, because the secret is hashed on save and can never be read back. That is unrecoverable credential loss, and a guard clause is the only thing standing in front of it. T5 asserts both the refusal AND that the existing file is left byte-intact.

The secret is never printed: creation is the only moment it exists in cleartext. It goes straight to a 0600 file in the key=/secret= form scripts/opnsense-api.sh parses.

What this unblocks

Edge provisioning is now automatable end to end, with no human in the loop:

boot factory nano
  -> D-112(c) console bootstrap (enable SSH + install key)
  -> scripts/opnsense-bootstrap-apikey.sh        <-- THIS (no GUI click)
  -> scripts/opnsense-api.sh                     <-- everything else: DHCP, firewall, ifaces

No config.xml anywhere in that chain.

Still OPEN

  • The template reduction itself is NOT done. opentofu/templates/opnsense-config.xml.tmpl still renders a FULL config. Under D-113(a2) it should shrink to a minimal bootstrap or vanish entirely -- the chain above suggests it may not be needed at all, since the console bootstrap plus this script covers first contact. That is the next call.
  • The opnsense-edge module still wires config_seed + a cdrom disk for the INERT config-ISO path (D-112). Removing them touches an INSTANTIATED resource (libvirt_volume.config_seed is in state), so it is a live change, not a docs change -- deliberately not done here.

Revert

git rm scripts/opnsense-bootstrap-apikey.sh scripts/opnsense-mint-apikey.php
git rm -r tests/opnsense-bootstrap-apikey/

Nothing live depends on these: the live edge's key was minted via the GUI and is unaffected.