Proxy - firewalld on dc2-a-vcprx001
Proxy Solution · Config document · referenced from Interfaces and firewalld zones and firewalld rich rules
2001:db8:a2:b96::f:1 and appears in the listing with datacenter 1's 2001:db8:a1:b96::f:1. The commands are reproduced as typed; the mismatches are named in the sentences before the fences. Do not copy the Ansible and Sensu addresses of datacenter 1 without asking whether they belong on your host.The whole firewalld command set of the datacenter 2 proxy dc2-a-vcprx001 in the order of my working notes: the network state, the leftover eth0/eth1 bindings found and removed, the two interfaces bound to their zones, target DROP on internal, external and public, the default services, four ports and the squid service removed from internal, masquerading and ssh removed from external, logging of denied traffic, then the twenty-nine rich rules and one direct rule, the reload and the listings. Everything ran as root on dc2-a-vcprx001.
| Item | Value |
|---|---|
| Host | dc2-a-vcprx001.adm.example.net, RHEL 7.5, the proxy virtual machine of datacenter 2 |
| Interfaces | ens3 internal, 10.12.16.113/28 and 2001:db8:a2:b96::f:1/64, zone internal; ens4 external, 10.12.17.145/28 and 2001:db8:a4:b95::f:1/64, zone external |
| Run as | root on the host |
| Activated with | --permanent changes, systemctl restart firewalld after each change of the interface bindings, firewall-cmd --reload after each group of changes and once at the end |
Rich rules on internal | 29: 4 for the proxy port, 1 for ICMP, 8 for Ansible SSH, 4 for management SSH, 12 for Sensu SNMP; plus 1 direct rule for ICMPv6 |
| Software | firewalld as shipped with RHEL 7.5; the notes do not record the package version |
| Final state | the --list-rich-rules output of internal is its own Config document, rich rules on dc2-a-vcprx001; external and public listed no rich rules |
The commands
The network state before anything was changed. ifconfig as root on dc2-a-vcprx001. The MAC addresses and the link-local addresses are the same as on dc1-a-vcprx001, which the notes do not explain; I do not derive anything from it.
$ ifconfigoutput 28 lines
ens3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.12.16.113 netmask 255.255.255.240 broadcast 10.12.16.127
inet6 fe80::94ea:30ff:fe12:3456 prefixlen 64 scopeid 0x20<link>
inet6 2001:db8:a2:b96::f:1 prefixlen 64 scopeid 0x0<global>
ether 96:ea:30:12:34:56 txqueuelen 1000 (Ethernet)
RX packets 4768864 bytes 1293914724 (1.2 GiB)
RX errors 4152 dropped 0 overruns 0 frame 4152
TX packets 7406259 bytes 7004042893 (6.5 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
ens4: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.12.17.145 netmask 255.255.255.240 broadcast 10.12.17.159
inet6 fe80::fec4:a1ff:fe12:3457 prefixlen 64 scopeid 0x20<link>
inet6 2001:db8:a4:b95::f:1 prefixlen 64 scopeid 0x0<global>
ether fc:c4:a1:12:34:57 txqueuelen 1000 (Ethernet)
RX packets 3189163 bytes 3667930988 (3.4 GiB)
RX errors 3822 dropped 0 overruns 0 frame 3822
TX packets 992919 bytes 217685651 (207.6 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 4579 bytes 9584042 (9.1 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 4579 bytes 9584042 (9.1 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0The utilities installed first, as root: net-tools, bind-utils, mc and nc. No output in the notes.
$ yum install net-tools $ yum install bind-utils $ yum install mc $ yum install nc
Which zones were active. My NOTE: this is wrong, because the interfaces eth0 and eth1 are not in the system, which uses ens3 and ens4. The notes do not say where these bindings and the 192.168.0.0/16 source came from; it was a configuration that was already on the machine.
$ firewall-cmd --get-active-zonesoutput 5 lines
internal interfaces: eth0 sources: 192.168.0.0/16 external interfaces: eth1
The zones RHEL 7.5 defines by default.
$ firewall-cmd --get-zonesoutput 1 line
block dmz drop external home internal public trusted work
Steps 1 to 3: assign ens4 to zone external, ens3 to zone internal, restart firewalld. My NOTE: this method works also when NetworkManager is disabled (NM_CONTROLLED=no).
$ firewall-cmd --permanent --change-interface=ens4 --zone=external $ firewall-cmd --permanent --change-interface=ens3 --zone=internal $ systemctl restart firewalld
Step 4: which zones are active now. The new interfaces were added next to the old names, not instead of them.
$ firewall-cmd --get-active-zonesoutput 5 lines
internal interfaces: eth0 ens3 sources: 192.168.0.0/16 external interfaces: eth1 ens4
Steps 1 to 3: remove eth1 from external and eth0 from internal, restart firewalld. The same NOTE about NM_CONTROLLED=no stands above this step too.
$ firewall-cmd --permanent --remove-interface=eth1 --zone=external $ firewall-cmd --permanent --remove-interface=eth0 --zone=internal $ systemctl restart firewalld
Step 4: which zones are active now. The sources: 192.168.0.0/16 line on internal stays; nothing in the notes removes it.
$ firewall-cmd --get-active-zonesoutput 5 lines
internal interfaces: ens3 sources: 192.168.0.0/16 external interfaces: ens4
Steps 1 and 2: the default target of the two zones, both default.
$ firewall-cmd --permanent --zone=internal --get-targetoutput 1 line
default
The same for external.
$ firewall-cmd --permanent --zone=external --get-targetoutput 1 line
default
Steps 1 to 3: set the target of internal, external and public (the firewalld default zone) to DROP.
$ firewall-cmd --permanent --zone=internal --set-target=DROP $ firewall-cmd --permanent --zone=external --set-target=DROP $ firewall-cmd --permanent --zone=public --set-target=DROP
Step 1: what is allowed for zone external. The listing shows target: default: the --set-target was --permanent and nothing had been reloaded. The lab and datacenter 1 notes show target: DROP at the same point; which of the two is what firewalld prints is discussed in the Article.
$ firewall-cmd --zone=external --list-alloutput 13 lines
external (active) target: default icmp-block-inversion: no interfaces: ens4 sources: services: ssh ports: protocols: masquerade: yes forward-ports: source-ports: icmp-blocks: rich rules:
Step 2: zone internal. Besides the four default services this host had the squid service and the ports 443, 3128, 80 and 22 open, and the 192.168.0.0/16 source. The service and the ports are not on the datacenter 1 host; its first active-zones listing had the same sources: line, which had gone by this point there, after --change-interface and the restart, without a step that removed it.
$ firewall-cmd --zone=internal --list-alloutput 13 lines
internal (active) target: default icmp-block-inversion: no interfaces: ens3 sources: 192.168.0.0/16 services: mdns squid dhcpv6-client samba-client ssh ports: 443/tcp 3128/tcp 80/tcp 22/tcp protocols: masquerade: no forward-ports: source-ports: icmp-blocks: rich rules:
Step 3: zone public, not active, with dhcpv6-client, ssh and the port 3128/tcp.
$ firewall-cmd --zone=public --list-alloutput 13 lines
public target: default icmp-block-inversion: no interfaces: sources: services: dhcpv6-client ssh ports: 3128/tcp protocols: masquerade: no forward-ports: source-ports: icmp-blocks: rich rules:
Steps 1 to 9: remove the services mdns, samba-client and dhcpv6-client, the ports 443/tcp, 80/tcp, 3128/tcp and 22/tcp, and the service squid from internal, then reload. The step titles write "dhcp6-client"; the command uses the real name. Unlike datacenter 1, dhcpv6-client was not removed from public here, and 3128/tcp stayed open in public too; public has no interface, so it carries nothing.
$ firewall-cmd --permanent --zone=internal --remove-service=mdns $ firewall-cmd --permanent --zone=internal --remove-service=samba-client $ firewall-cmd --permanent --zone=internal --remove-service=dhcpv6-client $ firewall-cmd --permanent --zone=internal --remove-port=443/tcp $ firewall-cmd --permanent --zone=internal --remove-port=80/tcp $ firewall-cmd --permanent --zone=internal --remove-port=3128/tcp $ firewall-cmd --permanent --zone=internal --remove-port=22/tcp $ firewall-cmd --permanent --zone=internal --remove-service=squid $ firewall-cmd --reload
Steps 1 to 3: remove masquerading from external, in the words of my step title "we are not firewall, but proxy", remove the ssh service from external, reload. This is the only host of the three where ssh was taken off the external zone.
$ firewall-cmd --permanent --zone=external --remove-masquerade $ firewall-cmd --permanent --zone=external --remove-service=ssh $ firewall-cmd --reload
Step 1: the logging configuration before the change. This section is indented with a tab in the notes; that is formatting.
$ firewall-cmd --get-log-deniedoutput 1 line
off
Steps 2 and 3: log all denied traffic (unicast, broadcast, multicast) and show the setting again. The two spaces in the command are as typed. There is no --permanent on it: as I understand firewall-cmd, --set-log-denied changes the runtime and the permanent configuration at once.
$ firewall-cmd --set-log-denied=all $ firewall-cmd --get-log-denied
output 1 line
all
Rich rules, zone internal, proxy. For the IPv4 networks 192.168.0.0/16, 172.16.0.0/12 and 10.0.0.0/8 allow the proxy port TCP 3128 on the IPv4 address 10.12.16.113.
$ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 source address=192.168.0.0/16 destination address=10.12.16.113/32 port port=3128 protocol=tcp accept' $ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 source address=172.16.0.0/12 destination address=10.12.16.113/32 port port=3128 protocol=tcp accept' $ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 source address=10.0.0.0/8 destination address=10.12.16.113/32 port port=3128 protocol=tcp accept'
For the IPv6 network 2001:db8::/32 allow the proxy port TCP 3128 on the IPv6 address of this host. The step title in the notes says "on the IPv6 address 2001:db8:a1:b96::f:1", datacenter 1's address; the command has 2001:db8:a2:b96::f:1, this host's; the listing after the reload shows a1 again. Which address the rule had I cannot tell from the notes.
$ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv6 source address=2001:db8::/32 destination address=2001:db8:a2:b96::f:1/128 port port=3128 protocol=tcp accept'
Rich rules, zone internal, ICMP for IPv4. Entered with family=ipv4; the listing after the reload shows it as rule protocol value="icmp" accept, without the family.
$ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 protocol value=icmp accept'
ICMP for IPv6, as a direct rule in the INPUT_direct chain of the IPv6 filter table rather than a rich rule. Direct rules belong to no zone, so the --zone=internal on the command line has nothing to act on; the rule accepts ICMPv6 on every interface. That reading is mine, from the firewalld direct interface as I understand it; the notes only give the command.
$ firewall-cmd --permanent --zone=internal --direct --add-rule ipv6 filter INPUT_direct -p icmp6 -j ACCEPTRich rules, zone internal, Ansible, datacenter 1. For the addresses 10.11.17.241 and 10.11.17.242 allow SSH, TCP 22, on the IPv4 address 10.12.16.113.
$ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 source address=10.11.17.241/32 destination address=10.12.16.113/32 port port=22 protocol=tcp accept' $ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 source address=10.11.17.242/32 destination address=10.12.16.113/32 port port=22 protocol=tcp accept'
Ansible, datacenter 2. For the addresses 10.12.17.241 and 10.12.17.242 allow SSH on the IPv4 address 10.12.16.113.
$ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 source address=10.12.17.241/32 destination address=10.12.16.113/32 port port=22 protocol=tcp accept' $ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 source address=10.12.17.242/32 destination address=10.12.16.113/32 port port=22 protocol=tcp accept'
Ansible IPv6, datacenter 1. For the addresses 2001:db8:a1:b89::f:1 and 2001:db8:a1:b89::f:2 allow SSH on the IPv6 address of this host. The step title says "on the IPv6 address 2001:db8:a1:b96::f:1"; the rules have 2001:db8:a2:b96::f:1 and are right for this host.
$ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv6 source address=2001:db8:a1:b89::f:1/128 destination address=2001:db8:a2:b96::f:1/128 port port=22 protocol=tcp accept' $ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv6 source address=2001:db8:a1:b89::f:2/128 destination address=2001:db8:a2:b96::f:1/128 port port=22 protocol=tcp accept'
Ansible IPv6, datacenter 2. For the addresses 2001:db8:a2:b89::f:1 and 2001:db8:a2:b89::f:2 allow SSH on the IPv6 address of this host. The step title again says 2001:db8:a1:b96::f:1; the rules say a2.
$ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv6 source address=2001:db8:a2:b89::f:1/128 destination address=2001:db8:a2:b96::f:1/128 port port=22 protocol=tcp accept' $ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv6 source address=2001:db8:a2:b89::f:2/128 destination address=2001:db8:a2:b96::f:1/128 port port=22 protocol=tcp accept'
Rich rules, zone internal, OS management. For the IPv4 networks 192.168.0.0/16, 172.16.0.0/12 and 10.0.0.0/8 allow the SSH port TCP 22 on the IPv4 address 10.12.16.113.
$ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 source address=192.168.0.0/16 destination address=10.12.16.113/32 port port=22 protocol=tcp accept' $ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 source address=172.16.0.0/12 destination address=10.12.16.113/32 port port=22 protocol=tcp accept' $ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 source address=10.0.0.0/8 destination address=10.12.16.113/32 port port=22 protocol=tcp accept'
For the IPv6 network 2001:db8::/32 allow SSH, TCP 22, on the IPv6 address 2001:db8:a2:b96::f:1. The step title says "the proxy port (tcp/3128)", copied from the proxy group; the rule says port 22 and is right.
$ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv6 source address=2001:db8::/32 destination address=2001:db8:a2:b96::f:1/128 port port=22 protocol=tcp accept'
Rich rules, zone internal, Sensu, datacenter 1. For the IPv4 addresses 10.11.16.129, 10.11.16.130 and 10.11.16.131 allow SNMP, UDP 161, on the IPv4 address 10.12.16.113.
$ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 source address=10.11.16.129/32 destination address=10.12.16.113/32 port port=161 protocol=udp accept' $ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 source address=10.11.16.130/32 destination address=10.12.16.113/32 port port=161 protocol=udp accept' $ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 source address=10.11.16.131/32 destination address=10.12.16.113/32 port port=161 protocol=udp accept'
Sensu, datacenter 2. For the IPv4 addresses 10.12.16.129, 10.12.16.130 and 10.12.16.131 allow SNMP on the IPv4 address 10.12.16.113. The step title lists 10.11.16.131/32, a datacenter 1 host, as the third address; the third rule has 10.12.16.131 and is right.
$ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 source address=10.12.16.129/32 destination address=10.12.16.113/32 port port=161 protocol=udp accept' $ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 source address=10.12.16.130/32 destination address=10.12.16.113/32 port port=161 protocol=udp accept' $ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv4 source address=10.12.16.131/32 destination address=10.12.16.113/32 port port=161 protocol=udp accept'
Sensu IPv6, datacenter 1. For the IPv6 addresses 2001:db8:a1:bb1::f:1, 2001:db8:a1:bb1::f:2 and 2001:db8:a1:bb1::f:3 allow SNMP on the IPv6 address 2001:db8:a2:b96::f:1.
$ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv6 source address=2001:db8:a1:bb1::f:1/128 destination address=2001:db8:a2:b96::f:1/128 port port=161 protocol=udp accept' $ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv6 source address=2001:db8:a1:bb1::f:2/128 destination address=2001:db8:a2:b96::f:1/128 port port=161 protocol=udp accept' $ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv6 source address=2001:db8:a1:bb1::f:3/128 destination address=2001:db8:a2:b96::f:1/128 port port=161 protocol=udp accept'
Sensu IPv6, datacenter 2. For the IPv6 addresses 2001:db8:a2:bb1::f:1, 2001:db8:a2:bb1::f:2 and 2001:db8:a2:bb1::f:3 allow SNMP on the IPv6 address 2001:db8:a2:b96::f:1.
$ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv6 source address=2001:db8:a2:bb1::f:1/128 destination address=2001:db8:a2:b96::f:1/128 port port=161 protocol=udp accept' $ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv6 source address=2001:db8:a2:bb1::f:2/128 destination address=2001:db8:a2:b96::f:1/128 port port=161 protocol=udp accept' $ firewall-cmd --permanent --zone=internal --add-rich-rule='rule family=ipv6 source address=2001:db8:a2:bb1::f:3/128 destination address=2001:db8:a2:b96::f:1/128 port port=161 protocol=udp accept'
Steps 1 and 2: reload, then list the rich rules of zone internal. The twenty-nine lines it printed are the Config document rich rules on dc2-a-vcprx001.
$ firewall-cmd --reload $ firewall-cmd --permanent --zone=internal --list-rich-rules
Steps 3 and 4: list the rich rules of external and of public. Both printed nothing; the notes write "no output = ok" under each.
$ firewall-cmd --permanent --zone=external --list-rich-rules $ firewall-cmd --permanent --zone=public --list-rich-rules
What the rules say
| Part of a rule | Meaning |
|---|---|
rule family=ipv4 or family=ipv6 | the address family the rule is written for; a rule with addresses needs one |
source address=… | the client network or host; /32 and /128 are single hosts |
destination address=10.12.16.113/32 | the proxy's own address on ens3; the rule matches only traffic to this address, not to the external one |
port port=3128 protocol=tcp | the destination port and protocol: 3128 TCP for Squid, 22 TCP for SSH, 161 UDP for SNMP |
protocol value=icmp | the IP protocol ICMP, every type |
--direct --add-rule ipv6 filter INPUT_direct -p icmp6 -j ACCEPT | an iptables rule handed to ip6tables as written: table filter, chain INPUT_direct (the chain firewalld keeps for direct rules in INPUT), protocol ICMPv6, accept |
accept | the action; everything a rule does not accept falls through to the zone target DROP |
--permanent | written to the permanent configuration, active after --reload |
Two things need care. The sources: 192.168.0.0/16 on internal, left over from the earlier configuration, was never removed, so traffic from that range is placed in internal by its source address as well as by its interface; the notes do not mention it after the active-zones listing. And the direct rule does not appear in --list-rich-rules; as I understand firewalld, it would show in --direct --get-all-rules, which the notes did not run.
Checked against firewalld 2.5.2
| As built | Today |
|---|---|
firewalld-0.4.4.4-14.el7 on RHEL 7.5, iptables backend | firewalld 2.5.2 (September 2026); RHEL 7.9 ended with 0.6.3, RHEL 8 ships 0.9.x, RHEL 9 1.3.4, RHEL 10 2.3.1 and later 2.4.3. nftables has been the default backend since 0.6.0 and the iptables backend is deprecated since 1.0.0. RHEL 7 left Maintenance Support on 30 June 2024; only 7.9 is patched under ELS, until 31 May 2029 |
--direct --add-rule ipv6 filter INPUT_direct -p icmp6 -j ACCEPT | The direct interface "has been deprecated. It will be removed in a future release. It is superseded by policies" (firewalld.direct(5), since 1.0.0). Red Hat adds that the nftables backend does not support passing custom nftables rules through --direct and that direct rules "are not future-proof". The same thing without it: the rich rule rule family="ipv6" protocol value="ipv6-icmp" accept, or --add-protocol=ipv6-icmp on the zone; --add-protocol did not exist in 0.4.4.4 |
sources: 192.168.0.0/16 left next to interfaces: ens3 on internal | Still dispatched in the same order: source-based zone bindings are matched before interface-based ones ("Old behavior dictates that source based must dispatch before interface based" in the nftables backend). firewalld 2.0 added an explicit ingress and egress priority per zone; Red Hat documents that overlapping source zones are ordered by name and only the first is considered |
--permanent --change-interface, --remove-interface | Unchanged in meaning; for an interface under NetworkManager the zone is set on the connection, for others firewalld edits ZONE= in the ifcfg file. ifcfg files are gone in RHEL 10; the binding is nmcli connection modify <profile> connection.zone <zone> |
--set-target=DROP, and target: default in the listing | The four targets remain. Since 1.0.0 default "is identical to reject with one caveat: default allows ICMP packets". In 0.4.4.4 --set-target was documented as "Set the target of a permanent zone", a permanent-only option, which fits a runtime --list-all still printing default |
external with ssh and masquerade, internal with ssh mdns samba-client dhcpv6-client, public with ssh dhcpv6-client | The shipped zone files have the same services and masquerade and add <forward/>: intra-zone forwarding is on by default since 1.0.0 |
--remove-service, --remove-port=443/tcp, --remove-masquerade, --set-log-denied=all, --reload, --list-rich-rules | All documented with the same meaning; --set-log-denied still takes all, unicast, broadcast, multicast, off and defaults to off |
rule family=ipv4 source address=… destination address=… port port=3128 protocol=tcp accept | Grammar unchanged; priority= and the port protocols sctp and dccp were added. A family is still required whenever a source or destination address is used |
rule family=ipv4 protocol value=icmp accept | Unchanged; needed on a DROP zone, redundant on a default zone since 1.0.0 |
ifconfig | Deprecated in favour of ip since RHEL 7; net-tools still packaged in RHEL 9 and 10 |
Everything except the direct rule would be accepted as typed by a current firewalld. The direct rule is the one line to rewrite, and the sources: line left on internal is the one to remove, because its precedence over the interface binding is unchanged.