Auditing your network exposure with Nmap and ss
How to audit Linux network exposure the sane way: join local listener inventory from ss with remote reachability checks from Nmap instead of trusting only one view.
If you only run ss, you are inventorying.
If you only run nmap, you are guessing.
Real exposure auditing is the join between the two.
That is the main mistake in most "check open ports on Linux" guides. They answer one question and quietly pretend it was the important one. A public-host audit needs both:
- What is the kernel listening on?
- What can the network actually reach?
Those are not the same question.
You can have a daemon listening locally that the network can never reach. You can also have a service reachable from the network that does not show up the way you expect locally because a proxy, DNAT rule, or container runtime is involved. Auditing exposure means reconciling those stories.
Start local: inventory listeners and owners
On modern Linux, the tool you want is ss, not netstat.
netstat still matters for search intent and muscle memory, but ss is the current default because it understands modern socket state and gives you better process context.
The useful starting point is:
sudo ss -ltnup
That means:
-llistening sockets-tTCP-uUDP-nnumeric output-powning process
The -p flag is what turns a port list into an audit. A port without an owner is trivia. A port owned by a specific daemon is an action item.
If you expect only SSH and WireGuard, then "why is Java listening on 0.0.0.0:8080?" is exactly the right reaction.
You can narrow the view when you already know the interesting surfaces:
sudo ss -ltnup '( sport = :22 or sport = :443 or sport = :51820 )'
Two things matter immediately in local output:
- Wildcard bind versus loopback bind
- Expected service versus surprise service
127.0.0.1:8080 is a different story from 0.0.0.0:8080. One is local plumbing. The other may be public exposure, depending on firewall and routing.
This is also where the "expected listeners" idea helps. On a single-purpose public box, you should usually be able to say in one sentence what is supposed to be listening. If you cannot, your first problem is not Nmap. It is role sprawl.
That sentence is worth writing down somewhere. "This host should expose SSH and WireGuard only" is a much stronger audit baseline than "we'll know something weird when we see it."
Then scan from the outside
Now switch perspective.
From another host, run Nmap:
nmap -sS --open -p- vps.example.com
sudo nmap -sU --top-ports 100 --open vps.example.com
nmap -sV -p 22,80,443,51820 vps.example.com
The first command is the fast TCP inventory. The second checks the most common UDP ports. The third asks Nmap to identify what is actually speaking on the ports you care about.
This matters because the network view includes realities your local socket table does not:
- packet filters
- routing path
- cloud firewall rules
- DNAT
- mistakes in published container ports
That is why this article pairs well with /blog/nftables-vs-iptables-vs-ufw. Local listeners are only half the truth on a host with real firewall policy.
It is also why cloud dashboards can mislead you. A security group saying "22 and 443 only" does not prove the host is clean. It proves only that one layer of intent exists somewhere above the machine.
Do not lie to yourself about Nmap states
Nmap's port-state model is one of the most useful pieces of operator documentation on the Internet because it explains what scanners are actually telling you.
The states are scanner results, not metaphysical truths:
openclosedfilteredunfilteredopen|filteredclosed|filtered
The important translations are:
open: something is actively accepting traffic there.closed: the port is reachable, but nothing is listening.filtered: some packet filter prevented Nmap from telling which it is.open|filtered: common with UDP; ambiguity is part of the result.
That last point matters because UDP audits make people overconfident. An ambiguous UDP result is not a bug in Nmap. It is the reality of stateless protocols plus filtering.
This is one reason UDP exposure should be deliberate and sparse. The less surprise UDP you have, the less guesswork you need to tolerate in the results.
Join the two views
This is the shortest sane audit workflow I know:
1. Inventory listeners locally
sudo ss -ltnup
2. Scan remotely from another host
nmap -sS --open -p- vps.example.com
3. Correlate anything unexpected
sudo ss -ltnup | grep ':443'
nmap -Pn -p 443 vps.example.com
If Nmap says 443 is open and ss says nothing is listening there, you probably have:
- port forwarding
- container publishing
- a proxy layer you forgot about
- the wrong target IP
If ss says something is listening and Nmap says filtered, your local service exists but the network cannot reach it. That might be exactly what you intended.
That sentence is important. "Filtered" is not automatically a problem. If the port is intentionally private, filtered is success.
The audit job is not to maximize open ports or minimize filtered ports. It is to make reality match intent.
That sounds simple, but it is the sentence most exposure audits quietly forget.
Intent is the standard. Everything else is detail.
Use version detection when the port number is not enough
Open is not the end of the question.
Nmap's version detection with -sV tries to identify protocol, app name, version, and related metadata from open ports. That is the right second pass once you know which ports are reachable.
It also helps catch wrong assumptions. A port you labeled "HTTPS" may in fact be a control panel, an outdated proxy, or a service speaking something entirely different.
And for a lot of real incidents, that is the whole value of the scan: not discovering a secret port, but discovering that an expected port is serving the wrong thing.
This matters even more for crypto cleanup. If SSH is open, follow up with:
nmap --script ssh2-enum-algos -p 22 vps.example.com
That script is the perfect bridge into /blog/removing-weak-crypto-algorithms, because now you are not just asking whether the service is reachable. You are asking what it actually offers.
What to do after every firewall or routing change
Exposure auditing is not a one-time ceremony. Run it:
- after firewall changes
- after Docker or reverse-proxy changes
- after VPN or routing changes
- after adding a new service
This is where /blog/linux-sysctl-network-hardening and exposure auditing meet. Hardening changes are only real if the network view still matches your intended surface.
If you want the shortest practical workflow, make it habitual:
- inventory with
ss - scan remotely with Nmap
- investigate every surprise
- repeat after every network-facing change
That is enough to catch most embarrassing exposure mistakes before someone else catches them for you.
And if a host keeps producing surprises, treat that as a design smell. Exposure auditing works best on machines with clear roles and boring network paths.
My opinion here is simple and worth repeating:
If you only run ss, you are inventorying. If you only run nmap, you are guessing.
The audit you actually want is the difference between what the kernel is willing to serve and what the outside world can touch.