Security

Close unused ports and disable unneeded services on Linux

Fewer running services means fewer things to patch and attack. List what listens on your VPS, disable what you do not use, bind private services to localhost and firewall the rest.

BeginnerLinux server5 min read

Every service that listens on a network port is something an attacker can probe, and something you have to keep updated. The simplest hardening step on any server is to run only what you need. This guide shows you how to find what is running, switch off what you do not use and keep the remaining services as private as possible.

Before you start

  • A Linux VPS running Ubuntu 22.04/24.04, Debian 12, or AlmaLinux/Rocky Linux 8 or 9.
  • An SSH session as root or a sudo user.
  • A rough list of what the server is for, such as "Nginx, PHP and MariaDB for two websites". Anything outside that list is a candidate for removal.

Warning: Do not stop or disable sshd, systemd-networkd, NetworkManager, systemd-resolved or the firewall unless you understand the effect. Turning off the wrong one can cut your connection. If that happens, log in through the browser console in your VPS provider's control panel.

Step 1: list what listens on the network

Bash
sudo ss -tulpn

Write down each line with its port, address and process. For example:

tcp LISTEN 0.0.0.0:22     users:(("sshd",...))
tcp LISTEN 0.0.0.0:80     users:(("nginx",...))
tcp LISTEN 0.0.0.0:3306   users:(("mariadbd",...))
tcp LISTEN 0.0.0.0:111    users:(("rpcbind",...))

For each one ask:

  • Do I use this at all?
  • Does it need to be reachable from the internet, or only from the server itself?

In the example, SSH and Nginx must be public. MariaDB only needs localhost. rpcbind is only for NFS and can usually go.

For more on reading this output, see Check which ports are open and listening on Linux.

Step 2: list enabled services

Some services do not listen on the network but still run at every boot. List them:

Bash
systemctl list-unit-files --type=service --state=enabled

And what is running right now:

Bash
systemctl list-units --type=service --state=running

Look up any name you do not recognise before you touch it:

Bash
systemctl status rpcbind

The description line usually explains its purpose.

Step 3: stop and disable what you do not need

disable --now stops a service and prevents it starting at boot:

Bash
sudo systemctl disable --now rpcbind

Some services are started by a socket unit that wakes them on demand. Disable the socket too:

Bash
sudo systemctl disable --now rpcbind.socket

If you are sure you will never need a service, mask it. That blocks it from being started, even by another package:

Bash
sudo systemctl mask rpcbind.service rpcbind.socket

Undo with sudo systemctl unmask <name>.

Common candidates on a web or app server, if you do not use them:

  • rpcbind and NFS services.
  • cups (printing).
  • avahi-daemon (local network discovery).
  • postfix or exim4 listening on the public IP when you do not send mail from this server.
  • Old FTP servers such as vsftpd. Use SFTP over SSH instead.

Remove the package if you never need it

Disabling is reversible. Removing is cleaner when you are sure:

Bash
sudo apt purge rpcbind             # Ubuntu and Debian
sudo dnf remove rpcbind            # AlmaLinux and Rocky Linux

Read the list of packages the command will remove before you confirm. Some packages pull others with them.

Step 4: bind private services to localhost

Services used only by programs on the same server should listen on 127.0.0.1. Examples:

  • MariaDB/MySQL: set bind-address = 127.0.0.1 in the [mysqld] section of the server config, then restart.
  • Redis: set bind 127.0.0.1 -::1 in redis.conf, then restart.
  • Node.js, Python or other apps behind Nginx: listen on 127.0.0.1:3000 and let Nginx proxy public traffic.
  • Docker containers: publish with -p 127.0.0.1:8080:80 instead of -p 8080:80.

Check the result with sudo ss -tulpn. The address should now start with 127.0.0.1.

Tip: If you need to reach a private service from your own computer, such as a database GUI, use an SSH tunnel instead of opening the port. See SSH local port forwarding.

Step 5: firewall the rest

What is left should be the services that must be public. Use a firewall with a default-deny policy for incoming traffic, and allow only those ports.

With UFW on Ubuntu or Debian:

Bash
sudo ufw allow 22/tcp
sudo ufw allow 80,443/tcp
sudo ufw default deny incoming
sudo ufw enable

Allow your SSH port first. If you use a custom SSH port, replace 22. On AlmaLinux and Rocky Linux, use firewalld instead.

Check it works

  1. Run sudo ss -tulpn again. Only the expected services should be on 0.0.0.0 or [::].
  2. From another machine, scan your own server:
Bash
   nmap -Pn 203.0.113.10

Only the public ports should appear as open.

  1. Reboot once and repeat. Disabled services must stay off.

Troubleshooting

  • A website broke after disabling a service: re-enable it with sudo systemctl enable --now <name> and check its role before trying again.
  • A disabled service came back: another unit or a socket starts it. Disable the .socket unit too, or mask the service.
  • App cannot reach the database after binding to localhost: the app connects to the public IP. Change its config to 127.0.0.1.
  • Container port still public after editing UFW: Docker publishes ports around UFW. Bind it to 127.0.0.1 in the -p option instead.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.