I run one always-on Linux box in my basement. It serves media, filters DNS for the whole house, hosts a workflow automator, and runs a handful of scheduled agents. It has been up for two days straight at the time of writing, and before today I could not have told you, accurately, every port it was listening on.

That is the part that bothered me. Not that ports were open — a home server is supposed to have open ports — but that my picture of it lived in my head and in scattered shell history. On a box I only touch over SSH, “I think that’s the media server” is not an inventory.

So I made the machine tell me, on a timer, and write it down.

What I actually did

I wrote a shell script and put it on an hourly systemd timer. Every run it dumps the same set of facts, then diffs its own findings into a one-line summary: failures, warnings, everything else. No model calls, no API keys, nothing that can fail because a vendor had a bad afternoon. The only outbound request it makes is a single HTTPS fetch, which exists to prove the clock is sane and the trust store works — if TLS validates, the system time is almost certainly not wildly wrong.

It logs a full report each hour and keeps a latest.txt symlink pointing at the newest one, so reading the current state of the machine is one cat.

Here is what that report said about exposure, verbatim — listeners bound to all interfaces:

0.0.0.0:22      0.0.0.0:2222    0.0.0.0:5055
0.0.0.0:53      0.0.0.0:5678    0.0.0.0:6881
0.0.0.0:8081    0.0.0.0:8082    0.0.0.0:8096
0.0.0.0:9117    0.0.0.0:9696
[::]:22         [::]:5055       [::]:53
[::]:6881       [::]:8081       [::]:8082
[::]:9117       [::]:9696

Eleven services, on both stacks. Which is either completely fine or a disaster, depending entirely on one question: who can reach them.

The question that actually matters

“Listening on 0.0.0.0” does not mean “on the internet”. It means “on every interface this host has”. On a home network that is: the LAN, the tailnet, and any container bridge. It does not include the public internet unless something upstream forwards traffic in.

So I checked the upstream. No port forwards on the router for any of these, and nothing published through a tunnel. The public internet can reach exactly nothing on this box. What can reach it:

  • anything on the home LAN, including devices I don’t administer
  • anything on my tailnet, which is me, on purpose
  • other containers, if I have misjudged a network

That reframes the list. A DNS resolver open to the LAN is a feature — that is the whole point of running it. A torrent client’s web UI cached at the LAN is a convenience with a password I should care about. A workflow automator’s admin console on the LAN is fine until a guest is on the Wi-Fi.

None of these are emergencies. All of them are things I should be able to name from memory, and now I can, because the machine prints them at me every hour.

The one warning

The report’s only warning is a missing mount. A 1TB data drive is not seated in the case yet, so a storage mount is absent — which code that assumes it exists will discover in the most creative way possible. I’d rather read “NOT mounted” in a report at 4pm than debug a failed job at 4am. The warning stays until the drive is in.

Zero failed units, DNS resolving, Pi-hole answering, TLS validating, Tailscale up, clock synced.

Why this and not a dashboard

A dashboard is something you look at when you remember to look. It went down with the rest of the stack in the scenarios that matter, and it never tells you what changed.

What earned its place on this box instead:

  • It runs whether or not I am at a computer, with no dependency on a browser, a phone app, or a vendor’s API
  • It writes plain text I can read over SSH at 3am on the worst connection imaginable
  • It has a summary line, so “did anything change” is a one-second question and not a review task
  • Hourly reports make a baseline obvious. The second hour is when the diff starts carrying information

The honest limitation: this tells me what is listening, not whether a service is healthy. It would happily report a media server that answers its port and serves nothing. Every number in it needs a companion check that does something real, and mine is thinner than I’d like.

If you run a box you only touch over SSH, the cheap version of this is one shell script and one timer. The port list is the part that surprised me most — not because it was dangerous, but because I had been carrying an approximate version of it in my head for months and calling that knowledge.