Find out which ports your server has open (and close the ones you don't need)

Every open port is a door that anyone on the internet can knock on. A PHP web server needs three. This guide shows how to list them all, check them from outside, and close the rest.

Only scan your own servers. Scanning machines you don't own or manage can break your hosting provider's rules, and in some countries the law.

Before you start

  • You need a user with sudo on the server (see our sudo guide), and a second machine for step 4, such as your own computer.
  • The examples use Ubuntu 24.04 and Debian 13 with Nginx, PHP-FPM, MariaDB and Redis. The server's public address is 203.0.113.5. Use yours.

1. List everything that listens

sudo ss -tulpn

The letters mean: t TCP, u UDP, l only ports that listen, p show the program, n show numbers instead of names. Use sudo, or the program column stays empty.

You get one line per port. A trimmed example from our test server:

Netid State  Local Address:Port  Process
tcp   LISTEN       0.0.0.0:22    users:(("sshd",pid=3480,fd=6))
tcp   LISTEN       0.0.0.0:80    users:(("nginx",pid=2914,fd=5),...)
tcp   LISTEN     127.0.0.1:3306  users:(("mariadbd",pid=8537,fd=24))
tcp   LISTEN     127.0.0.1:6379  users:(("redis-server",pid=8260,fd=8))
tcp   LISTEN         [::1]:6379  users:(("redis-server",pid=8260,fd=9))
tcp   LISTEN          [::]:22    users:(("sshd",pid=3480,fd=7))
tcp   LISTEN          [::]:80    users:(("nginx",pid=2914,fd=6),...)

The column that matters is Local Address:Port. The number after the last colon is the port. The part before it says who can connect.

2. Read the address

You seeIt means
127.0.0.1:3306 or [::1]:6379Only programs on the server itself can connect. Good for databases and caches.
0.0.0.0:80, [::]:80 or *:80Every network, including the internet (IPv4, IPv6, or both). Only the firewall stands in the way.
A public address, such as 203.0.113.5:80Open on that one address. Treat it like the line above.

To hide the local-only lines and see just what's open to the network:

sudo ss -tulpn | grep -vE '127\.0\.0\.|\[::1\]'

Lines with 127.0.0.53 or 127.0.0.54 port 53 belong to systemd-resolved, which looks up domain names for the server. They're local, and fine.

3. Know what should be open

A typical server for PHP websites needs only these open to the internet:

  • 22 for SSH (or the port you moved SSH to)
  • 80 for HTTP, which redirects to HTTPS and is used to renew Let's Encrypt certificates
  • 443 for HTTPS

These should never be open to everyone:

PortWhat it isWhy it matters
3306MySQL or MariaDBBots try passwords on it all day.
5432PostgreSQLThe same, for PostgreSQL.
6379RedisOften has no password. Anyone can read or wipe your data.
11211MemcachedHas no password at all.
9000PHP-FPMWhoever reaches it can make PHP run code on your server.
8080, 8443, 8888 and similarAdmin panels, test servers, dashboardsOften left with default passwords or no login.

On Ubuntu 24.04, port 22 shows systemd as the program, not sshd. That's normal: systemd holds the port and starts SSH when someone connects. systemctl status ssh.socket shows it.

4. Find out what a port belongs to

Take the pid number from the ss line and ask systemd which service it is:

systemctl status 10891

The first line names the service, for example postgresql@17-main.service. Now you know what to stop or change.

5. Check from outside with nmap

ss shows what listens. It doesn't show what the internet can actually reach, because a firewall may block some of it. To see that, scan the server from another machine. Install nmap there (sudo apt install nmap on Ubuntu or Debian; there are downloads for Windows and macOS on nmap.org):

sudo nmap -p- 203.0.113.5

Don't leave out -p-. It scans all 65,535 ports. Without it, nmap only tries the 1,000 most common ones. In our test, a plain nmap missed both Redis (6379) and Memcached (11211), even though both were wide open. It works without sudo too, just a little slower.

Each port gets a state:

  • open: something answers. You should only see 22, 80 and 443.
  • closed: nothing listens there. Not a risk. If you have a firewall, it lets this port through.
  • filtered: the firewall blocks it. This is what you want for everything else.

If the server has an IPv6 address, scan that too, with nmap -6 -p- 2001:db8::5. Your own computer needs IPv6 for this to work.

6. Stop services you don't use

The best closed port is one where nothing runs. Maybe a package pulled in a service you never needed, or you tried something once. Stop it now and keep it off after a restart:

sudo systemctl disable --now memcached

If you'll never need it again, remove the package: sudo apt purge memcached. For PostgreSQL, sudo systemctl disable --now postgresql stops every database cluster on the server. Run sudo ss -tulpn again to check the port is gone.

7. Make the rest listen on the server only

Databases and caches are used by your websites on the same server, so they don't need the network at all. On Ubuntu and Debian, most are local by default. If ss shows one on 0.0.0.0, [::] or *, someone changed it. Change it back:

  • MySQL or MariaDB: bind-address = 127.0.0.1. Full steps in our MySQL and MariaDB guide.
  • Redis: bind 127.0.0.1 -::1. Full steps in our Redis guide.
  • PostgreSQL: in /etc/postgresql/17/main/postgresql.conf (your version number), listen_addresses = 'localhost', then sudo systemctl restart postgresql.
  • Memcached: in /etc/memcached.conf, the lines -l 127.0.0.1 and -l ::1, then sudo systemctl restart memcached.
  • PHP-FPM: the default is a socket file, such as listen = /run/php/php8.4-fpm.sock, which uses no port at all. If a pool in /etc/php/8.4/fpm/pool.d/ says listen = 9000, that means every network. Use the socket, or listen = 127.0.0.1:9000. Check with sudo php-fpm8.4 -t, then restart php8.4-fpm.
  • An admin panel in Nginx on 8080: listen 127.0.0.1:8080;

Restart Nginx after changing a listen line. Don't just reload. In our test, systemctl reload nginx said nothing, but Nginx couldn't take the new address and kept the old one open to everyone. The error was only in /var/log/nginx/error.log. After a change, always run sudo ss -tulpn again.

To use a panel that now listens on the server only, open an SSH tunnel from your computer:

ssh -L 8080:127.0.0.1:8080 you@203.0.113.5

Leave it open and browse to http://127.0.0.1:8080 on your own computer. The panel works for you and stays closed to everyone else.

8. Add the firewall as a second lock

A firewall catches what you missed: a service that comes back after an update, or a new one somebody installs. Set it to block everything coming in except SSH, HTTP and HTTPS. Our UFW firewall guide walks you through it. Then run the nmap scan from step 5 again: every other port should now say filtered.

Don't rely on the firewall alone. If it's ever turned off, or a rule is too wide, everything that listens on every network is open again. Steps 6 and 7 are the real fix.

9. Watch out for Docker

If you run containers with Docker, this one catches a lot of people. Ports that Docker publishes skip UFW. Docker writes its own firewall rules, and traffic for containers never reaches UFW's rules. In our test, UFW was blocking everything except 22, 80 and 443, and ufw status showed nothing else. But a container started with -p 8081:80 was still open to the outside.

In ss, these ports show up as docker-proxy. The fix is to publish only to the server itself, and put Nginx in front if the internet needs to reach it:

docker run -d -p 127.0.0.1:8081:80 nginx

In a compose.yaml file, write it the same way: "127.0.0.1:8081:80" under ports:. Databases in containers often don't need a published port at all, because other containers on the same Docker network reach them directly. Docker explains the details in its firewall documentation.

If something goes wrong

  • The program column in ss is empty: run it with sudo.
  • A port is still there after you changed the setting: the service wasn't restarted, or the setting is in a second file. Search for it, for example sudo grep -rn bind-address /etc/mysql/. For Nginx, check /var/log/nginx/error.log and restart.
  • The website shows "502 Bad Gateway" after changing PHP-FPM: Nginx's fastcgi_pass must point at the same socket or address that the pool listen line uses.
  • nmap says "Host seems down": the server doesn't answer ping. Add -Pn to scan anyway.
  • A port is open in nmap even though UFW blocks it: it's probably a Docker container (step 9). docker ps lists the published ports.

Official sources

Next steps: use SSH keys and turn off password login for the one port that must stay open, and work through the security checklist for a new PHP server.

ServerNest is being built with an agent that only makes outgoing connections, so it won't add an open port to your server. Join early access to try it first.

← Back to the security blog