Every open port on a server is a service that someone on the internet can talk to, so knowing exactly which ports are open, and why, is one of the first things to check on any machine. In this tutorial you will look at the problem from two sides: from inside the server with ss (and the older netstat) to see which programs are listening, and from outside with nmap to see what is actually reachable through your firewall. Then you will close what you do not need and save a baseline to compare against later.

Prerequisites

To follow this tutorial, you will need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user that has sudo privileges.
  • A second machine to scan from (your workstation or another server). Scanning a server from itself goes through the loopback interface and bypasses the firewall, so it does not show what the internet sees.
  • The public IP address of the server, referred to as your_server_ip in this guide.

Step 1 - Listing listening ports with ss

ss is part of the iproute2 package, is installed by default on Ubuntu and replaces netstat. The following command shows every listening TCP and UDP socket with the process that owns it:

sudo ss -tulpn

The flags mean: -t TCP, -u UDP, -l listening sockets only, -p show the process (needs sudo to see processes of other users) and -n show numeric ports instead of service names.

Netid State  Recv-Q Send-Q  Local Address:Port  Peer Address:Port Process
udp   UNCONN 0      0       127.0.0.54:53       0.0.0.0:*         users:(("systemd-resolve",pid=498,fd=16))
udp   UNCONN 0      0       127.0.0.53%lo:53    0.0.0.0:*         users:(("systemd-resolve",pid=498,fd=14))
tcp   LISTEN 0      4096    127.0.0.53%lo:53    0.0.0.0:*         users:(("systemd-resolve",pid=498,fd=15))
tcp   LISTEN 0      4096    0.0.0.0:22          0.0.0.0:*         users:(("sshd",pid=1021,fd=3),("systemd",pid=1,fd=86))
tcp   LISTEN 0      511     0.0.0.0:80          0.0.0.0:*         users:(("nginx",pid=1210,fd=5),("nginx",pid=1209,fd=5))
tcp   LISTEN 0      151     127.0.0.1:3306      0.0.0.0:*         users:(("mysqld",pid=1105,fd=23))
tcp   LISTEN 0      4096    [::]:22             [::]:*            users:(("sshd",pid=1021,fd=4),("systemd",pid=1,fd=87))

The most important column is Local Address, because it tells you who can reach the service:

Local addressReachable from
0.0.0.0:port or [::]:port or *:portEvery network interface, including the public one
127.0.0.1:port, 127.0.0.53%lo, [::1]:portOnly the server itself
A specific IP such as 10.0.0.5:portOnly that interface (for example a private network)

In the example above, SSH and Nginx are exposed to the internet, while MySQL and the local DNS resolver only accept local connections. On Ubuntu 24.04, port 22 also shows systemd as an owner because SSH uses socket activation through ssh.socket.

To look up a single port, filter by it:

sudo ss -tlpn 'sport = :80'

Step 2 - Using netstat and lsof

netstat belongs to the net-tools package, which is deprecated and not installed on Ubuntu 24.04 by default. If you prefer its output or have scripts that depend on it, install it:

sudo apt update
sudo apt install net-tools

The flags are the same as with ss:

sudo netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      1021/sshd: /usr/sbi
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      1209/nginx: master
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      1105/mysqld

lsof is useful when you want the full picture for one port, including the user the process runs as. It is installed by default:

sudo lsof -nP -i :80
COMMAND  PID     USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
nginx   1209     root    5u  IPv4  18833      0t0  TCP *:80 (LISTEN)
nginx   1210 www-data    5u  IPv4  18833      0t0  TCP *:80 (LISTEN)

Once you know the PID, find which systemd unit started it, so you know what to disable if the service should not be running:

systemctl status 1209

The first line of the output shows the unit name, for example nginx.service.

Step 3 - Scanning the server from outside with nmap

The list from Step 1 is what the server offers; the firewall decides what the internet can actually reach. Install nmap on the machine you will scan from (on Ubuntu or Debian):

sudo apt install nmap

Start with a default scan, which checks the 1,000 most common TCP ports:

sudo nmap your_server_ip
Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-09-25 10:12 UTC
Nmap scan report for your_server_ip
Host is up (0.021s latency).
Not shown: 997 filtered tcp ports (no-response)
PORT    STATE  SERVICE
22/tcp  open   ssh
80/tcp  open   http
443/tcp closed https

Nmap done: 1 IP address (1 host up) scanned in 6.43 seconds

Run with sudo, nmap uses a SYN scan (-sS), which is faster and more accurate than the unprivileged connect scan. Each port gets one of these states:

  • open: a service accepted the connection.
  • closed: the host answered, but nothing listens on that port. The firewall let the packet through.
  • filtered: no answer, usually because a firewall dropped the packet. This is what you want for every port you do not use.

