This runbook migrates a remotely administered Debian host from ifupdown with dhcpcd and wpa_supplicant to NetworkManager. It then configures this policy:
- Use Ethernet on
enp1s0whenever it has carrier and a global IP address. - Disconnect Wi-Fi on
wlo1while Ethernet is usable. - Connect the saved Wi-Fi profile when Ethernet loses carrier or its address.
The migration is staged so the existing SSH connection remains available until a timed, rollback-protected handoff.
| Purpose | Name |
|---|---|
| Ethernet interface | enp1s0 |
| Wi-Fi interface | wlo1 |
| NetworkManager Ethernet profile | wired-enp1s0 |
| NetworkManager Wi-Fi profile | wifi-fallback |
| Hostname | bee |
Change these names throughout if the target host differs.
Run these commands before changing anything:
printf '%s\n' '=== NetworkManager package ==='
dpkg-query -W -f='${Status} ${Version}\n' network-manager 2>/dev/null || true
command -v nmcli
command -v nmtui
printf '%s\n' '=== Network services ==='
systemctl is-active NetworkManager.service 2>/dev/null || true
systemctl list-units --type=service --all --no-pager |
grep -E 'NetworkManager|dhcpcd|wpa_supplicant|networking|systemd-networkd'
printf '%s\n' '=== Addresses and routes ==='
ip -br link
ip -br -4 address
ip route
printf '%s\n' '=== Current SSH path ==='
client_ip="${SSH_CONNECTION%% *}"
ip route get "$client_ip"
printf '%s\n' '=== Ethernet state ==='
cat /sys/class/net/enp1s0/carrier 2>/dev/null || true
ip -4 address show dev enp1s0
printf '%s\n' '=== ifupdown configuration ==='
sudo sed -n '1,200p' /etc/network/interfaces
sudo find /etc/network/interfaces.d -maxdepth 1 -type f \
-exec sh -c 'echo "### $1"; sed -n "1,200p" "$1"' sh {} \; 2>/dev/nullTo identify which systemd unit owns a known networking process, pass its PID to systemctl status. For example:
sudo systemctl status 831 813 --no-pagerOn the original host, this established that:
- NetworkManager was absent.
networking.serviceandifup@wlo1.servicemanaged networking.ifup@wlo1.servicelaunched a per-interfacewpa_supplicantanddhcpcd.- SSH was using
wlo1, so stopping the old stack interactively would terminate the only remote connection. /etc/network/interfacescontained the Wi-Fi SSID and PSK.
Do not publish the contents of /etc/network/interfaces while it contains a plaintext Wi-Fi password.
sudo cp -a /etc/network/interfaces \
/etc/network/interfaces.before-networkmanagerKeep this backup until Wi-Fi fallback and Ethernet preference have both been tested.
Create a temporary safety configuration before installing NetworkManager:
sudo mkdir -p /etc/NetworkManager/conf.d
sudoedit /etc/NetworkManager/conf.d/00-migration-unmanaged.confContents:
[keyfile]
unmanaged-devices=interface-name:wlo1;interface-name:enp1s0This allows NetworkManager to start without taking either interface from the working ifupdown session.
sudo apt update
sudo apt install network-managerVerify the daemon and confirm that both interfaces remain unmanaged:
systemctl is-active NetworkManager
nmcli general status
nmcli device status
ip -br -4 addressExpected state during staging:
enp1s0 ethernet unmanaged
wlo1 wifi unmanaged
The original Wi-Fi address and SSH session should still be present.
sudo nmcli connection add \
type ethernet \
ifname enp1s0 \
con-name wired-enp1s0 \
connection.autoconnect yes \
connection.autoconnect-priority 100 \
ipv4.method auto \
ipv4.route-metric 100 \
ipv6.method auto \
ipv6.route-metric 100The route metric makes Ethernet the preferred default route whenever it is active.
The following commands read the existing SSID and PSK without embedding the password in shell history:
wifi_ssid="$(
sudo sed -n \
's/^[[:space:]]*wpa-ssid[[:space:]]\+//p' \
/etc/network/interfaces |
head -n1
)"
wifi_psk="$(
sudo sed -n \
's/^[[:space:]]*wpa-psk[[:space:]]\+//p' \
/etc/network/interfaces |
head -n1
)"Create the connection:
sudo nmcli connection add \
type wifi \
ifname wlo1 \
con-name wifi-fallback \
ssid "$wifi_ssid"
sudo nmcli connection modify wifi-fallback \
wifi-sec.key-mgmt wpa-psk \
wifi-sec.psk "$wifi_psk" \
connection.autoconnect yes \
connection.autoconnect-priority 10 \
ipv4.method auto \
ipv4.route-metric 600 \
ipv6.method auto \
ipv6.route-metric 600
unset wifi_pskCheck the stored SSID and password length without printing the secret:
nmcli -g 802-11-wireless.ssid connection show wifi-fallback
sudo nmcli --show-secrets \
-g 802-11-wireless-security.psk \
connection show wifi-fallback |
awk '{ print "PSK length:", length }'A WPA passphrase should contain 8 through 63 characters.
Inspect both staged profiles:
nmcli -f NAME,UUID,TYPE,DEVICE,AUTOCONNECT,AUTOCONNECT-PRIORITY \
connection showNeither staged profile will have a device assigned while the temporary unmanaged rule is active.
Create the replacement file without altering the live file yet:
sudoedit /etc/network/interfaces.networkmanagerContents:
source /etc/network/interfaces.d/*
auto lo
iface lo inet loopbackThe physical-interface configuration is removed because NetworkManager will own both interfaces.
sudoedit /usr/local/sbin/networkmanager-migration-switchContents:
#!/bin/sh
logger -t nm-migration "Starting NetworkManager handoff"
ifdown --force wlo1 || true
install -o root -g root -m 0644 \
/etc/network/interfaces.networkmanager \
/etc/network/interfaces
if [ -f /etc/NetworkManager/conf.d/00-migration-unmanaged.conf ]; then
mv /etc/NetworkManager/conf.d/00-migration-unmanaged.conf \
/etc/NetworkManager/conf.d/00-migration-unmanaged.conf.disabled
fi
systemctl restart NetworkManager.service
nmcli radio wifi on
nmcli --wait 60 connection up wifi-fallbacksudoedit /usr/local/sbin/networkmanager-migration-rollbackContents:
#!/bin/sh
logger -t nm-migration "Rolling back to ifupdown"
nmcli device disconnect wlo1 2>/dev/null || true
systemctl stop NetworkManager.service || true
if [ -f /etc/NetworkManager/conf.d/00-migration-unmanaged.conf.disabled ]; then
mv /etc/NetworkManager/conf.d/00-migration-unmanaged.conf.disabled \
/etc/NetworkManager/conf.d/00-migration-unmanaged.conf
fi
cp -af /etc/network/interfaces.before-networkmanager \
/etc/network/interfaces
ifdown --force wlo1 2>/dev/null || true
ifup --force wlo1Secure both scripts:
sudo chown root:root \
/usr/local/sbin/networkmanager-migration-switch \
/usr/local/sbin/networkmanager-migration-rollback
sudo chmod 0700 \
/usr/local/sbin/networkmanager-migration-switch \
/usr/local/sbin/networkmanager-migration-rollbackArm a five-minute rollback:
sudo systemd-run \
--unit=nm-migration-rollback \
--on-active=5m \
/usr/local/sbin/networkmanager-migration-rollbackConfirm that it is waiting:
systemctl status nm-migration-rollback.timer --no-pagerSchedule the handoff in five seconds:
sudo systemd-run \
--unit=nm-migration-switch \
--on-active=5s \
/usr/local/sbin/networkmanager-migration-switchThe SSH connection will close while wlo1 changes managers. Wait about 15 seconds and reconnect. The DHCP address may change. Check the router for the host named bee if the former address does not respond.
After reconnecting, cancel the rollback immediately:
sudo systemctl stop nm-migration-rollback.timerValidate the migration:
nmcli device status
nmcli connection show --active
ip -br -4 address
ip routeAt this stage, wlo1 should be connected using wifi-fallback, and enp1s0 should be unavailable when its cable is unplugged.
Create the dispatcher script:
sudoedit /etc/NetworkManager/dispatcher.d/70-wifi-fallbackContents:
#!/bin/sh
WIRED="enp1s0"
WIFI="wlo1"
WIFI_CONNECTION="wifi-fallback"
wired_has_ip() {
[ "$(cat "/sys/class/net/$WIRED/carrier" 2>/dev/null)" = "1" ] &&
ip -o address show dev "$WIRED" scope global |
grep -qv ' tentative '
}
wifi_active() {
LC_ALL=C nmcli -t -f DEVICE,STATE device status |
grep -Eq "^${WIFI}:(connected|connecting)"
}
if wired_has_ip; then
if wifi_active; then
logger -t wifi-fallback \
"$WIRED has an address; disconnecting $WIFI"
nmcli --wait 15 device disconnect "$WIFI"
fi
else
if ! wifi_active; then
logger -t wifi-fallback \
"$WIRED unavailable; connecting $WIFI"
nmcli --wait 30 connection up "$WIFI_CONNECTION" ifname "$WIFI"
fi
fi
exit 0The wifi_active expression deliberately accepts states beginning with connecting, including values such as connecting (prepare).
Set the permissions required for a NetworkManager dispatcher:
sudo chown root:root /etc/NetworkManager/dispatcher.d/70-wifi-fallback
sudo chmod 0755 /etc/NetworkManager/dispatcher.d/70-wifi-fallback
stat -c '%U:%G %a %n' \
/etc/NetworkManager/dispatcher.d/70-wifi-fallbackExpected result:
root:root 755 /etc/NetworkManager/dispatcher.d/70-wifi-fallback
No NetworkManager restart is required. Dispatcher scripts are read when network events occur.
Keep Ethernet unplugged and invoke the policy manually:
sudo /etc/NetworkManager/dispatcher.d/70-wifi-fallback enp1s0 down
nmcli device statuswlo1 should remain connected through wifi-fallback.
Before connecting Ethernet, create a five-minute test rollback. It disables the dispatcher and brings Wi-Fi back if remote access is lost:
sudo systemd-run \
--unit=wifi-policy-test-rollback \
--on-active=5m \
/bin/sh -c 'chmod 0644 /etc/NetworkManager/dispatcher.d/70-wifi-fallback; nmcli connection up wifi-fallback'Connect the Ethernet cable. Once enp1s0 receives an address, the dispatcher should disconnect wlo1, ending the Wi-Fi SSH session.
Find the Ethernet address in the router and reconnect. Then cancel the timer:
sudo systemctl stop wifi-policy-test-rollback.timerIf the safety timer fired, restore the dispatcher permission after resolving the problem:
sudo chmod 0755 /etc/NetworkManager/dispatcher.d/70-wifi-fallbacknmcli device status
nmcli connection show --active
ip -br -4 address
ip routeExpected state:
enp1s0is connected throughwired-enp1s0.wlo1is disconnected.- The default route uses
enp1s0with metric100. - Wi-Fi radio remains enabled so fallback can reconnect it.
Confirm that the current SSH session is using Ethernet:
client_ip="${SSH_CONNECTION%% *}"
ip route get "$client_ip"The result should include dev enp1s0.
Check local and internet connectivity:
ping -c 3 172.16.0.1
ping -c 3 deb.debian.org
nmcli radio wifiInspect policy events:
sudo journalctl -b -t wifi-fallback --no-pagerA successful Ethernet transition produces a message resembling:
enp1s0 has an address; disconnecting wlo1
Unplug Ethernet. The wired SSH session will end. Within several seconds, NetworkManager should activate wifi-fallback. Reconnect using the Wi-Fi address shown by the router.
Validate the state:
nmcli device status
ip -br -4 address
ip route
sudo journalctl -b -t wifi-fallback --since "5 minutes ago"The journal should contain:
enp1s0 unavailable; connecting wlo1
Reconnect Ethernet and confirm the reverse transition. The resulting sequence should resemble:
enp1s0 unavailable; connecting wlo1
enp1s0 has an address; disconnecting wlo1
Cancel any remaining test timer and confirm the dispatcher is executable:
sudo systemctl stop wifi-policy-test-rollback.timer 2>/dev/null || true
stat -c '%U:%G %a %n' \
/etc/NetworkManager/dispatcher.d/70-wifi-fallbackUseful ongoing checks:
nmcli device status
nmcli connection show --active
ip -br -4 address
ip route
sudo journalctl -b -t wifi-fallback --no-pagerThe disabled safety configuration, migration scripts, and staged interfaces file are inert. Keep them until the machine has rebooted successfully and both network paths have been tested again.
Afterward, they can be removed:
sudo rm -f \
/etc/NetworkManager/conf.d/00-migration-unmanaged.conf.disabled \
/etc/network/interfaces.networkmanager \
/usr/local/sbin/networkmanager-migration-switch \
/usr/local/sbin/networkmanager-migration-rollbackRetaining /etc/network/interfaces.before-networkmanager provides a compact record of the former setup. Protect it because it may contain the old Wi-Fi PSK:
sudo chown root:root /etc/network/interfaces.before-networkmanager
sudo chmod 0600 /etc/network/interfaces.before-networkmanagerCreate DHCP reservations in the router for both interface MAC addresses. Each interface needs its own address because Ethernet and Wi-Fi have separate MAC addresses.
On the tested host, the final addresses were:
| Interface | Role | Address during testing |
|---|---|---|
enp1s0 |
Primary Ethernet | 172.16.0.32 |
wlo1 |
Wi-Fi fallback | 172.16.0.166 |
Reservations make it straightforward to reconnect after either interface takes over.
Check for both causes used during migration:
cat /etc/NetworkManager/conf.d/00-migration-unmanaged.conf 2>/dev/null
sudo sed -n '1,200p' /etc/network/interfacesPhysical interfaces should no longer appear in /etc/network/interfaces, and the temporary unmanaged file should have been renamed with a .disabled suffix.
Then restart NetworkManager:
sudo systemctl restart NetworkManager
nmcli device statusnmcli -g 802-11-wireless.ssid connection show wifi-fallback
sudo nmcli --show-secrets \
-g 802-11-wireless-security.psk \
connection show wifi-fallback |
awk '{ print "PSK length:", length }'
sudo journalctl -b -u NetworkManager --no-pagerThe SSID must match exactly. The passphrase length must be valid.
This normally means it lacks carrier:
cat /sys/class/net/enp1s0/carrier 2>/dev/null
ip link show enp1s0With a working cable and switch port, carrier should be 1.
ip -o address show dev enp1s0 scope global
stat -c '%U:%G %a %n' \
/etc/NetworkManager/dispatcher.d/70-wifi-fallback
sudo /etc/NetworkManager/dispatcher.d/70-wifi-fallback enp1s0 up
sudo journalctl -b -t wifi-fallback --no-pagerThe dispatcher must be owned by root and executable. Ethernet must have carrier plus a global IPv4 or IPv6 address.
If NetworkManager cannot provide connectivity and the automatic rollback is unavailable:
sudo systemctl stop NetworkManager.service
sudo mv \
/etc/NetworkManager/conf.d/00-migration-unmanaged.conf.disabled \
/etc/NetworkManager/conf.d/00-migration-unmanaged.conf 2>/dev/null || true
sudo cp -af \
/etc/network/interfaces.before-networkmanager \
/etc/network/interfaces
sudo ifdown --force wlo1 2>/dev/null || true
sudo ifup --force wlo1The backup may contain the Wi-Fi credential, so keep its permissions at 0600.