Introduction

One of the most effective ways to harden a Linux server is to restrict SSH access to a known set of trusted IP addresses or network ranges. Leaving SSH open to the entire internet exposes your server to brute-force attacks, credential stuffing, and zero-day exploits targeting the SSH daemon. In this guide, we’ll use firewalld and its IPSet feature to whitelist specific networks and completely block all other SSH connections — in just a handful of commands.

This approach is particularly well-suited for server administrators, ISPs, and infrastructure teams who manage access from known office ranges, VPN exit nodes, or jump hosts.


Prerequisites

  • A Linux server running firewalld (Debian, RHEL, CentOS, Rocky Linux, AlmaLinux, Fedora, or compatible)
  • Root or sudo access
  • Important: Before you begin, make sure at least one of the IP ranges you will whitelist covers your current connection. Locking yourself out of SSH is a painful experience.

What Is a firewalld IPSet?

An IPSet is a named collection of IP addresses, network ranges (CIDRs), or MAC addresses that firewalld can reference in rules. Instead of writing one rich rule per IP address, you define the set once and reference it by name. This keeps your firewall configuration clean, readable, and easy to maintain — adding or removing a trusted network is a single command rather than a rule rewrite.

The IPSet type we use here is hash:net, which is optimized for storing and matching network prefixes (CIDR notation). It uses a hash table internally, making lookups extremely fast even with large sets.

Step 1 — Create the IPSet

firewall-cmd --permanent --new-ipset=adminnets --type=hash:net

This creates a persistent IPSet named adminnets of type hash:net. The --permanent flag writes the configuration to disk so it survives reboots. The set is empty at this point — we’ll populate it in the next step.

You can name the set anything meaningful. adminnets clearly communicates its purpose: the networks from which administrative access is permitted.

Step 2 — Add Your Trusted Networks to the IPSet

firewall-cmd --permanent --ipset=adminnets --add-entry=6.7.8.0/20
firewall-cmd --permanent --ipset=adminnets --add-entry=11.12.13.0/24
firewall-cmd --permanent --ipset=adminnets --add-entry=15.1.1.1/32

Each command adds a network entry to the adminnets IPSet:

  • 6.7.8.0/20 — a /20 block covering 4,096 addresses (e.g., a corporate or upstream network range)
  • 11.12.13.0/24 — a /24 block covering 256 addresses (e.g., an office or data center subnet)
  • 15.1.1.1/32 — a single host (e.g., a VPN exit node or a specific jump server)

Replace these with the actual IP ranges relevant to your infrastructure. You can add as many entries as needed, and you can always add more later with the same --add-entry syntax without reloading the entire ruleset.

Step 3 — Create a Rich Rule to Allow SSH from the IPSet

firewall-cmd --permanent \
  --zone=public \
  --add-rich-rule='rule source ipset=adminnets service name=ssh accept'

This is where the IPSet gets put to work. A rich rule in firewalld allows you to express more complex matching logic than simple service allow/deny rules. This particular rule instructs firewalld to accept SSH connections (service name=ssh, which targets TCP port 22 by default) only when the source IP matches an entry in the adminnets IPSet.

The rule is applied in the public zone, which is the default zone for most server deployments. If your server uses a different active zone, substitute accordingly — you can check with firewall-cmd --get-active-zones.

Step 4 — Apply the Configuration

firewall-cmd --reload

The --reload command makes firewalld re-read its permanent configuration and apply it to the running instance without dropping existing connections. At this point, SSH is accepted from your trusted ranges via the rich rule, but SSH is still also open to the world via the default service rule in the public zone. The next step closes that gap.

Step 5 — Remove the Default SSH Service Rule

firewall-cmd --permanent --zone=public --remove-service=ssh
firewall-cmd --reload

By default, many distributions add ssh as a permitted service in the public zone, which allows SSH from any source address. This step removes that blanket permission. After the second reload, the only rule allowing SSH is your rich rule tied to the adminnets IPSet — all other SSH connection attempts will be silently dropped.

⚠️ Critical Warning: Do not run Step 5 without first verifying that Step 4 succeeded and that your current IP is covered by one of the entries added in Step 2. Open a second SSH session to confirm your access before removing the default service rule.

Verification

After the final reload, confirm your configuration is correct:

# Check the contents of the IPSet
firewall-cmd --ipset=adminnets --get-entries

# Verify the rich rule is in place
firewall-cmd --zone=public --list-rich-rules

# Confirm the default SSH service is no longer listed
firewall-cmd --zone=public --list-services

The output of the last command should no longer include ssh in the service list, and the rich rule referencing adminnets should appear in the rich rules output.

Managing the IPSet Over Time

One of the key benefits of this approach is how easy ongoing maintenance becomes:

# Add a new trusted network later
firewall-cmd --permanent --ipset=adminnets --add-entry=203.0.113.0/24
firewall-cmd --reload

# Remove a network that is no longer trusted
firewall-cmd --permanent --ipset=adminnets --remove-entry=51.15.42.187/32
firewall-cmd --reload

There is no need to touch the rich rule itself — it always references the current contents of the adminnets IPSet dynamically.

Why This Approach Is Better Than /etc/hosts.allow or iptables Rules

Compared to /etc/hosts.allow (TCP Wrappers, which is deprecated in modern distributions) or raw iptables rules, the firewalld IPSet approach offers several advantages. Configuration is persistent and human-readable in XML, the IPSet abstraction decouples the list of trusted sources from the rule logic, and the hash:net data structure scales efficiently to hundreds or thousands of entries without performance degradation. firewalld also integrates cleanly with NetworkManager and systemd, making it the preferred firewall management layer on modern RHEL-family systems.

Conclusion

In fewer than ten commands, you’ve gone from a server with SSH exposed to the entire internet to one where only explicitly trusted networks can reach it. This is one of the highest-impact, lowest-effort hardening measures you can apply to any Linux server. Combined with key-based authentication and fail2ban for defense-in-depth, IPSet-based SSH whitelisting gives you strong protection against the vast majority of automated attacks targeting exposed SSH services.

If you manage multiple servers, consider exporting the IPSet definition and distributing it via your configuration management tool of choice (Ansible, Puppet, Salt) so your trusted source list stays consistent across your entire infrastructure.