A default scan skips most ports, so run a full TCP scan at least once. It takes a few minutes:

sudo nmap -p- your_server_ip

UDP is slower to scan because closed UDP ports often do not reply. Check the most common ones:

sudo nmap -sU --top-ports 50 your_server_ip

If you run IPv6, scan the IPv6 address too with -6. Firewall rules are often configured for IPv4 only by mistake:

sudo nmap -6 -p- your_server_ipv6

Step 4 - Identifying services and versions

Knowing that port 8080 is open is not enough; you need to know what answers on it. Service detection (-sV) connects to each open port and identifies the software and its version:

sudo nmap -sV -p 22,80 your_server_ip
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    nginx 1.24.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The Nmap Scripting Engine adds specific checks. Two that are useful on any web server:

nmap --script http-headers,http-methods -p 80 your_server_ip
nmap --script ssl-enum-ciphers -p 443 your_domain

The first shows the HTTP response headers and the allowed methods; the second lists the TLS versions and ciphers your server accepts, so you can spot old protocols such as TLS 1.0 that should be disabled.

Step 5 - Closing unneeded ports

Compare the output of Steps 1 and 3. For every open port, you should be able to say which service it belongs to and why it must be public. There are three ways to close a port, from best to worst:

  1. Stop the service if you do not need it at all:

    sudo systemctl disable --now service_name
    
  2. Bind the service to localhost or a private IP if only local applications use it. For example, MySQL uses bind-address = 127.0.0.1 in /etc/mysql/mysql.conf.d/mysqld.cnf, and Redis uses bind 127.0.0.1 -::1 in /etc/redis/redis.conf.

  3. Block it in the firewall if the service must listen publicly but only for some clients. With UFW, allow what you need and deny everything else by default:

    sudo ufw default deny incoming
    sudo ufw allow OpenSSH
    sudo ufw allow 80,443/tcp
    sudo ufw allow from 203.0.113.10 to any port 5432 proto tcp
    sudo ufw enable
    

    Replace 203.0.113.10 with the IP of the client that needs access. Always allow SSH before enabling UFW so you do not lock yourself out.

Check the result:

sudo ufw status verbose

Then repeat the external scan from Step 3. Ports you closed should now show as filtered.

Step 6 - Saving a baseline and detecting changes

A port audit is only useful if you repeat it. Save the result of a full scan in nmap's XML format:

sudo nmap -sV -p- -oX baseline.xml your_server_ip

After changes to the server, or on a regular schedule, run the same scan into a new file and compare the two with ndiff, which is packaged separately:

sudo apt install ndiff
sudo nmap -sV -p- -oX current.xml your_server_ip
ndiff baseline.xml current.xml
-Nmap 7.94SVN scan initiated Thu Sep 25 10:20:01 2026 as: nmap -sV -p- -oX baseline.xml your_server_ip
+Nmap 7.94SVN scan initiated Fri Oct 03 09:02:44 2026 as: nmap -sV -p- -oX current.xml your_server_ip

 your_server_ip:
-Not shown: 65533 filtered ports
+Not shown: 65532 filtered ports
 PORT     STATE SERVICE VERSION
+6379/tcp open  redis   Redis key-value store

Any line starting with + under PORT is a port that was not open before and needs an explanation. ndiff exits with status 1 when it finds differences, which makes it easy to use from a cron job that emails you only when something changes.

Troubleshooting

A port is listening in ss but nmap shows it as filtered. The firewall is blocking it, which is expected for services you only use locally. If the service should be public, add a UFW rule. Also check your provider's network firewall, if you configured one.

nmap shows a port as open that UFW should block. Docker publishes container ports by writing its own iptables rules, which are evaluated before UFW. A container started with -p 8080:80 is reachable from the internet regardless of UFW. Publish it on localhost only (-p 127.0.0.1:8080:80) and put a reverse proxy in front.

The Process column in ss or netstat is empty. You ran the command without sudo, so the tool cannot see processes owned by other users.

Scanning the server from itself shows ports as open that are filtered from outside. Traffic to your own public IP is routed through the loopback interface, which UFW accepts by default. Always scan from a separate machine.

The scan is very slow or hosts appear down. Some networks drop ICMP. Add -Pn to skip host discovery and treat the host as up.

Conclusion

You now know how to list listening sockets with ss, netstat and lsof, verify from outside with nmap what is really exposed, close what is not needed, and track changes with ndiff. Run the external scan after every deployment that adds a service. As next steps, harden the services that must stay public (SSH key authentication, Fail2ban) and put the baseline comparison on a weekly schedule.