Skip to main content
abel deloo

Status Publisher

A C# service that tells this site whether the home server is up, without opening a single inbound port.

Home server

status-publisher reads uptime and container counts

HTTPS PATCH · every 15 min

GitHub Gist

one public JSON file, six fields

HTTPS GET · every 60 s

This site

reads the file, shows the last known values

Data flows one way: out of the server, never in. Connections the publisher accepts: none.

The problem

The portfolio claims the home server keeps running. That claim is either backed by live data or it isn't. The obvious approach, an endpoint on the server that the site calls, requires a public IP, port-forwarding on the router, and a service on the home network reachable from the internet. That is a larger attack surface than the information is worth.

The design

The publisher runs as a Docker container on the server. Every 15 minutes it reads two things locally: uptime from /proc/uptime and container counts from Docker. It then makes one outbound HTTPS call to the GitHub Gist API and overwrites a single JSON file.

The portfolio fetches that file. The data goes in one direction only: server → GitHub → site. The publisher never accepts a connection; it only makes them.

What is deliberately not published

The JSON contains six fields: a timestamp, uptime in days, how many containers are running, how many exist in total, and two optional incident dates. That is all.

No hostnames. No IP addresses. No container names. No file paths. No service versions. An attacker reading the snapshot learns that some number of containers are running on some machine somewhere. They learn nothing that helps them find or reach that machine. The sanitization is not cosmetic: it is the security model.

Docker access without the keys to the server

Counting containers needs the Docker API. The usual shortcut is mounting the Docker socket read-only. That :ro protects the socket file, not what you can do through it: any process holding the socket can start a privileged container, which is root on the host.

So the publisher never sees the socket. It talks to a small proxy on an internal network with no route to the internet. The proxy allows exactly one request, GET /containers/json, and only from the publisher. I tested that list instead of trusting it: the container list returns 200; inspecting a container (which would expose other services' environment variables, and with them their passwords), reading logs, listing images and creating containers all return 403 or 405, and so does the allowed request from any other container.

Why push, not pull

The server already serves a few things to the internet through a reverse proxy with TLS, each one a deliberate choice, kept patched and behind authentication. A status endpoint would be one more of those, for data that does not need it.

Push adds nothing to that surface. The publisher makes one outbound HTTPS call to a known host and accepts no connections at all.

If the publisher crashes or the server goes offline, the portfolio displays the last known snapshot with a 'last updated' timestamp. A stale widget, not a broken one.

What I would do differently

The GitHub token has gist scope only, which limits blast radius if it leaks. What it does not do is alert me if it does leak. I would add token rotation on a schedule and a check that the published content has not been tampered with: a hash of the expected shape signed with a key that never leaves the server.

Stack

  • C#
  • .NET 9
  • Docker
  • socket-proxy
  • GitHub Gist API