Skip to content

Bridge a Single NIC for VM Networking

By default, a host onboarded via SSH-Push (or any vanilla Rocky/RHEL/Alma box) only has libvirt's built-in default network — a NAT'd, isolated subnet (192.168.122.0/24) behind virbr0. VMs on it work, but they're hidden behind the host: no real LAN IP, no inbound access, invisible to DHCP/DNS on your actual network.

This guide converts the host's single physical NIC into a Linux bridge, so VMs attach directly to your real LAN and get DHCP addresses from it — without adding a second physical NIC.

This touches the interface you're SSHed in on

There's no second physical NIC to fall back to. A botched bridge conversion can leave the host completely unreachable over the network until someone gets physical or out-of-band (iDRAC/BMC) access. Don't attempt this without one of those available as a fallback, and use the dead-man's-switch pattern below — don't just run the commands directly.

Why not just edit the interface directly

Running nmcli/ip commands one-by-one over the same SSH session you're connected through is how hosts get bricked: the moment the IP moves off the old interface, your session can drop mid-sequence, and you're left in a half-converted state with no way back in.

Instead, arm an automatic rollback before touching anything live:

  1. Write the conversion as one script, and a separate revert script that undoes it.
  2. Schedule the revert script to fire automatically after ~90 seconds via a transient systemd timer.
  3. Run the conversion script in the background (detached from your SSH session, so it survives the session dropping).
  4. Reconnect and confirm. If it works, cancel the timer. If you can't reconnect, the timer fires on its own and restores the original config.

Prerequisites

  • Physical or out-of-band (iDRAC/BMC) console access to the host, as a last-resort fallback.
  • The host's current static IP config (nmcli -t -f all con show <interface> — note ipv4.addresses, ipv4.gateway, ipv4.dns).
  • NetworkManager (nmcli) — this is the default on Rocky/RHEL/Alma; the commands below assume it.

1. Stage the scripts — in /usr/local/sbin, not /root

SELinux will silently block this from /root

Files under /root get the admin_home_t SELinux label. systemd-run-launched transient services execute as the init_t domain, which cannot execute admin_home_t files — you'll get Permission denied / status=203/EXEC in journalctl, with no SELinux AVC denial showing up in ausearch -m avc -ts recent (you have to grep /var/log/audit/audit.log directly to see it). /usr/local/sbin carries bin_t, which init_t is allowed to execute — same pattern services/vgpu_setup.py's boot-persistence script already uses for exactly this reason.

bridge_setup.sh:

#!/bin/bash
set -e -x
exec >/root/bridge_setup.log 2>&1

# Replace enp8s0 / 192.168.7.7 / 192.168.7.1 / 192.168.7.80 with your values.
nmcli con add type bridge ifname br0 con-name br0 \
  ipv4.method manual ipv4.addresses 192.168.7.7/24 ipv4.gateway 192.168.7.1 ipv4.dns 192.168.7.80 \
  ipv6.method ignore bridge.stp no connection.autoconnect yes

nmcli con modify enp8s0 connection.master br0 connection.slave-type bridge ipv6.method ignore

nmcli con up enp8s0
sleep 2
nmcli con up br0

bridge_revert.sh:

#!/bin/bash
exec >/root/bridge_revert.log 2>&1

nmcli con down br0 2>/dev/null
nmcli con delete br0 2>/dev/null
nmcli con modify enp8s0 connection.master "" connection.slave-type ""
nmcli con modify enp8s0 \
  ipv4.method manual ipv4.addresses 192.168.7.7/24 ipv4.gateway 192.168.7.1 ipv4.dns 192.168.7.80 \
  ipv6.method ignore
nmcli con up enp8s0
scp bridge_setup.sh bridge_revert.sh root@<host>:/usr/local/sbin/
ssh root@<host> chmod +x /usr/local/sbin/bridge_setup.sh /usr/local/sbin/bridge_revert.sh

