Skip to content

Firewalla Router Migration

Runbook for replacing the primary Firewalla router with a new unit using the same external /extdata USB disk.

  • Source router goes offline before target router is brought online.
  • The /extdata USB disk is physically moved from source to target.
  • Router remains at 192.168.20.1 and inventory host router continues to resolve/reach correctly.
  • Router services are managed by ansible/router-playbook.yml.

Capture these values before cutover and compare after migration.

CheckCurrent Value
Capture time (local)2026-02-21 09:09:40 EST
/extdata mount/dev/sda1 mounted as ext4 at /extdata
USB labelfirewalla_extdat
Boot hooks present0000-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 healthdocker-compose@router-hosts, docker-compose@alloy, kopia-backup.timer are active
Container healthtailscale 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-data2026-02-21T13:30:08.756177782Z
Latest snapshot: /extdata/router-hosts/data2026-02-21T13:30:16.865653328Z
Latest snapshot: /extdata/router-hosts/hosts2026-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)”
  1. Confirm Vault secrets exist:

    Terminal window
    vault kv get secret/fzymgc-house/infrastructure/router/dhcpv6
    vault kv get secret/fzymgc-house/infrastructure/router/kopia-r2
  2. Record current IPv6 global addresses:

    Terminal window
    cd ansible
    ansible router -i inventory/hosts.yml -m ansible.builtin.shell -a "ip -6 addr show | grep -E 'inet6.*global' || true" -b
  3. Run a final manual backup:

    Terminal window
    cd ansible
    ansible router -i inventory/hosts.yml -m ansible.builtin.command -a "/extdata/tools/kopia-backup" -b
  4. Verify backup source coverage:

    Terminal window
    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
  5. Save current runbook-baseline outputs for comparison after cutover.

  1. Power off source Firewalla.
  2. Move WAN/LAN cabling to target Firewalla.
  3. Move external USB disk to target Firewalla.
  4. Power on target Firewalla.
  5. Complete minimum Firewalla app onboarding needed for:
    1. LAN gateway function on 192.168.20.1
    2. SSH access as pi
  1. Remove stale SSH host keys from workstation:

    Terminal window
    ssh-keygen -R router
    ssh-keygen -R 192.168.20.1
  2. Confirm SSH and Ansible reachability:

    Terminal window
    ssh router "hostname"
    cd ansible
    ansible router -i inventory/hosts.yml -m ansible.builtin.ping
  3. Verify USB disk mounted as /extdata:

    Terminal window
    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)

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.

  1. Check Vault DNS resolution via local router DNS:

    Terminal window
    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
  2. 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.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

Section titled “Phase 5: Converge Configuration with Ansible”
  1. Export VAULT_TOKEN in the same shell as ansible-playbook:

    Terminal window
    export VAULT_TOKEN="$(vault token lookup -format=json | jq -r '.data.id')"
  2. Run full router convergence:

    Terminal window
    cd ansible
    ansible-playbook -i inventory/hosts.yml router-playbook.yml --limit router
  3. Confirm play recap has failed=0.

  1. Validate key services:

    Terminal window
    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
  2. Validate DNS integration:

    Terminal window
    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
  3. Validate backup operation on target:

    Terminal window
    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
  4. Compare IPv6 global addresses/prefix behavior to pre-cutover capture.

If routing, DNS, or service health cannot be recovered quickly:

  1. Power off target Firewalla.
  2. Reconnect source Firewalla with original WAN/LAN and USB disk.
  3. Power on source Firewalla and verify internet/DNS.
  4. Investigate and retry cutover in a new maintenance window.
  • ansible/router-playbook.yml
  • ansible/roles/router-kopia-backup/defaults/main.yml
  • docs/operations/router-hosts.md
  • docs/reference/secrets.md