>
Open Source

How to expose a Linux service through whichever firewall is running

If a service works on the box you started it on but cannot be reached from another machine, the host firewall is one of the first places to look. Opening a port tells the firewall to accept matching traffic, but it does not start the service and it does not change the address the service listens on. The Linuxize walkthrough of opening ports on ufw (Uncomplicated Firewall, the default firewall front-end on Ubuntu), firewalld (the default on Fedora, RHEL, and Rocky Linux), nftables (the modern replacement for iptables on most current distros), and legacy iptables is thorough and worth reading. What I want to add is what catches people who only read the first command and skip the verification steps.

Most “I opened the port and it still doesn’t work” tickets I have seen come down to one of three things: the service is not actually listening, the rule landed in the wrong firewall zone, or the cloud provider’s security group (a stateful packet filter that sits in front of your VM and applies before any host-level firewall rule) is silently blocking the traffic. The Linux firewall is rarely the only thing between your service and the network. It is just the layer most people touch first.

The right firewall manager to use

The single most important rule is to use the firewall manager that is already active on the system. Do not add raw nftables or iptables rules alongside ufw or firewalld, because the manager may replace or reorder your hand-written rules on the next reload. On a current Ubuntu box, that is ufw. On Fedora, RHEL, Rocky, or Alma, that is firewalld. On anything older or anything custom, you are probably looking at nftables directly.

A quick way to confirm which manager is in charge:

  • sudo ufw status returns a status block on Ubuntu-style systems.
  • sudo firewall-cmd --state returns “running” on firewalld systems.
  • sudo nft list ruleset returns the active ruleset when nftables is the underlying engine.
  • sudo iptables -L -n returns rules when the legacy backend is in use, but that backend is deprecated on most current distros.

If two of these respond, you have a confused host. Pick one manager, disable the other, and confirm before you add rules. Mixed firewall state is the kind of mess that takes a day to untangle and a second outage to discover.

Confirm the service is actually listening

Before you change a single firewall rule, confirm the service is listening on the expected port. The ss command (replacement for the older netstat) is the right tool:

sudo ss -ltnp 'sport = :80'

If the command returns no output, nothing is listening on port 80, and no firewall rule will fix that. Start or reconfigure the service first.

If the listening address is 127.0.0.1:80 or [::1]:80, the service is accepting only local connections. Configure it to listen on the host’s network address (0.0.0.0:80 or [::]:80) before you bother with the firewall. Many tutorials skip this step and the user spends an hour wondering why the open port is not reachable.

For UDP services, swap -ltnp for -lunp. The two protocols use separate rule tables in every Linux firewall manager, and a rule on the wrong protocol does nothing.

ufw, the path of least resistance

On Ubuntu and Debian systems with ufw enabled:

sudo ufw allow 80/tcp

That command accepts TCP traffic to port 80 from any source. If you want to restrict the source, append from <cidr> (CIDR notation, a way of writing an IP address plus its network mask, like 192.0.2.0/24) to limit the rule to a specific network. ufw persists the change across reboots by default, which is one of the things that makes it the easiest manager to get right.

To confirm the rule is in place, sudo ufw status verbose prints the active ruleset with numbers, and sudo ufw status numbered lets you reference a rule by number for deletion. The numbering is useful when you are tightening the ruleset in stages and need to remove temporary open rules.

firewalld and the zone trap

On Fedora, RHEL, Rocky, and Alma, firewalld is the default and works through zones rather than a flat ruleset:

sudo firewall-cmd --zone=public --add-port=80/tcp

The --zone=public flag is the part most people get wrong. The active zone is whatever zone is attached to the incoming network interface, and a rule in the wrong zone is silently ignored. Confirm with sudo firewall-cmd --get-active-zones and use the zone that actually owns the interface the traffic arrives on.

firewalld has a runtime configuration and a permanent configuration, and they are separate. The --add-port flag without --permanent only updates the runtime, and the change disappears on the next reload or reboot. To persist:

sudo firewall-cmd --permanent --zone=public --add-port=80/tcp && sudo firewall-cmd --reload

The reload applies the permanent config. Skipping --permanent is the most common firewalld mistake, and the symptom is “the rule works until I reboot.”

Raw nftables, when you actually need it

nftables is the underlying engine on most modern distros, but ufw and firewalld both write nftables rules under the hood. When you want to manage rules directly, the syntax is more flexible and the foot-guns are larger:

sudo nft add rule inet filter input tcp dport 80 accept

That assumes you have an inet filter input chain defined. Raw nftables rules use administrator-defined table and chain names, and the right move is to inspect the existing ruleset with sudo nft -a list ruleset before adding anything. If your table and chain names differ, the rule lands nowhere.

nftables rules are not persistent by default. Persist them by saving the ruleset to /etc/nftables.conf and enabling the nftables service, or by writing a small systemd unit that loads the ruleset at boot. The persistence step is the part most tutorials gloss over.

Verify from the outside

Open the rule, then prove it works from another host. The nc command (netcat, a small utility for reading and writing raw network connections) is the right tool:

nc -zv server.example.com 80

The -z flag tells nc to scan without sending data; the -v flag prints the result. A “Connection to server.example.com 80 port [tcp/http] succeeded!” line confirms the port is reachable. A timeout or “Connection refused” means the rule did not land where you thought it did.

If nc from another host succeeds but your application still cannot connect, the problem is at the application layer, not the firewall. If nc fails, walk back through the chain: is the service listening, is the firewall rule in the right zone, is the cloud security group open, is there a router doing NAT (network address translation, the process where a router rewrites the destination address of incoming traffic to a different internal address) that needs a port-forward rule?

Trade-offs

Convenience versus control is the core trade-off in firewall choice. ufw and firewalld are both easier to use and harder to misuse than raw nftables, but they hide what is happening at the underlying layer. If you need to debug a complex rule set or you need a feature the front-ends do not expose, raw nftables gives you that, at the cost of owning every part of the configuration.

Availability versus exposure is the trade-off in opening a port. Opening 80/tcp to the world is the right call for a public web server. Opening it for “just my office” is a maintenance burden the day your office IP changes. Narrower source ranges mean fewer things break later, but also more time spent maintaining CIDR lists when the office network changes.

Trust versus proof is the trade-off in verification. A successful nc from another host is proof. The absence of an error in the firewall logs is not proof. The discipline of testing from the outside is the cheapest insurance you can buy for this layer of the stack.

What I would tell past me

A short list, if I were starting over.

  • Always check ss first. A firewall rule on a port that is not listening is a rule that does nothing, and a service bound to 127.0.0.1 is not reachable from the network no matter what the firewall says.
  • Use the firewall manager that is already on the system. Mixing ufw with hand-written iptables is the kind of mistake that takes a day to debug.
  • On firewalld, use --permanent and reload. Skipping it works until the next reboot.
  • Test from outside the host, not from inside. Inside the host, the firewall is bypassed for loopback traffic, so an “it works locally” result tells you nothing about reachability.
  • Remember the cloud security group. A correct Linux firewall rule behind a provider firewall that blocks the port is still a blocked port. The Linux firewall is one layer, not the only layer.

Leave a comment