Firewalla Router Migration¶
Runbook for replacing the primary Firewalla router with a new unit using the same external /extdata USB disk.
Scope and Assumptions¶
- Source router goes offline before target router is brought online.
- The
/extdataUSB disk is physically moved from source to target. - Router remains at
192.168.20.1and inventory hostroutercontinues to resolve/reach correctly. - Router services are managed by
ansible/router-playbook.yml.
Known-Good Baseline (Captured 2026-02-21)¶
Capture these values before cutover and compare after migration.
| Check | Current Value |
|---|---|
| Capture time (local) | 2026-02-21 09:09:40 EST |
/extdata mount |
/dev/sda1 mounted as ext4 at /extdata |
| USB label | firewalla_extdat |
| Boot hooks present | 0000-a-remount-root.sh, 0000-ensure-dhcpcd6-duid.sh, 0000-mount-extdata.sh, 0001-ipv6-ula.sh, 0002-dhcp6-vzw-fix.sh, 0050-start-docker.sh, 0100-install-docker-compose.sh, 0125-start-alloy.sh, 0150-start-tailscale.sh, 0151-start-router-hosts.sh, 0200-start-vault-unseal.sh |
| Service health | docker-compose@router-hosts, docker-compose@alloy, kopia-backup.timer are active |
| Container health | tailscale and vault-unseal are running |
Vault DNS via router DNS (@192.168.20.1) |
vault*.fzymgc.house -> 192.168.20.145 |
| Backup source paths | /extdata/tailscale-data, /extdata/router-hosts/data, /extdata/router-hosts/hosts |
Latest snapshot: /extdata/tailscale-data |
2026-02-21T13:30:08.756177782Z |
Latest snapshot: /extdata/router-hosts/data |
2026-02-21T13:30:16.865653328Z |
Latest snapshot: /extdata/router-hosts/hosts |
2026-02-21T13:30:24.465340575Z |
| Kopia timer (next / last trigger) | Sun 2026-02-22 03:04:12 EST / Sat 2026-02-21 03:06:38 EST |
Phase 1: Pre-Cutover (Source Router Still Online)¶
- Confirm Vault secrets exist:
vault kv get secret/fzymgc-house/infrastructure/router/dhcpv6
vault kv get secret/fzymgc-house/infrastructure/router/kopia-r2
- Record current IPv6 global addresses:
cd ansible
ansible router -i inventory/hosts.yml -m ansible.builtin.shell -a "ip -6 addr show | grep -E 'inet6.*global' || true" -b
- Run a final manual backup:
cd ansible
ansible router -i inventory/hosts.yml -m ansible.builtin.command -a "/extdata/tools/kopia-backup" -b
- Verify backup source coverage:
cd ansible
ansible router -i inventory/hosts.yml -m ansible.builtin.shell -a \
"export KOPIA_CONFIG_PATH=/extdata/kopia/config/repository.config; /extdata/tools/kopia snapshot list --json | python3 -c \"import sys,json; print('\n'.join(sorted({x.get('source',{}).get('path','') for x in json.load(sys.stdin)})))\"" -b
- Save current runbook-baseline outputs for comparison after cutover.
Phase 2: Physical Cutover¶
- Power off source Firewalla.
- Move WAN/LAN cabling to target Firewalla.
- Move external USB disk to target Firewalla.
- Power on target Firewalla.
- Complete minimum Firewalla app onboarding needed for:
- LAN gateway function on
192.168.20.1 - SSH access as
pi
Phase 3: Bootstrap Access on Target¶
- Remove stale SSH host keys from workstation:
- Confirm SSH and Ansible reachability:
- Verify USB disk mounted as
/extdata:
cd ansible
ansible router -i inventory/hosts.yml -m ansible.builtin.shell -a "lsblk -o NAME,LABEL,FSTYPE,MOUNTPOINT | grep -E 'sda|extdata|firewalla_extdat'" -b
ansible router -i inventory/hosts.yml -m ansible.builtin.shell -a "mount | grep '/extdata'" -b
Phase 4: DNS Bootstrap Guard (Before Full Ansible Run)¶
Because DNS interception can hide resolver behavior on this network, explicitly validate Vault hostnames from the target router.
- Check Vault DNS resolution via local router DNS:
cd ansible
ansible router -i inventory/hosts.yml -m ansible.builtin.shell -a \
"for h in vault.fzymgc.house vault-0.fzymgc.house vault-1.fzymgc.house vault-2.fzymgc.house; do echo -n \"$h \"; dig +short @192.168.20.1 $h | paste -sd, -; done" -b
- If any entry is missing, add temporary bootstrap DNS records and reload
firerouter_dns:
ssh router "cat > /home/pi/.firewalla/config/dnsmasq_local/00-bootstrap-vault.conf <<'EOF'
address=/vault.fzymgc.house/192.168.20.145
address=/vault-0.fzymgc.house/192.168.20.145
address=/vault-1.fzymgc.house/192.168.20.145
address=/vault-2.fzymgc.house/192.168.20.145
EOF
sudo killall -HUP firerouter_dns"
Phase 5: Converge Configuration with Ansible¶
- Export
VAULT_TOKENin the same shell asansible-playbook:
- Run full router convergence:
- Confirm play recap has
failed=0.
Phase 6: Post-Cutover Validation¶
- Validate key services:
cd ansible
ansible router -i inventory/hosts.yml -m ansible.builtin.command -a "systemctl is-active docker-compose@router-hosts docker-compose@alloy kopia-backup.timer" -b
ansible router -i inventory/hosts.yml -m ansible.builtin.command -a "docker inspect tailscale --format {{\"{{\"}}.State.Status{{\"}}\"}}" -b
ansible router -i inventory/hosts.yml -m ansible.builtin.command -a "docker inspect vault-unseal --format {{\"{{\"}}.State.Status{{\"}}\"}}" -b
- Validate DNS integration:
cd ansible
ansible router -i inventory/hosts.yml -m ansible.builtin.command -a "cat /home/pi/.firewalla/config/dnsmasq_local/local-hosts.conf" -b
dig @192.168.20.1 vault.fzymgc.house +short
- Validate backup operation on target:
cd ansible
ansible router -i inventory/hosts.yml -m ansible.builtin.command -a "/extdata/tools/kopia-backup" -b
ansible router -i inventory/hosts.yml -m ansible.builtin.shell -a \
"export KOPIA_CONFIG_PATH=/extdata/kopia/config/repository.config; /extdata/tools/kopia snapshot list --json | python3 -c \"import sys,json; print('\n'.join(sorted({x.get('source',{}).get('path','') for x in json.load(sys.stdin)})))\"" -b
- Compare IPv6 global addresses/prefix behavior to pre-cutover capture.
Rollback¶
If routing, DNS, or service health cannot be recovered quickly:
- Power off target Firewalla.
- Reconnect source Firewalla with original WAN/LAN and USB disk.
- Power on source Firewalla and verify internet/DNS.
- Investigate and retry cutover in a new maintenance window.
References¶
ansible/router-playbook.ymlansible/roles/router-kopia-backup/defaults/main.ymldocs/operations/router-hosts.mddocs/reference/secrets.md