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
sudoprivileges. - 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_ipin this guide.
WarningOnly scan systems that you own or have written permission to test. Port scanning third-party networks can breach your provider's acceptable use policy and, in some countries, the law.
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 address | Reachable from |
|---|---|
0.0.0.0:port or [::]:port or *:port | Every network interface, including the public one |
127.0.0.1:port, 127.0.0.53%lo, [::1]:port | Only the server itself |
A specific IP such as 10.0.0.5:port | Only 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:
-
Stop the service if you do not need it at all:
sudo systemctl disable --now service_name -
Bind the service to localhost or a private IP if only local applications use it. For example, MySQL uses
bind-address = 127.0.0.1in/etc/mysql/mysql.conf.d/mysqld.cnf, and Redis usesbind 127.0.0.1 -::1in/etc/redis/redis.conf. -
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 enableReplace
203.0.113.10with 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.
