Network Hardening
Reducing Exposed Services
Finding and disabling listening services you do not need.
The cheapest way to defend a service is to make sure it is not listening in the first place. A port that nothing binds to cannot be exploited, no matter how many vulnerabilities exist in the software that might have opened it. A fresh Ubuntu install typically exposes more than an operator expects: a stub DNS resolver, sometimes a mail transfer agent, occasionally a print daemon. Each is attack surface you probably do not need. This page is the first move in network hardening, ahead of Firewalls (which gate the listeners you keep) and SSH Hardening (which locks the one you must keep open).
Enumerating listeners with ss
ss is the modern replacement for netstat. The flags you want are -t (TCP), -u (UDP), -l (listening only), -p (show the process, needs root), and -n (numeric, do not resolve names).
sudo ss -tulpn
Expected output:
Netid State Local Address:Port Peer Address:Port Process
udp UNCONN 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=612,fd=12))
tcp LISTEN 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=612,fd=13))
tcp LISTEN 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=""789,fd=3))
tcp LISTEN 0.0.0.0:25 0.0.0.0:* users:(("master",pid=""901,fd=13))
Read the Local Address column carefully. 127.0.0.53:53 and 127.0.0.1 are bound to loopback, so they are reachable only from the host itself. 0.0.0.0:22 and 0.0.0.0:25 are bound to all interfaces, so they are reachable from the network. Here SSH on 0.0.0.0:22 is expected, but a mail server (Postfix master) listening on 0.0.0.0:25 is exposed to the world and worth questioning.
Mapping a listener to its unit
Once ss names the process, find the systemd unit that manages it so you can control it cleanly.
# Which unit owns the process holding port 25?
systemctl status $(sudo ss -tulpnH 'sport = :25' | grep -oP 'pid=\K[0-9]+' | head -1)
Expected output:
* postfix@-.service - Postfix Mail Transport Agent (instance -)
Loaded: loaded (/lib/systemd/system/postfix@.service; enabled)
Active: active (running) since Mon 2026-07-06 09:02:11 UTC
Disabling and masking services
For a service you do not need at all, disable --now stops it and prevents it starting at boot. mask goes further, linking the unit to /dev/null so it cannot be started even as a dependency of something else.
| Action | Command | Effect |
|---|---|---|
| Stop now | systemctl stop postfix | Frees the port until next boot or manual start |
| Disable at boot | systemctl disable --now postfix | Stops it and removes boot autostart |
| Mask entirely | systemctl mask postfix | Prevents any start, including as a dependency |
# Remove an unneeded mail server entirely
sudo systemctl disable --now postfix
sudo systemctl mask postfix
sudo ss -tulpn | grep ':25' || echo "port 25 no longer listening"
Expected output:
Created symlink /etc/systemd/system/postfix.service -> /dev/null.
port 25 no longer listening
Use mask for services you are certain you never want (a rule of thumb: if you would be alarmed to see it running, mask it). Use plain disable when you might re-enable it later, since a masked unit gives a confusing failure until unmasked.
Binding services to localhost
Some services you genuinely need but only for local use: a database that only the local application talks to, or a metrics endpoint scraped from the same host. Rather than removing them, bind them to 127.0.0.1 so they never accept remote connections. This is a per-service setting. For example, PostgreSQL uses listen_addresses = 'localhost' in postgresql.conf, and the stub resolver already binds to 127.0.0.53. After changing the config and restarting, confirm with ss that the Local Address is a loopback address rather than 0.0.0.0.
# After binding a local service to loopback, verify
sudo ss -tulpn | grep 5432
Expected output:
tcp LISTEN 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=""1120,fd=7))
A service bound to loopback needs no firewall rule and is unreachable from the network by construction, which is a stronger guarantee than an allow rule you might later mis-edit.
Practical Guidance
- Run
sudo ss -tulpnon every host and account for each listener: know what it is, why it listens, and on which address. - Treat
0.0.0.0bindings as the ones to scrutinise; loopback bindings are already unreachable from the network. - For services you never want,
disable --nowthenmaskso they cannot start even as a dependency. - For services needed only locally, bind them to
127.0.0.1rather than firewalling them, since a non-listening-to-the-world socket is a stronger guarantee than an allow rule. - Re-run
ss -tulpnafter changes to prove the port is gone, rather than assuming the config edit took effect. - Reducing listeners comes before firewalling: the smaller the surface, the simpler and safer your firewall rules in Firewalls.