How to check open ports on a Linux server — from inside and outside
ss -tulpn shows what's listening; nmap from another machine shows what the internet sees; and Docker-published ports bypass your firewall rules. The audit, plus what an agent box should show.
Two views, and you need both. From inside: ss -tulpn lists every listening socket with the process that owns it. Anything bound to 0.0.0.0 or [::] is offered to every network the machine touches, while 127.0.0.1 is local-only. From outside: nmap from a different machine shows what the internet actually reaches once firewalls have their say. Run both, because they disagree in exactly the cases that hurt, most famously Docker, whose published ports bypass ordinary firewall rules. On a healthy agent server the outside view is one line long: SSH.
The inside view: what’s listening
sudo ss -tulpn
Flags: TCP and UDP listeners, numeric ports, owning process. Read the Local Address column with one rule: the address is who may connect. 127.0.0.1:18789 means loopback-only: reachable from the machine itself and nothing else. 0.0.0.0:22 means every interface. Seeing a service you don’t recognize is the moment to chase the process name in the last column before anything else. (Older guides use netstat -tulpn, same idea; ss is its modern replacement and ships everywhere.)
The outside view: what actually answers
nmap -sT -p 1-65535 <your-server-ip> --open
Run this from your laptop or another server, not from the box you’re testing: the point is to see through the same door strangers use. Firewalls (ufw, nftables, a cloud provider’s firewall) sit between the listener and the world, so the outside list is usually shorter than the inside one. When it’s longer than you expected, you’ve found the day’s work. Every port in the outside view is a surface strangers can probe, and probe they will, thousands of times a week, as your SSH logs already show.
The Docker exception everyone learns the hard way
Published container ports (-p 18789:18789 or a Compose ports: list without an IP prefix) bind to 0.0.0.0 and route through Docker’s own forwarding chains, not your host’s INPUT rules. A ufw “deny” you swear you added can be silently bypassed. The fixes, in order of preference: publish to loopback explicitly ( "127.0.0.1:18789:18789" in the ports list), or enforce rules in the DOCKER-USER chain, which Docker respects. Then re-run the outside nmap: it’s the only view that settles the argument.
If the server runs an AI agent
This audit is the operational half of the never-expose-your-Gateway rule. OpenClaw’s Gateway listens on port 18789 and defaults to loopback; it will even refuse to start bound wider without authentication configured. But note the compose caveat: the official Docker setup publishes the port and sets the container’s bind to lan so the host can reach it, which on an unfirewalled VPS means the auth token is what stands between the Gateway and the internet. Exactly the case the outside scan catches. OpenClaw also ships its own checks: openclaw security audit --deep (covered here) flags remote binds without auth as critical, and openclaw gateway status shows what the gateway thinks it’s bound to.
What good looks like
For a single-purpose agent box: outside view shows SSH and nothing else; inside view shows SSH on 0.0.0.0 (keys-only), the Gateway on loopback, and any remote access flowing through an outbound tunnel or mesh (both patterns here) rather than an opened port. Five minutes, quarterly, and after any Docker or config change. The scan is cheap; the surprise it prevents is not.