Firewalla Router Migration
Runbook for replacing the primary Firewalla router with a new unit using the same external /extdata USB disk.
Scope and Assumptions
Section titled “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)
Section titled “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)
Section titled “Phase 1: Pre-Cutover (Source Router Still Online)”-
Confirm Vault secrets exist:
Terminal window vault kv get secret/fzymgc-house/infrastructure/router/dhcpv6vault kv get secret/fzymgc-house/infrastructure/router/kopia-r2 -
Record current IPv6 global addresses:
Terminal window cd ansibleansible 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:
Terminal window cd ansibleansible router -i inventory/hosts.yml -m ansible.builtin.command -a "/extdata/tools/kopia-backup" -b -
Verify backup source coverage:
Terminal window cd ansibleansible 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
Section titled “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
- LAN gateway function on
Phase 3: Bootstrap Access on Target
Section titled “Phase 3: Bootstrap Access on Target”-
Remove stale SSH host keys from workstation:
Terminal window ssh-keygen -R routerssh-keygen -R 192.168.20.1 -
Confirm SSH and Ansible reachability:
Terminal window ssh router "hostname"cd ansibleansible router -i inventory/hosts.yml -m ansible.builtin.ping -
Verify USB disk mounted as
/extdata:Terminal window cd ansibleansible router -i inventory/hosts.yml -m ansible.builtin.shell -a "lsblk -o NAME,LABEL,FSTYPE,MOUNTPOINT | grep -E 'sda|extdata|firewalla_extdat'" -bansible router -i inventory/hosts.yml -m ansible.builtin.shell -a "mount | grep '/extdata'" -b
Phase 4: DNS Bootstrap Guard (Before Full Ansible Run)
Section titled “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:
Terminal window cd ansibleansible 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:Terminal window ssh router "cat > /home/pi/.firewalla/config/dnsmasq_local/00-bootstrap-vault.conf <<'EOF'address=/vault.fzymgc.house/192.168.20.145address=/vault-0.fzymgc.house/192.168.20.145address=/vault-1.fzymgc.house/192.168.20.145address=/vault-2.fzymgc.house/192.168.20.145EOFsudo killall -HUP firerouter_dns"
Phase 5: Converge Configuration with Ansible
Section titled “Phase 5: Converge Configuration with Ansible”-
Export
VAULT_TOKENin the same shell asansible-playbook:Terminal window export VAULT_TOKEN="$(vault token lookup -format=json | jq -r '.data.id')" -
Run full router convergence:
Terminal window cd ansibleansible-playbook -i inventory/hosts.yml router-playbook.yml --limit router -
Confirm play recap has
failed=0.
Phase 6: Post-Cutover Validation
Section titled “Phase 6: Post-Cutover Validation”-
Validate key services:
Terminal window cd ansibleansible router -i inventory/hosts.yml -m ansible.builtin.command -a "systemctl is-active docker-compose@router-hosts docker-compose@alloy kopia-backup.timer" -bansible router -i inventory/hosts.yml -m ansible.builtin.command -a "docker inspect tailscale --format {{\"{{\"}}.State.Status{{\"}}\"}}" -bansible router -i inventory/hosts.yml -m ansible.builtin.command -a "docker inspect vault-unseal --format {{\"{{\"}}.State.Status{{\"}}\"}}" -b -
Validate DNS integration:
Terminal window cd ansibleansible router -i inventory/hosts.yml -m ansible.builtin.command -a "cat /home/pi/.firewalla/config/dnsmasq_local/local-hosts.conf" -bdig @192.168.20.1 vault.fzymgc.house +short -
Validate backup operation on target:
Terminal window cd ansibleansible router -i inventory/hosts.yml -m ansible.builtin.command -a "/extdata/tools/kopia-backup" -bansible 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
Section titled “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
Section titled “References”ansible/router-playbook.ymlansible/roles/router-kopia-backup/defaults/main.ymldocs/operations/router-hosts.mddocs/reference/secrets.md