ports-checker — Detect unexpected externally reachable TCP ports

PORTS-CHECKER ASDF System Details

ports-checker verifies that no unexpected TCP ports have become reachable after a server configuration change or software installation. It does not scan a range of ports. Instead, it:

  1. connects to the specified server over SSH;

  2. runs ss -H -lnt to obtain the TCP sockets listening on wildcard and non-loopback interfaces;

  3. closes the SSH connection;

  4. attempts to connect from the local machine only to the discovered ports;

  5. exits with code 1 if a reachable port is not on the allowlist.

A service blocked by an external firewall is therefore not considered reachable, even if its process listens on a non-loopback interface. Startup and SSH errors cause the command to exit with code 2.

Why Not Nmap?

ports-checker complements general-purpose port scanners such as nmap with a narrower, policy-oriented check:

  1. It probes only ports that can actually be open. Instead of scanning a range, it first obtains the listening TCP ports from the server over SSH and then tests only those ports from the outside.

  2. It avoids broad scan-like network activity. In some environments, endpoint protection or network security tooling may classify an nmap run as a port-scanning or attack attempt. ports-checker performs ordinary SSH access followed by targeted connection attempts, which is less likely to trigger rules intended specifically for broad port scans. These checks are still normal network activity and may remain visible in security logs.

  3. It checks policy, not just reachability. nmap reports scan results; ports-checker compares reachable ports with an explicit allowlist and returns a non-zero exit code when it finds an unauthorized port. This makes it straightforward to use in automated checks, deployment verification, and CI jobs.

Requirements

The SSH host key must already be present in known_hosts. The command intentionally does not accept unknown host keys automatically.

Installation

Install the command directly from GitHub with Roswell:

ros install 40ants/ports-checker
ports-checker --allow 22,80,443 example.com

To run the command from a local checkout, install and use the Qlot dependencies:

qlot install
chmod +x roswell/ports-checker.ros
qlot exec ./roswell/ports-checker.ros --allow 22,80,443 example.com

Usage

The --allow (-a) option accepts a comma-separated list of ports. The SSH user and SSH port can be specified separately:

qlot exec ./roswell/ports-checker.ros --ssh-user deploy --ssh-port 2222 \
  --allow 2222,443 server.example.com

Set the external TCP probe timeout with --timeout and the SSH connection timeout with --ssh-timeout. Both values are specified in seconds.

Exit Codes

Development

Project dependencies are pinned with Qlot. Tests use Rove:

qlot install
qlot exec ros -Q -e '(asdf:load-asd (truename "ports-checker.asd"))' \
  -e '(asdf:test-system "ports-checker")' -q

The network-logic tests do not require a real SSH server and do not open actual network connections.

Install the project-local documentation builder once, then regenerate README.md, ChangeLog.md, and the HTML site after changing documentation or public API docstrings:

qlot exec ros install 40ants/docs-builder
CL_SOURCE_REGISTRY=$(pwd)/ .qlot/bin/build-docs ports-checker-docs

Initial Release Limitations

API

PORTS-CHECKER/CHECKER

Functions

function
host discovered-ports allowed-ports &key (timeout 3) (probe-function #'probe-port)

Return unexpected reachable ports among DISCOVERED-PORTS on HOST.

ALLOWED-PORTS are never probed. PROBE-FUNCTION accepts HOST and PORT plus a TIMEOUT keyword, which makes the network boundary replaceable in tests.

Return true when a TCP connection to HOST and PORT succeeds within TIMEOUT.

PORTS-CHECKER/REMOTE

Classes

REMOTE-COMMAND-ERROR

An error reported when remote listener discovery over SSH fails.

Readers

Functions

function
destination &key ssh-user ssh-port (connect-timeout 10)

Return externally bound TCP ports reported by DESTINATION over SSH.

SSH-USER and SSH-PORT select the SSH account and port. CONNECT-TIMEOUT is passed to OpenSSH. Authentication is deliberately non-interactive.