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.

ActionCommandEffect
Stop nowsystemctl stop postfixFrees the port until next boot or manual start
Disable at bootsystemctl disable --now postfixStops it and removes boot autostart
Mask entirelysystemctl mask postfixPrevents 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

  1. Run sudo ss -tulpn on every host and account for each listener: know what it is, why it listens, and on which address.
  2. Treat 0.0.0.0 bindings as the ones to scrutinise; loopback bindings are already unreachable from the network.
  3. For services you never want, disable --now then mask so they cannot start even as a dependency.
  4. For services needed only locally, bind them to 127.0.0.1 rather than firewalling them, since a non-listening-to-the-world socket is a stronger guarantee than an allow rule.
  5. Re-run ss -tulpn after changes to prove the port is gone, rather than assuming the config edit took effect.
  6. Reducing listeners comes before firewalling: the smaller the surface, the simpler and safer your firewall rules in Firewalls.