firewalld Basics on CentOS 7
CentOS 7 replaced the old iptables service with firewalld, and a lot of admins reacted by disabling it and installing iptables-services to get the familiar /etc/sysconfig/iptables back. I did that too on my first few CentOS 7 boxes. It turns out firewalld is worth learning: zones map cleanly onto the "public interface vs. internal interface" split most database servers have, and rules can be changed at runtime without flushing everything and dropping connections.
Below are the firewall-cmd commands I run on nearly every new box, roughly in order. Firewalld is in the default @core group (yum install firewalld if it's missing). Have console access ready, because sooner or later you will lock yourself out of SSH.
Is it running, and which zone am I in?
systemctl enable firewalld
systemctl start firewalld
firewall-cmd --state
firewall-cmd --get-default-zone
firewall-cmd --get-active-zones
On a stock install the default zone is public, and every interface without an explicit zone lands there. The public zone allows ssh and dhcpv6-client and nothing else. That is a sane starting point.
To see the full rule set of the zone you are about to modify:
firewall-cmd --zone=public --list-all
Runtime vs. permanent
This is the part that trips people up, and it got me more than once. Without --permanent, a change applies immediately but disappears on the next reload or reboot. With --permanent, it is written to /etc/firewalld/zones/public.xml but does not take effect until you reload.
My habit is to make the permanent change and then reload, so the two never drift apart:
firewall-cmd --permanent --zone=public --add-service=http
firewall-cmd --reload
If you want to test a rule first and only keep it if it works, do it the other way around: apply at runtime, verify, then run firewall-cmd --runtime-to-permanent to save the current live state.
Opening services and ports
Services are just named XML files in /usr/lib/firewalld/services/ that bundle the ports. List them with firewall-cmd --get-services. Opening the usual web and database ports:
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --add-port=3306/tcp
firewall-cmd --reload
If you run SSH on a non-standard port, add the port and, once you have confirmed you can connect on it, remove the ssh service:
firewall-cmd --permanent --add-port=2222/tcp
firewall-cmd --reload
# test the new port from another machine first!
firewall-cmd --permanent --remove-service=ssh
firewall-cmd --reload
Remember that on CentOS 7 with SELinux enforcing you also need semanage port -a -t ssh_port_t -p tcp 2222 before sshd will bind to that port.
Limiting a port to certain source addresses
I never leave 3306 open to the whole interface. Rich rules let you say who gets in:
firewall-cmd --permanent --remove-port=3306/tcp
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.1.0/24" port port="3306" protocol="tcp" accept'
firewall-cmd --reload
An alternative that reads better when there are many trusted hosts is a dedicated zone. Bind the source network to the internal zone and allow the service there:
firewall-cmd --permanent --zone=internal --add-source=10.0.1.0/24
firewall-cmd --permanent --zone=internal --add-port=3306/tcp
firewall-cmd --reload
Traffic from 10.0.1.0/24 is now evaluated against internal; everything else stays in public.
A second interface
Servers with a private NIC usually want it in trusted or internal:
firewall-cmd --permanent --zone=internal --change-interface=eth1
firewall-cmd --reload
firewalld also writes ZONE=internal into /etc/sysconfig/network-scripts/ifcfg-eth1 so the assignment survives NetworkManager restarts.
Checking the result
firewall-cmd --list-all-zones | less
firewall-cmd --zone=public --list-services
firewall-cmd --zone=public --list-ports
firewall-cmd --zone=public --list-rich-rules
From a machine that should be blocked, nc -zv 203.0.113.10 3306 should time out rather than connect. From an allowed host it should print Connection to 203.0.113.10 3306 port [tcp/mysql] succeeded!. If you want to see the actual iptables rules firewalld generated, iptables -L IN_public_allow -n shows the chain for the public zone.
Gotchas
FirewallD is not running: the service is stopped or masked.systemctl unmask firewalld; systemctl start firewalld. If you previously installediptables-services, the two conflict; disable one.Error: ALREADY_ENABLED: 3306:tcpis harmless; the rule already exists in that mode (runtime or permanent). Check the other mode.- A rule you added "disappeared" after a reboot: you forgot
--permanent. Comparefirewall-cmd --list-portswithfirewall-cmd --permanent --list-ports. - Docker bypasses firewalld: Docker inserts its own iptables rules, so a published container port is reachable even if the zone does not allow it. Either bind containers to
127.0.0.1or put the Docker rules in theDOCKER-USERchain (available in Docker 17.06 and later).
I keep /etc/firewalld/zones/ in version control with the rest of the server's config. Rebuilding a firewall on a replacement machine is then a copy and a reload.