Stripping firewalld down to what you serve
After the Virtualmin installer has run on a CentOS 7 box, the firewalld public zone is wide open. The installer assumes you'll run the whole stack, so mail, IMAP, POP3, FTP, DNS, MySQL, SSH, Webmin and Usermin are all reachable from anywhere. On a web server that only serves websites and sends mail through a relay, almost none of that should face the internet.
The approach: leave HTTP and HTTPS in the public zone, and move everything administrative into the internal zone, which only a short list of source addresses ever land in.
How zones decide
firewalld picks a zone for each incoming packet. If the source address matches a source bound to a zone, that zone wins. Otherwise the zone of the interface the packet arrived on is used, which on a single-NIC server is public. So adding your office address as a source of internal means traffic from the office gets internal's rules, and everyone else gets public's.
See where you are first:
firewall-cmd --get-active-zones
firewall-cmd --zone=public --list-all
firewall-cmd --zone=internal --list-all
Set up the trusted zone first
Do this before removing anything from public, or you can cut off your own SSH session.
firewall-cmd --zone=internal --add-source=203.0.113.10/32 --permanent
firewall-cmd --zone=internal --add-source=203.0.113.11/32 --permanent
firewall-cmd --zone=internal --add-service=ssh --permanent
firewall-cmd --zone=internal --add-service=mysql --permanent
firewall-cmd --zone=internal --add-port=10000-10010/tcp --permanent
firewall-cmd --reload
internal also ships with mdns, samba-client and dhcpv6-client. None of those are wanted on a server, so take them out:
firewall-cmd --zone=internal --remove-service=mdns --permanent
firewall-cmd --zone=internal --remove-service=samba-client --permanent
firewall-cmd --zone=internal --remove-service=dhcpv6-client --permanent
firewall-cmd --reload
Now open a second SSH session from one of the trusted addresses and check it works. Keep it open.
Strip the public zone
Services the installer added:
firewall-cmd --zone=public --remove-service=smtp --permanent
firewall-cmd --zone=public --remove-service=ftp --permanent
firewall-cmd --zone=public --remove-service=pop3 --permanent
firewall-cmd --zone=public --remove-service=pop3s --permanent
firewall-cmd --zone=public --remove-service=imap --permanent
firewall-cmd --zone=public --remove-service=imaps --permanent
firewall-cmd --zone=public --remove-service=mysql --permanent
firewall-cmd --zone=public --remove-service=dhcpv6-client --permanent
firewall-cmd --zone=public --remove-service=ssh --permanent
And the raw ports:
firewall-cmd --zone=public --remove-port=587/tcp --permanent
firewall-cmd --zone=public --remove-port=53/tcp --permanent
firewall-cmd --zone=public --remove-port=53/udp --permanent
firewall-cmd --zone=public --remove-port=20/tcp --permanent
firewall-cmd --zone=public --remove-port=2222/tcp --permanent
firewall-cmd --zone=public --remove-port=10000-10010/tcp --permanent
firewall-cmd --zone=public --remove-port=10000-10100/tcp --permanent
firewall-cmd --zone=public --remove-port=20000/tcp --permanent
firewall-cmd --reload
Some of these will say NOT_ENABLED because the installer's version or options differed. That's harmless; the point is that none of them stay open. Port 10000 is Webmin, 20000 is Usermin, 587 is mail submission and 53 is DNS. If the server doesn't host mail or DNS for anyone, none of them should be public.
What should be left:
firewall-cmd --zone=public --list-all
public (active)
target: default
interfaces: eth0
services: http https
ports:
...
If http and https aren't there, add them:
firewall-cmd --zone=public --add-service=http --add-service=https --permanent
firewall-cmd --reload
Things to watch
--permanent only writes the configuration; it doesn't change the running firewall until --reload. Without --permanent the change is live but gone at the next reload or reboot. I use --permanent everywhere and reload once at the end, so the running state and the saved state never drift apart. If you've been testing with runtime-only rules and want to keep them, firewall-cmd --runtime-to-permanent saves the current state.
Removing a service from firewalld doesn't stop it. Dovecot, BIND and ProFTPD will still be running and listening, just unreachable. If you don't need them, stop and disable them as well, so a mistake in the firewall later doesn't expose them again.
If your own address changes (a new office line, working from home), you'll need a way back in that doesn't depend on it: the provider's console, or a second trusted address such as a VPN endpoint or bastion host in the source list.