HostAtlas / Tools / Portwatch
v1.0.0 Open source · MIT · cron-friendly · no daemon

Open ports change. Make sure you notice.

Scans the listening sockets on a host, diffs against the last run, alerts when something opens that shouldn’t. Useful for the boxes you don’t have full HostAtlas on; useful for the audit story when you do.

$ curl -fsSL https://tools.hostatlas.app/install.sh | sh -s portwatch copy

Source on GitHub. Drop into cron or a systemd timer; no long-running daemon.

MIT
licensed
cron
friendly · no daemon
v4+v6
both bind families diffed
portwatch · keywest · daily run · 03:12 UTC
$ portwatch run --config /etc/portwatch.yml
→ scanning host netns + 8 container netns…

PORT SERVICE BIND STATUS
22 sshd 0.0.0.0 unchanged
80 nginx 0.0.0.0 unchanged
443 nginx 0.0.0.0 unchanged
5432 postgres 0.0.0.0 NEW · was filtered
6379 redis 0.0.0.0 CHANGED · was 127.0.0.1
9200 elasticsearch 127.0.0.1 unchanged

→ 2 diffs · 1 CRIT · 1 WARN
→ alert sent · slack #infra-sec, webhook https://audit.acme.app/portwatch
→ snapshot saved · /var/lib/portwatch/2026-05-15.json _
— Why local port-diff

A new open port is a new attack surface.

External scanners see the perimeter. Portwatch sees the host. Both views matter — and the inside view catches changes the outside view never will.

changes are accidents
A misconfigured docker-compose, a bind that should have stayed on 127.0.0.1, a test setup left running. Most new open ports are accidents — catch them before someone else does.
compliance asks
Auditors ask for "show me port changes per host, per month." Portwatch produces the evidence trail: per-host, per-day, signed snapshots.
internal binds are missed
External scanners see what an attacker sees — they miss the internal binds. A host scanning itself catches the 0.0.0.0:6379 mistake the outside scanner can't reach.
— How a run works

Six steps, every invocation.

local scan
ss -tlnp + a netns walk. No external network calls; nothing leaks while scanning.
PID + service attribution
Each listening socket gets mapped to its process and service. Not "5432 open" — "5432 open, postgres, just-now".
diff against last run
New / removed / changed-bind / changed-process. Severity per category, configurable.
alert on diff
Slack, email, plain webhook. Quiet when nothing changes; loud when something does.
JSON snapshot history
Last N runs kept locally for audit. Snapshots can be uploaded to S3 / object store if you want a remote trail.
per-port allow-list
Expected ports stay silent. Unexpected ones notify. The allow-list is a YAML file you check into git, reviewed like any other config.
— Install + schedule

Drop it in. Run it on a timer.

Single binary. Daily / hourly / on-demand — whatever your fleet rhythm is. No daemon, no port of its own.

Install

Single binary, signed. Drops a sample YAML config to /etc/portwatch.yml on first run.

$curl -fsSL https://tools.hostatlas.app/install.sh | sh -s portwatch

Schedule

Run from cron or a systemd timer. Daily at 03:12 is a good default; hourly when you’re testing.

cron12 3 * * * portwatch run --config /etc/portwatch.yml
— Honest answers

Common questions.

Does it scan remotely?

No — local-only. For external scanning use a dedicated network-scanner role. Portwatch sees the host from the inside, which is the angle attackers don't have.

IPv6?

Both v4 and v6. Each bind family is diffed separately so a v6-only regression doesn't hide under a v4 unchanged-state.

Containers?

Walks Docker / Podman netns when run as root. Without root, only the host netns. Per-container binds attributed to the container ID + name.

License?

MIT. Use commercially, modify, redistribute, fork.

— Works with

Connected capabilities.

Know what’s listening, always.

MIT-licensed. Cron-friendly, single binary, no daemon.

View on GitHub Get early access