Use when a DC edge forwards but does not NAT, or its GUI answers on the WAN, or dc-egress-check <dc> fails A3/A4 while the edge answers A2 -- i.e. the edge lost its pf ruleset (typically fsck damage to /etc/master.passwd + /etc/group after an unclean power cut; the SEC-031 class). Verdict is REBUILD, not in-place repair: a fresh pristine disk + this bootstrap is faster and more reliable than single-user user-DB surgery, and the edge config is fully recreated by the addressing step below.
Proven twice: dc0 2026-08-02 (docs/audit/dc0-edge-rebuild-20260802.txt, the worked reference) and dc1 2026-08-07 (docs/changelog-20260807-dc1-region-sequence.md Item 4). Both ended dc-egress-check <dc> 8/8. This runbook GENERALISES those; it exists because the dc0 run lived only as an audit capture, so dc1 had to reconstruct it.
Tools (all in-repo -- do NOT reinvent): scripts/opnsense-console-rebuild.py (site-agnostic D-112(c) console driver), scripts/opnsense-bootstrap-apikey.sh, scripts/opnsense-api.sh, scripts/opnsense-set-interface-v4.sh, scripts/dc-egress-check.sh.
| var | dc0 | dc1 | source |
|---|---|---|---|
RACK (transit IP) |
172.31.0.2 | 172.31.0.6 | scripts/lib-hosts.sh |
DOMAIN |
vr1-dc0-opnsense | vr1-dc1-opnsense | inner tofu module |
| inner root | opentofu/vr1-dc0-substrate |
opentofu/vr1-dc1-substrate |
on voffice1 |
WAN addr/pl gw |
172.30.2.2/24 172.30.2.1 | 172.30.3.2/24 172.30.3.1 | G12 / D-125 |
LAN (provider-public gw) |
10.12.4.1/22 | 10.12.64.1/22 | lib-net PLANE_GW |
| rack provider-public leg / bridge | 10.12.4.2 / virbrN | 10.12.64.2 / virbr4 | ip -4 -o addr on rack |
| edge key (jumphost) | ~/vr1-dc0-creds/vr1-dc0-edge_ed25519 |
~/vr1-dc1-creds/vr1-dc1-edge_ed25519 |
SEC-013/-015 |
| base image (voffice1) | ~/vr1-dc0-images/opnsense-26.7-nano.qcow2 |
~/vr1-dc1-images/opnsense-26.7-nano.qcow2 |
present-before-apply |
All virsh on the rack takes -c qemu:///system (rack default is qemu:///session).
virsh -c qemu:///system shutdown $DOMAIN (ACPI works even with a broken login); wait for shut off; then on the rack sudo cp -p <edge-disk>.qcow2 <edge-disk>.qcow2.pre-rebuild-<date>.cp -p terraform.tfstate ~/<dc>-substrate-tfstate-pre-edge-rebuild-<date>.json + sha256-verify identical.cd "$REPO"/opentofu/<dc>-substrate/ # $REPO set per session bootstrap
tofu plan -replace=module.<dc>_opnsense.libvirt_volume.disk \
-replace=module.<dc>_opnsense.libvirt_domain.vm -out=/tmp/edge-rebuild.plan
tofu show -json /tmp/edge-rebuild.plan | jq -r '.resource_changes[] | select(.change.actions!=["no-op"]) | .address+" "+(.change.actions|join(","))'REQUIRE: exactly 2 non-no-op changes, BOTH the edge domain+disk, each delete,create; 0 non-no-op changes naming anything else. (The refresh reports ~20 resources "changed outside OpenTofu" -- that is DRIFT, not an action; assert on ACTIONS only.)tofu apply /tmp/edge-rebuild.plan -> 2 added, 0 changed, 2 destroyed. Fresh disk is base-image size / new mtime (measure the artifact, don't trust the log). Domain boots factory: LAN 192.168.1.1/24, WAN address-less, root/opnsense login.
Runs ON the rack (has pexpect). Stage the driver + the site edge PUBKEY (public):
scp scripts/opnsense-console-rebuild.py <edge-pubkey>.pub -> rack ssh $RACK 'python3 ~/opnsense-console-rebuild.py --domain '$DOMAIN' --pubkey ~/<edge>.pub'
Expect CONSOLE-STATE=0 then BOOTSTRAP-SEQUENCE-COMPLETE. Verify the transcript shows CONFIG-WRITTEN, AK-IN-CONFIG=152, USER-MATERIALIZED, OK (sshd restart).
A REBUILT edge has NEW host keys -- clear the rack's stale entries or every BatchMode script fails with HOST IDENTIFICATION CHANGED:
ssh $RACK 'cp -p ~/.ssh/known_hosts ~/.ssh/known_hosts.pre-rebuild-<date>;
ssh-keygen -R 192.168.1.1; ssh-keygen -R $LAN_IP;
sudo ip addr add 192.168.1.2/24 dev <rack-provider-public-bridge>; # interim reach to factory LAN
ssh-keyscan -T8 192.168.1.1 >> ~/.ssh/known_hosts'
Stage the edge PRIVATE key to the rack transiently (0600, SEC-015 pattern -- SHRED in Step 7) OR reach the edge from the jumphost via ProxyCommand. Prove: ssh -i <edge-key> root@192.168.1.1 opnsense-version -> OPNsense 26.7.
cd ~/repo-stage/scripts # sha256-verify the 5 opnsense-*.sh/php against the repo first OPNSENSE_SSH_KEY=<edge-key> bash opnsense-bootstrap-apikey.sh 192.168.1.1 <api-out>.txt # WAN (also REGENERATES the pf ruleset -> automatic outbound NAT appears here): OPNSENSE_SSH_KEY=<edge-key> bash opnsense-set-interface-v4.sh --commit 192.168.1.1 wan $WAN_ADDR $WAN_PL $WAN_GW # PROVE egress FROM THE EDGE before the LAN move (after it, the reach path is gone): ssh -i <edge-key> root@192.168.1.1 'ping -c3 1.1.1.1; fetch -o /dev/null http://archive.ubuntu.com/ubuntu/dists/jammy/Release' # LAN LAST -- the apply drops the session it rides on. EXPECTED to time out; config saves+applies. OPNSENSE_SSH_KEY=<edge-key> bash opnsense-set-interface-v4.sh --commit 192.168.1.1 lan $LAN_ADDR 22
After the LAN move the edge is at $LAN_ADDR, reachable from the rack's PERMANENT provider-public leg (no interim leg needed). If the script version only SAVES lan, apply it: ssh -i <edge-key> root@$LAN_ADDR configctl interface reconfigure lan. Then configctl filter reload and confirm pfctl -s nat shows nat on vtnet1 ... from (vtnet0:network) to any -> (vtnet1:0). tcsh trap: the edge root shell is tcsh -- NO 2>&1 in edge commands ("Ambiguous output redirect").
ssh $RACK 'sudo -n bash -s -- check <dc>' < scripts/dc-egress-check.sh -> pass=8 fail=0. Confirm the region VM egresses: from the .6, ping 1.1.1.1 and curl images.maas.io 200.
shred -u); remove the interim 192.168.1.2/24 reach leg. Consolidate the API key to the jumphost ~/<dc>-creds/ (0600, sha256-verified, secret never printed).docs/CURRENT-STATE.md (status). A REBUILT edge invalidated the rack known_hosts (Step 4) -- fold any new lesson here.2>&1, no nested-quoted one-liners.opnsense-set-interface-v4.sh uses the LEGACY interfaces page, not REST (D-113 amdt) -- do not "fix" it to the API.