Kyber Cypher

Learn

Field Log 023

Your own service was handing out your keys

Field Log // 023 Status Live Difficulty Free Cost Nothing
The Story

I wrote a small helper service to read files for me. Convenience software, a hundred lines, the kind of thing you build on a Tuesday and forget. Months later I pointed another machine at it, out of idle curiosity, and asked it for a file it had no business handing over. It handed it over.

No password. No token. No question. Any device on the network could ask, and it would answer.

The uncomfortable part is not that there was a hole. Holes are ordinary. The uncomfortable part is that I had audited my firewall, my router and my remote access, and never once audited the thing I wrote myself, because I trusted the author. I was the author. Trust was doing no work at all.

You check locks on doors other people installed. The door you hung yourself gets a glance, because you remember hanging it. That memory is not an inspection.

There is a second assumption underneath it, and it is the one that actually matters: I had treated the local network as a safe room. Everything inside is mine, so everything inside is fine. But a home network is not one trusted space. It holds a television that phones home, a thermostat running firmware from 2019, a guest phone, a printer nobody has updated since it came out of the box. Any of those is a foothold, and from a foothold your helpful little service is just a helpful little service.

So the lesson is two lessons welded together. Software you wrote is still an exposed surface. And the network it sits on is not a boundary, it is a room full of strangers you happen to own.

The fix took fifteen minutes. Finding it took months, because I was never looking. That ratio is the whole point of this log.

The Build

How to inventory what your own machines expose, test each service the way an unwelcome visitor would, and close what should never have been open. Steps one and two change nothing. Nothing here is about attacking anyone else's systems; it is about auditing your own.

1. List every port your machine is actually listening on

Start with the truth, not your memory of it. Ask the operating system what is listening and on which address. The address matters more than the port.

# Linux and macOS: every listening socket, with the owning process
ss -tulpn
# older systems
netstat -tulpn

# Windows, in PowerShell
Get-NetTCPConnection -State Listen | Select-Object LocalAddress,LocalPort,OwningProcess

Read the local address column carefully. An address of 127.0.0.1 means only this machine can reach it. An address of 0.0.0.0 or :: means anything that can route to this machine can reach it. That single column is the difference between private and published.

2. Ask each service for something sensitive, from a different device

This is the step people skip, and it is the only one that tells the truth. Testing from the same machine proves nothing, because loopback always works. Pick up a second device on the same network and make the request from there with no credentials at all.

# from ANOTHER machine on your network, not the one running the service
curl -i http://<the-machine>:<port>/

# then try whatever your service treats as privileged
curl -i http://<the-machine>:<port>/<a-path-your-service-exposes>

You are looking for one thing: a 200 and real content where you expected a refusal. If you get data without authenticating, you have found what I found. Write down the service name and move on; fix them all in one pass rather than interrupting the inventory.

3. Decide, per service, which of three things it should be

Almost everything belongs in the first category, and almost nothing belongs in the third.

  1. Local only. Bind it to loopback. Nothing off this machine can reach it, and no authentication is needed because there is no one to authenticate.
  2. Reachable but authenticated. It needs to answer other devices, so it needs to ask who is calling.
  3. Open. Genuinely public, genuinely harmless if strangers read it. Be honest about how rare this is.

4. Bind to loopback, which is the strongest and cheapest fix

Most convenience services only ever talk to the machine they run on. Changing the bind address is usually one line and removes the entire problem rather than guarding it.

# bind to loopback instead of every interface
# before:  listen on 0.0.0.0, port P    (anything on the network)
# after:   listen on 127.0.0.1, port P  (this machine only)

# then prove it from the other device again. You want a refused connection.
curl -i --max-time 5 http://<the-machine>:<port>/

A connection refused or a timeout from the second device is the result you want. If it still answers, something else is forwarding to it, and a reverse proxy or container port mapping is the usual culprit.

5. If it must be reachable, make it demand a secret

Keep this boring. A single long shared token checked on every request stops casual access entirely, and casual access is what you are actually facing on a home network.

# generate something you would never think of yourself
openssl rand -hex 32

# keep it out of the code and out of your shell history file
# put it in an environment file readable only by you
chmod 600 <your-secret-file>

Reject the request before doing any work, compare the token in a way that does not leak its length, and never log the token itself. If you log requests, log that authentication failed, not what was attempted.

6. Narrow what the service can even see

Authentication decides who may ask. It does not decide what can be served. A file reader that will happily open anything is one typo away from serving something it should not, so constrain the scope as well.

Resolve every requested path to its real location, confirm it sits inside the one directory you intended, and refuse everything else. The pattern for that, and the tests that prove it works, are the subject of the next log, because a guard written after the fact is a guess.

7. Put it on a calendar, because you will not notice on your own

The reason this sat open for months is that nothing ever reminded me to look. An inventory you run once is a snapshot, and your machine keeps changing after the photograph.

# keep the inventory next to the machine it describes
# exposure.md
#   port P  -> loopback only   (helper service, no auth needed)
#   port Q  -> network, token required, last checked 2026-09
# re-run steps 1 and 2 every quarter and after adding anything new

The honest catch: this finds what is listening now, on the machines you remembered to check. It will not find the service on the box you forgot you still have running. Start the list anyway. A list with gaps beats a memory with none.

Related: prove nothing is using it before you shut it off is the discipline that keeps this cleanup from breaking something days later.