Choosing Your Configuration Backend
RHEL 9 and its derivatives ship NetworkManager as the only supported persistent configuration layer. Ubuntu 24.04 server images default to systemd-networkd with Netplan as the abstraction layer on top. Ubuntu 24.04 desktop uses NetworkManager. Knowing which backend owns your interface matters because writing files for the wrong backend produces silent failures: the file sits on disk, nothing reads it, and your config disappears on the next reboot.
Run this to identify the active backend on any systemd-based host:
On a RHEL 9 host you will see NetworkManager active and enabled. On a minimal Ubuntu 24.04 server install you will see systemd-networkd. If both are running simultaneously on an Ubuntu host, NetworkManager usually wins for interfaces it has claimed. Check which interfaces each backend manages with `nmcli device status` and `networkctl list` side by side.
For new deployments we recommend picking one backend and disabling the other. Mixing them is the single most common source of network config drift we see on long-lived servers.
systemctl status NetworkManager systemd-networkd 2>/dev/null | grep -E 'Active:|●'
Static IP with nmcli on RHEL 9 and Rocky Linux 9
nmcli writes connection profiles to /etc/NetworkManager/system-connections/ as .nmconnection files. Every change through nmcli is immediately persisted. Forget `ifcfg` files - on RHEL 9 the keyfile format is the default, and ifcfg support requires the NetworkManager-dispatcher-routing-rules package.
The command below sets a static IPv4 address, gateway, and two DNS servers on interface ens3, then activates the connection immediately. Substitute your actual interface name from `ip link show`.
To verify the config was written and is active:
`nmcli connection show ens3-static | grep -E 'ipv4|GENERAL.STATE'`
To modify a single field without recreating the connection, use `nmcli connection modify`. For example, adding a second IPv4 address to an existing connection:
`nmcli connection modify ens3-static +ipv4.addresses 10.0.0.20/24`
Then `nmcli connection up ens3-static` to apply. No reboot required. Changes survive reboots because the .nmconnection file is updated synchronously by nmcli before returning.
nmcli connection add \
type ethernet \
con-name ens3-static \
ifname ens3 \
ipv4.method manual \
ipv4.addresses 192.168.10.50/24 \
ipv4.gateway 192.168.10.1 \
ipv4.dns "1.1.1.1 8.8.8.8" \
ipv6.method disabled \
connection.autoconnect yes
nmcli connection up ens3-static
Static IP with systemd-networkd on Ubuntu 24.04
Ubuntu 24.04 server uses Netplan as the configuration abstraction layer, with systemd-networkd as the default renderer. You write YAML under /etc/netplan/ and run `netplan apply` to generate and load the underlying .network files. The generated systemd-networkd unit files land in /run/systemd/network/ and are not intended to be edited directly.
Netplan files must have a .yaml extension and be readable only by root (600 or 640). Files are processed in lexicographic order; 01-netcfg.yaml runs before 99-custom.yaml.
After editing, validate before applying:
`netplan generate && echo OK`
Then apply without disconnecting your SSH session (Netplan tries a revert if connectivity is lost):
`netplan try --timeout 30`
If the terminal stays connected after 30 seconds, accept the change:
`netplan apply`
To inspect the resulting systemd-networkd configuration that Netplan generated:
`cat /run/systemd/network/10-netplan-ens3.network`
This shows exactly what systemd-networkd received, which is useful for debugging address assignment failures.
# /etc/netplan/01-static.yaml
network:
version: 2
renderer: networkd
ethernets:
ens3:
dhcp4: false
addresses:
- 192.168.10.50/24
routes:
- to: default
via: 192.168.10.1
nameservers:
addresses: [1.1.1.1, 8.8.8.8]
search: [corp.example.com]
NIC Bonding for High Availability
Bonding two physical NICs in active-backup mode (mode=1) is the minimum viable HA config for a production server with two uplinks to separate switches. Active-LACP (mode=4, 802.3ad) gives you bandwidth aggregation but requires switch-side configuration. We tested both on a Dell PowerEdge R750 running Rocky Linux 9.4 with two 25GbE ports.
With nmcli on RHEL/Rocky, create the bond master first, then add slaves:
Check the bond status after both slaves are up:
`cat /proc/net/bonding/bond0`
The output shows active slave, link status, and MII polling interval. For mode=1, look for `Currently Active Slave: ens3` and both slaves showing `MII Status: up`.
On Ubuntu 24.04 with Netplan, bonding config in YAML:
```yaml network: version: 2 bonds: bond0: interfaces: [ens3, ens4] parameters: mode: active-backup mii-monitor-interval: 100 addresses: [192.168.10.50/24] routes: - to: default via: 192.168.10.1 ```
MII monitor interval of 100ms is a reasonable default. Lower values increase failover speed but add CPU overhead on systems with many bonds. We found 100ms gives sub-200ms failover on our test servers, which is acceptable for most non-realtime workloads.
# Create bond master
nmcli connection add type bond \
con-name bond0 ifname bond0 \
bond.options "mode=active-backup,mii-monitor=100"
# Assign static IP to bond
nmcli connection modify bond0 \
ipv4.method manual \
ipv4.addresses 192.168.10.50/24 \
ipv4.gateway 192.168.10.1
# Add slave interfaces
nmcli connection add type ethernet \
con-name bond0-slave1 ifname ens3 \
master bond0
nmcli connection add type ethernet \
con-name bond0-slave2 ifname ens4 \
master bond0
nmcli connection up bond0
VLAN Configuration
802.1Q VLAN tagging lets you run multiple logical networks over a single physical interface or bond. This is standard in environments where servers connect to a trunk port on a managed switch. The kernel's 8021q module handles the tagging; both NetworkManager and systemd-networkd configure it through normal interface abstractions.
With nmcli, create a VLAN interface on top of ens3 for VLAN ID 100:
With Netplan on Ubuntu:
```yaml network: version: 2 ethernets: ens3: dhcp4: false vlans: vlan100: id: 100 link: ens3 addresses: [10.100.0.50/24] routes: - to: default via: 10.100.0.1 ```
Verify the VLAN interface is up and carrying the right tag:
`ip -d link show vlan100`
The `-d` flag shows the link details including the VLAN ID and protocol (802.1Q). If you see `vlan id 100` in the output, the kernel has the tag correct. Packet captures on the physical interface with `tcpdump -i ens3 -e vlan` will show tagged frames for confirmation.
nmcli connection add type vlan \
con-name vlan100 \
dev ens3 \
id 100 \
ipv4.method manual \
ipv4.addresses 10.100.0.50/24 \
ipv4.gateway 10.100.0.1
nmcli connection up vlan100
Static Routes and Policy Routing
Adding a non-default static route is one of the most common post-install tasks in multi-homed environments. With iproute2, routes added via `ip route add` are not persistent. Persistence requires writing them through your backend.
With nmcli, add a host route and a network route to an existing connection:
`nmcli connection modify ens3-static +ipv4.routes "10.20.0.0/16 192.168.10.254"`
Apply without full reconnect:
`nmcli device reapply ens3`
For policy routing (source-based routing), you need routing tables and rules. This is common when a server has two uplinks with different ISPs and must ensure return traffic uses the same interface it arrived on. Create a custom routing table by adding an entry to /etc/iproute2/rt_tables:
`echo '200 isp2' >> /etc/iproute2/rt_tables`
Then add the default route for that table and the rule:
```bash ip route add default via 203.0.113.1 dev ens4 table isp2 ip rule add from 203.0.113.50 table isp2 ```
To make this persistent with nmcli, use the routing-rule property:
`nmcli connection modify ens4-static ipv4.routing-rules "priority 100 from 203.0.113.50 table 200"`
Verify rules are in effect: `ip rule list` should show your custom rule between the default rules at priority 0 and 32766.
# View current routing table
ip route show table main
# Flush and re-examine after nmcli apply
ip route show table all | grep -v 'local\|broadcast\|multicast'
DNS Resolver Configuration
DNS configuration in modern Linux is split between the stub resolver (systemd-resolved), the per-interface nameserver assignments (NetworkManager or networkd), and /etc/resolv.conf which may be a symlink, a static file, or managed by resolvconf.
On Ubuntu 24.04 server, /etc/resolv.conf is a symlink to /run/systemd/resolve/stub-resolv.conf by default. This points at 127.0.0.53, the systemd-resolved stub listener. DNS queries go: application -> 127.0.0.53 -> systemd-resolved -> upstream DNS.
Check current resolver status:
`resolvectl status`
This shows per-interface DNS servers, search domains, DNSSEC status, and DNS-over-TLS configuration. If Netplan assigned nameservers correctly, you will see them listed under the interface section.
To set global fallback DNS in systemd-resolved (used when no interface-specific DNS is configured), edit /etc/systemd/resolved.conf:
```ini [Resolve] DNS=1.1.1.1 8.8.8.8 FallbackDNS=9.9.9.9 Domains=corp.example.com DNSSEC=allow-downgrade DNSOverTLS=opportunistic ```
Then `systemctl restart systemd-resolved`.
On RHEL 9 with NetworkManager, resolved is not used by default. NetworkManager writes /etc/resolv.conf directly. To prevent conflicts with any local resolver you run:
`nmcli general hostname-mode none`
And set dns=none in /etc/NetworkManager/NetworkManager.conf to stop NetworkManager from touching /etc/resolv.conf entirely if you manage it manually.
# Check what manages resolv.conf
ls -la /etc/resolv.conf
# See per-interface DNS assignments
resolvectl dns
# Test resolution through resolved stub
resolvectl query myunix.org
Debugging Network Configuration Problems
When a network config does not behave as expected, the fastest path to root cause is examining what the kernel actually has loaded versus what your config management layer thinks it configured.
Start with the kernel view:
```bash ip addr show ip route show table all ip rule list ss -tulnp ```
Then compare against your backend's view. For NetworkManager:
`nmcli connection show --active`
For systemd-networkd:
`networkctl status -a`
If addresses or routes differ between the kernel and the backend, it usually means a second process applied config after NetworkManager or networkd initialized the interface. Common culprits: cloud-init, Docker (it modifies iptables and routes), and VPN clients.
For teams running infrastructure automation, tools like those at taskbotshub.ai can detect configuration drift between intended state and live kernel state by polling `ip` output and comparing against your desired config definitions. This is useful on fleets where manual changes accumulate between config management runs.
Packet-level debugging: `tcpdump -i ens3 -n -c 100 'not port 22'` captures 100 packets excluding your SSH session. Add `-w /tmp/cap.pcap` and pull the file for analysis in Wireshark when the issue is subtle.
For routing decisions, `ip route get 8.8.8.8` shows exactly which route the kernel would use for a packet to that destination, including the source address it would select. This is the fastest way to diagnose asymmetric routing problems.
# Full connectivity diagnostic sequence
ip addr show
ip route get 8.8.8.8
resolvectl query google.com
ping -c 3 -I ens3 192.168.10.1
tracepath 1.1.1.1
Interface Naming: Predictable vs. Kernel-Assigned
Predictable interface naming (biosdevname, systemd's udev naming policy) assigns names like ens3, enp2s0, or eno1 based on hardware topology. This replaced the old eth0/eth1 scheme to avoid renaming after hardware changes. On most physical servers you want predictable names. On VMs it depends on the hypervisor.
To see why an interface got its name:
`udevadm info /sys/class/net/ens3 | grep -E 'ID_NET_NAME|ID_PATH|ID_NET_DRIVER'`
To rename an interface permanently with a udev rule - useful for VMs where you want eth0 back or for servers where you want human-readable names like `wan0` and `lan0`:
Create /etc/udev/rules.d/70-persistent-net.rules:
``` SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="52:54:00:ab:cd:ef", NAME="wan0" ```
Then `udevadm control --reload && udevadm trigger`. The interface name change takes effect on the next interface down/up cycle or reboot.
For internal infrastructure projects and services running on these interfaces, a consistent naming convention across hosts saves confusion during incidents. The same discipline applies when naming the project itself or registering domains for internal tooling - a service like nicename.me can help you check name availability and settle on a consistent identifier before you bake it into DNS, firewall rules, and monitoring configs.
If you are running Kubernetes with Calico or Cilium, interface naming matters because CNI plugins often filter on interface name patterns. Verify your CNI config against actual interface names before cluster initialization.
# List all interfaces with their naming origin
ip -br link show
udevadm test /sys/class/net/ens3 2>/dev/null | grep ID_NET_NAME
IPv6 Configuration
IPv6 is enabled by default on most distributions and SLAAC (Stateless Address Autoconfiguration) will assign a global address automatically if your router sends Router Advertisements. On servers you typically want static IPv6 addressing instead.
Disable SLAAC and configure a static IPv6 address with nmcli:
```bash nmcli connection modify ens3-static \ ipv6.method manual \ ipv6.addresses 2001:db8:1::50/64 \ ipv6.gateway 2001:db8:1::1 \ ipv6.dns "2606:4700:4700::1111 2001:4860:4860::8888" ```
With Netplan:
```yaml ethernets: ens3: addresses: - 192.168.10.50/24 - 2001:db8:1::50/64 routes: - to: default via: 192.168.10.1 - to: '::/0' via: 2001:db8:1::1 nameservers: addresses: [1.1.1.1, 2606:4700:4700::1111] ```
To disable IPv6 entirely on a per-interface basis without disabling it system-wide, set the kernel parameter at runtime and persist it:
```bash sysctl -w net.ipv6.conf.ens3.disable_ipv6=1 echo 'net.ipv6.conf.ens3.disable_ipv6 = 1' >> /etc/sysctl.d/99-ipv6-disable.conf ```
Verify no IPv6 addresses are assigned: `ip -6 addr show ens3` should return empty output.
# Check IPv6 connectivity
ping6 -c 3 2606:4700:4700::1111
ip -6 route show
resolvectl status | grep -A5 'DNS over TLS'
Firewall and Network Interaction
nftables is the current Linux packet filtering framework, replacing iptables on RHEL 9 (firewalld uses nftables as its backend) and available on Ubuntu 24.04. Network configuration and firewall configuration interact at the interface level: firewalld zones are assigned per interface, and rules reference interface names.
After renaming an interface or adding a bond or VLAN, update the firewall zone assignment:
```bash # Check current zone assignments firewall-cmd --get-active-zones
# Move bond0 to the correct zone firewall-cmd --permanent --zone=internal --add-interface=bond0 firewall-cmd --reload ```
On Ubuntu without firewalld, use nftables directly. The default Ubuntu 24.04 install includes ufw which wraps nftables. After adding a new interface, check that ufw's default policies apply correctly:
`ufw status verbose`
One pattern we see cause outages: a VLAN interface created after ufw was configured inherits the default INPUT policy (usually DENY on hardened systems). Explicitly allow traffic on the new interface if needed:
`ufw allow in on vlan100`
For iptables users still managing legacy systems: `iptables -L -n -v --line-numbers` shows current rules with packet counts, which is useful for identifying whether rules are matching. On RHEL 9, iptables commands are transparently translated to nftables via iptables-nft. Check with `iptables-nft-save` versus `nft list ruleset` to see both views.
# View nftables ruleset directly
nft list ruleset
# View firewalld rules in nftables format
firewall-cmd --list-all-zones | grep -A10 'active'