Two nmcli gotchas baked into the script above

  • remove-setting ipv4 must be its own nmcli con modify invocation — combined inline with other property assignments, nmcli rejects it with invalid <setting>.<property> 'remove-setting'. In practice you don't even need it: setting connection.master/connection.slave-type alone (no ipv4.* flags in the same command) succeeds fine — NetworkManager just ignores the dormant ipv4.* values on a port/slave connection at activation time.
  • If you do pass ipv4.* properties in the same command as connection.master/connection.slave-type, you'll hit Error: invalid or not allowed setting 'ipv4': port connections cannot have IP configuration instead.

2. Verify the dead-man's switch actually works — before relying on it

Don't skip this. Test the exact mechanism with a harmless script first:

ssh root@<host> '
printf "#!/bin/bash\necho hello > /root/test_exec_output.txt\n" > /usr/local/sbin/test_exec.sh
chmod +x /usr/local/sbin/test_exec.sh
systemd-run --unit=test-exec --on-active=3 /usr/local/sbin/test_exec.sh
'
sleep 5
ssh root@<host> 'cat /root/test_exec_output.txt'   # must print "hello"

If this doesn't print hello, stop and fix it (check journalctl -u test-exec.service and the SELinux note above) before going anywhere near the real bridge conversion. Clean up the test files afterward.

3. Arm the timer and run the conversion

ssh root@<host> '
systemd-run --unit=net-revert --on-active=90 /usr/local/sbin/bridge_revert.sh
systemctl list-timers net-revert.timer --no-pager   # confirm it shows "1 timers listed"
setsid nohup /usr/local/sbin/bridge_setup.sh >/root/bridge_setup_outer.log 2>&1 </dev/null &
'

Wait ~15 seconds, then reconnect:

ssh root@<host> 'ip -br addr; cat /root/bridge_setup.log'

Expect br0 carrying the host IP, enp8s0 with no IP of its own (just UP), and the setup log showing both nmcli con up calls succeeded.

4. Cancel the timer once confirmed working

ssh root@<host> 'systemctl stop net-revert.timer net-revert.service'

If you skip this, the timer fires at 90s regardless and reverts everything — harmless if the bridge is already working (the revert script is idempotent), but you'll lose the bridge and have to redo it.

If the bridge didn't come up and you can't reconnect, just wait — the timer restores the original config on its own within 90 seconds of being armed.

5. Re-attach existing VM NICs to the new bridge

VMs already running on the old default NAT network need their interface moved. Keep the same MAC so the guest treats it as a link blip, not new hardware:

virsh detach-interface <vm> network --mac <mac> --live --config
virsh attach-interface <vm> bridge br0 --mac <mac> --model virtio --live --config

Check the result:

virsh domifaddr <vm> --source agent

With qemu-guest-agent running in the guest, this should show a freshly DHCP'd address on your real LAN within seconds — no reboot needed.

Troubleshooting

Symptom Cause & fix
journalctl -u net-revert.service shows Unable to locate executable ... Permission denied, status=203/EXEC SELinux blocking init_t from executing an admin_home_t-labeled script in /root. Move scripts to /usr/local/sbin (Step 1). ausearch -m avc -ts recent may show nothing — grep /var/log/audit/audit.log for denied directly to confirm.
nmcli: invalid <setting>.<property> 'remove-setting' remove-setting was combined with other properties in one command. Either give it its own command, or skip it entirely — setting connection.master/connection.slave-type alone is enough.
nmcli: port connections cannot have IP configuration You passed ipv4.* properties in the same command as connection.master/connection.slave-type. Drop them — see the tip in Step 1.
systemd-run --unit=X fails with Unit X.service was already loaded or has a fragment file A previous attempt's transient unit is still lingering. systemctl reset-failed X.service first, or use a new unique unit name.
VM doesn't get a new IP after re-attach Guest may not have noticed the link change. Inside the guest: disable/re-enable the NIC, or ipconfig /renew (Windows) / dhclient (Linux).