A client handed me SSH access to a server they'd been running for two years.
No documentation. No handoff notes. No "here's what's running and why." Just a root password in a LastPass share and a Slack message that said "it hosts our web app, let us know if you need anything."
I didn't know what the web app was. I didn't know what was installed. I didn't know what ports were open to the internet, what services were running, or what this machine had been doing quietly for 730 days before I touched it.
This is not unusual. This is actually most of the "inherited server" situations I've been in. The person who set it up is gone, or was a contractor, or was the founder who also did DevOps because someone had to, and the documentation lived entirely inside one person's head and left with them.
The first thing I do on an unfamiliar server is not grep logs or check running services. It's figure out what the machine is actually reachable on from the outside. Not what anyone says it's supposed to be doing. What it is doing. What ports are in LISTEN state, what process owns each one, and whether that process is bound to localhost or to every network interface on the machine.
I ran netstat -an first, because that's what I knew at the time. I got back 200 lines of connection state. TIME_WAIT entries for connections that had already closed. ESTABLISHED entries for active sessions. CLOSE_WAIT entries from something that hadn't cleaned up properly. All of it technically useful for debugging specific connection issues. None of it useful for getting a fast security picture of what's actually listening for new connections.
Finding the ports in LISTEN state in netstat -an output feels like searching for a specific ingredient in a paragraph of text. It's all there, but you have to read every line.
Then someone showed me ss.
The Command
ss -tlnp
That's the whole thing.
-t — TCP only (use -u for UDP, or -tu for both)
-l — listening sockets only (filters out ESTABLISHED, TIME_WAIT, everything else)
-n — numeric output (don't resolve port numbers to service names, don't do reverse DNS)
-p — show the process name and PID holding each socket
Output on a typical web server:
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1234,fd=3))
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=5678,fd=6))
LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=5678,fd=7))
LISTEN 0 128 0.0.0.0:5432 0.0.0.0:* users:(("postgres",pid=9012,fd=5))
Port 22 — SSH. Expected.
Port 80, 443 — nginx. Expected.
Port 5432 — Postgres. Bound to 0.0.0.0.
That last one.
0.0.0.0:5432 means Postgres was accepting connections from any IP address on the internet. Not just localhost. Not just the application server. Any IP. Port 5432, wide open, reachable from anywhere.
It had a strong password. It had been running that way for two years without an incident anyone knew about. But "strong password" is not a substitute for "not exposed to the internet." A service that should only accept connections from localhost or from your application tier has no business being bound to 0.0.0.0.
One command. Three seconds. Caught a misconfiguration that had been sitting there since the server was provisioned.
What the Output Is Actually Telling You
The column that matters is Local Address. This is where the socket is bound — who it will accept connections from.
0.0.0.0:5432 → Externally reachable on all IPv4 interfaces
127.0.0.1:5432 → Localhost only — not reachable from outside
:::5432 → All IPv6 interfaces (equivalent to 0.0.0.0 for IPv6)
::1:5432 → IPv6 localhost only
If a service should only be accessed from the same machine — a database, an internal cache, a monitoring agent — it should be bound to 127.0.0.1. If you see it bound to 0.0.0.0 and you don't have an explicit reason for that, that's a conversation to have with whoever configured the service.
For the Postgres case, the fix is one line in postgresql.conf:
listen_addresses = 'localhost'
Restart Postgres, confirm the bind address changed with ss -tlnp again, done. That configuration change is a 30-second fix. The two years it had been wrong before someone checked is the part that should concern you.
Why ss Instead of netstat
ss replaced netstat on most Linux distributions when iproute2 superseded net-tools around 2016. On a fresh Ubuntu or Debian install, netstat isn't installed by default — it's in the net-tools package which isn't pulled in automatically anymore. That's why you've probably SSH'd into a server, typed netstat, and gotten command not found.
ss reads kernel socket tables directly instead of going through /proc/net/tcp. It's faster on machines with thousands of connections, and it's maintained. net-tools has been in maintenance-only mode for years. For new work, use ss.
That said, netstat -tlnp does the same thing if you have it installed. The flags are identical. If you're on an older system or have net-tools available, either works.
The Process Name Caveat
Run ss -tlnp without sudo and you'll see the ports and bind addresses, but the Process column will be empty for sockets owned by other users. You'll see the port, you won't see what's holding it.
sudo ss -tlnp
With sudo, you see everything — the socket, the process name, the PID, the file descriptor number. That's the version to run when you're doing a security audit on a machine you have root on.
The lsof Alternative
lsof -i -P -n | grep LISTEN gives you the same information from a different angle — process name first, port second.
nginx 5678 root 6u IPv4 12345 0t0 TCP *:80 (LISTEN)
nginx 5678 root 7u IPv4 12346 0t0 TCP *:443 (LISTEN)
sshd 1234 root 3u IPv4 12347 0t0 TCP *:22 (LISTEN)
postgres 9012 postgres 5u IPv4 12348 0t0 TCP *:5432 (LISTEN)
Useful when you know the service name and want to find its ports. Less useful when you want a port-first view of everything listening. I use ss for the initial audit and lsof when I'm tracking down a specific process.
What I Actually Do With Ports I Can't Identify
When I see a port in LISTEN state and the process name isn't immediately obvious — java, python3, something that isn't a clear service name — I do three things:
# 1. Find the full command that started the process
ps aux | grep <PID>
# 2. Check what package installed the binary
dpkg -S /path/to/binary # Debian/Ubuntu
rpm -qf /path/to/binary # RHEL/CentOS
# 3. Check what the process has open (files, connections, everything)
sudo lsof -p <PID>
Most of the time it's something benign — a monitoring agent, a language runtime for an app, a service the previous admin installed and forgot about. Occasionally it's something that shouldn't be there at all. You don't know until you check.
Building This Into Your Server Checklist
Every time I take over a server now, this is the third command I run. After uptime (how long has it been running) and df -h (is it about to run out of disk), it's sudo ss -tlnp (what is this machine telling the internet it's listening on).
Takes three seconds. Has caught real problems more than once. The Postgres 0.0.0.0 situation is the one I remember most clearly, but it's not the only one — I've found web apps listening on non-standard ports that were supposed to be internal, Redis instances bound to 0.0.0.0 on development machines that got promoted to production without anyone auditing the config, SSH running on a second non-standard port "for convenience" that had been left open after a migration.
None of those were catastrophic on their own. All of them were things the people running those servers didn't know were there.
Three seconds to find out. I don't know why I didn't run this on every server from the beginning.
Full script with UDP ports, both TCP and UDP in one pass, lsof cross-reference, and notes on what to do when you can't identify a process:
Top comments (0)