Skip to content

Networking & firewall

Scope: the course page's networking section adds one static IP with nmcli and opens one port with firewall-cmd — and stops there. Add a second NIC, a bond, or a container that needs to publish a port, and both tools turn out to have a whole model hiding behind that one command. This page covers NetworkManager's connection-profile model (why a "profile" isn't the same thing as an "interface"), firewalld's zone model in depth (why a zone, not one global rule list, is the unit of policy), and the Netavark bridge path a container's packet actually takes to reach the host firewall.

TL;DR — in 30 seconds:

  • NetworkManager configures a connection profile, not "an interface" — nmcli connection modify changes stored settings, and nothing on the wire changes until nmcli connection up re-activates it.
  • firewalld's unit of policy is the zone, not one global rule list — every interface belongs to exactly one zone, and --permanent writes to disk without touching the live ruleset until --reload.
  • A container's published port crosses three layers to reach the outside world — Netavark's bridge NAT rule, the kernel's nftables, and the host's firewalld zone — any one of which can be the reason it's unreachable.

1. NetworkManager's model: profiles, not interfaces

A RHEL host doesn't configure "the eth0 interface" directly — it configures a connection profile, a named bundle of settings (IP mode, addresses, DNS, VLAN, bonding) that NetworkManager then activates onto a device:

flowchart LR
    P1["Profile: static-eth0\nipv4.method=manual"]
    P2["Profile: dhcp-eth0\nipv4.method=auto"]
    DEV["Device: eth0"]
    P1 -.->|nmcli connection up static-eth0| DEV
    P2 -.->|nmcli connection up dhcp-eth0| DEV

Only one profile can be active on a device at a time, but a device can have several profiles defined for it — a static one for a fixed deployment, a DHCP one for a lab, a VLAN one for a trunked port — and switching between them is nmcli connection up <name>, not editing files:

nmcli connection show                        # every defined profile, active or not
nmcli connection show --active               # only what's live right now
nmcli device status                          # device -> currently active profile

This is why nmcli connection modify (change a profile's settings) and nmcli connection up (apply them) are two separate steps — a modify only rewrites the stored profile; nothing on the wire changes until something re-activates it.

2. Bonds and VLANs — profiles that stack

A bond or a VLAN is not a special case bolted onto NetworkManager — it's another connection profile, one that names other profiles as its members instead of a physical device:

# Bond two NICs for redundancy/throughput
sudo nmcli connection add type bond con-name bond0 ifname bond0 bond.options "mode=active-backup"
sudo nmcli connection add type ethernet con-name bond0-eth0 ifname eth0 master bond0
sudo nmcli connection add type ethernet con-name bond0-eth1 ifname eth1 master bond0
sudo nmcli connection up bond0

# A VLAN on top of a physical interface
sudo nmcli connection add type vlan con-name vlan100 ifname eth0.100 dev eth0 id 100
sudo nmcli connection modify vlan100 ipv4.method manual ipv4.addresses 10.0.100.5/24
sudo nmcli connection up vlan100

The bridge profile type from the course page is the same idea again — a profile that other profiles attach to as master. Once you can read one of these (parent profile + member profiles + up to activate), you can read all of them.

3. firewalld's zone model, in depth

The course page opens one port in the default zone. The model underneath is richer: every network interface is assigned to exactly one zone, and a zone is a named bundle of trust decisions — which services and ports are allowed, whether ICMP is answered, whether the zone is even reachable at all:

flowchart TB
    ETH0["eth0 (uplink)"] --> PUBLIC["zone: public\nssh only"]
    ETH1["eth1 (internal)"] --> INTERNAL["zone: internal\nhttp, https, custom app ports"]
    DOCKER["podman0 (container bridge)"] --> TRUSTED["zone: trusted\n(or a dedicated zone)"]
sudo firewall-cmd --get-zones                              # every zone firewalld knows about
sudo firewall-cmd --get-active-zones                        # zone -> interface(s) actually assigned
sudo firewall-cmd --zone=internal --list-all                # rules for one specific zone
sudo firewall-cmd --zone=internal --change-interface=eth1 --permanent

A common two-NIC pattern: a locked-down public zone for the internet-facing interface (SSH only), and a more permissive internal zone for a private-network interface (application ports, monitoring scrape). Everything from §9 of the course page — --add-service, --add-port, --permanent, --reload — takes an explicit --zone=<name> the same way; omitting it just targets whichever zone is the current default.

Runtime vs. permanent, precisely: firewalld actually holds two configurations at once — the live, in-kernel ruleset ("runtime") and the on-disk ruleset ("permanent"). A plain firewall-cmd --add-service=https changes runtime only and is gone on the next reload or reboot; --permanent writes to disk but does not touch the live ruleset until --reload copies permanent over runtime. Testing a change safely means applying it without --permanent first, confirming it behaves, then re-running it with --permanent --reload once you're sure — a bad runtime-only rule is undone by firewall-cmd --reload, no bad state persists.

4. Netavark — the container bridge's path to the host firewall

The course page names the Netavark backend as the thing that programs podman0/podman1 and says it "traverses firewalld." Here is that path end to end:

flowchart LR
    C["Container\n10.88.0.5"] -->|veth pair| BR["podman0 bridge\n(Netavark-managed)"]
    BR -->|nftables NAT rule\nPublishPort 8080:80| NFT["kernel nftables"]
    NFT -->|matched against| ZONE["firewalld zone\nfor the physical uplink"]
    ZONE --> HOST["Host's external interface"]

Netavark is Podman's default network backend, replacing the older CNI plugins (per Podman's own docs). For every container network it creates, it writes the bridge (podman0 for the default network, podman1, … for additional ones), assigns a subnet, and programs the nftables NAT rules that make a --publish 8080:80 actually forward traffic from the host's port 8080 into the container's port 80. Those NAT rules sit inside the kernel's nftables tables that firewalld also manages — which is why a container's published port can still be blocked at the zone level if the host's zone doesn't allow inbound traffic on it, even though the container itself is running and listening fine.

The practical debugging order when "the container is running but the port isn't reachable from outside":

podman port <container>                          # confirm the publish mapping podman thinks exists
sudo firewall-cmd --list-all                      # is the port/service actually allowed in the active zone?
sudo nft list ruleset | grep -A2 <port>            # confirm the NAT rule is present at the nftables layer

Container-to-container traffic on the same bridge network doesn't touch firewalld at all — it's resolved by the bridge and Netavark's built-in DNS (containers on a shared network can reach each other by container name); firewalld only enters the path once a container's port is published to the host and needs to cross into the outside world.

5. How this connects

The course page's one static IP and one open port is the 80% you'll do on almost every RHEL host. This page is what to reach for once a host needs more than one network profile (a bond, a VLAN, a DHCP-vs-static switch), once policy needs to differ per interface instead of one flat rule list (the zone model), or once you're debugging why a container's published port isn't reachable and need to know which of three layers — Podman's mapping, firewalld's zone, or the raw nftables rule — is the one saying no.

Common gotchas

  • Running nmcli connection modify and expecting the change to take effect immediately. Fix: a modify only rewrites the stored profile — nothing changes on the wire until nmcli connection up <name> re-activates it.
  • Forgetting --zone=<name> and assuming a firewalld rule landed where intended. Fix: an --add-service/--add-port without an explicit zone targets whichever zone is the current default — on a multi-zone host that's easy to get wrong silently.
  • Testing a firewalld change with --permanent from the start. Fix: --permanent writes to disk but doesn't touch the live ruleset until --reload — test without --permanent first, confirm it behaves, then re-apply with --permanent --reload once you're sure.
  • Assuming a running, listening container means its published port is reachable. Fix: the port still has to clear the host's firewalld zone — check podman port, then firewall-cmd --list-all, then the nftables ruleset, in that order, before assuming the container itself is broken.
Check yourself — you run nmcli connection modify static-eth0 ipv4.addresses 10.0.0.5/24 and ping the host from another machine. The IP hasn't changed. Why?

modify only rewrites the stored profile on disk — it doesn't touch what's live on the wire. The profile has to be re-activated with nmcli connection up static-eth0 before the new address takes effect.

Check yourself — a container is running and podman port confirms it's publishing 8080:80, but the port isn't reachable from outside the host. Where do you look, in order, and why that order?

podman port (confirm the mapping podman thinks exists) → firewall-cmd --list-all (is the port allowed in the active zone?) → nft list ruleset (is the NAT rule actually present at the kernel level?). That order goes from the layer closest to the container outward to the layer closest to the outside world, so each check rules out one hop before moving to the next.

You can defend this when you can explain why nmcli connection modify doesn't change anything on the wire by itself, what a zone actually groups (rules) and how two zones on one host let policy differ per interface, the difference between firewalld's runtime and permanent configuration and why --reload matters, and the three-hop path (bridge → nftables NAT → firewalld zone) a published container port takes to reach the outside world.