← projects

Sentinel

Uptime and security monitoring for my deployed sites

GoGitHub Actionsntfy

Sentinel watches my deployed sites from the outside — Parliament and this portfolio — and pings my phone when something changes. It checks that each site is up and fast, that its TLS certificate isn’t about to expire, whether its TLS handshake is post-quantum, that it sends the security headers browsers rely on, and that files like .env or .git/HEAD aren’t publicly readable.

I wrote it to learn Go. It uses only the standard library, runs as a CLI, a local dashboard, or on a GitHub Actions schedule, and ships as a single binary.

Live Right Now

Published by Sentinel's scheduled run on GitHub Actions, every few hours.

Checking…

Check Your Own Site

A public version of Sentinel’s checks runs on Google Cloud Run. Enter any domain and it grades the site A–F on HTTPS, certificate health, post-quantum key exchange, security headers, and version-leaking headers, with a tip for each fix. Try it →

Safe to leave open to the internet

  • —Passive only: it reads what the site sends to any visitor, and never probes someone else's server for files like .env
  • —SSRF-proof: every connection is checked at connect time, so a domain that resolves to a private, loopback, or cloud-metadata address is refused, even after a redirect or a DNS change
  • —Rate limited per visitor and overall, with a 10-minute cache and a single instance, so it can't be used to flood a site or run up a bill

In Action

Sentinel's dashboard: Parliament and the portfolio, every check passing

The local dashboard (sentinel -serve), re-checking every five minutes.

$ sentinel
   SITE        CHECK          DETAIL
✓  parliament  status         200 in 251ms
✓  parliament  tls            expires in 45 days (2026-11-14)
✓  parliament  pq-tls         post-quantum key exchange (X25519MLKEM768)
✓  parliament  headers        all security headers present
✓  parliament  exposed-files  1 sensitive path(s) not exposed
✓  portfolio   status         200 in 167ms
✓  portfolio   tls            expires in 89 days (2026-12-28)
✓  portfolio   pq-tls         post-quantum key exchange (X25519MLKEM768)
✓  portfolio   headers        all security headers present
✓  portfolio   exposed-files  4 sensitive path(s) not exposed

Under the Hood

0

third-party dependencies

6

checks per site

30 min

schedule on GitHub Actions

27

automated tests

How it works

  • —Each site is checked in its own goroutine; the homepage is fetched once and every check reads that single response, TLS certificate included
  • —The post-quantum check reads which key exchange the handshake negotiated. Go's TLS client offers the hybrid X25519MLKEM768 (classical X25519 plus NIST's ML-KEM) by default, so if the server picks it, traffic recorded today stays safe from a future quantum computer
  • —Alerts fire on change, not on every run: a small state file remembers each check's last status, so a day-long outage is one alert, not 48 — plus one when it recovers
  • —If a notification fails to send, the change is rolled back and retried on the next run; state is written atomically so a crash can't corrupt it
  • —On GitHub Actions, the state file is carried between runs in the Actions cache, so the checks come from outside my network even while my laptop sleeps
  • —The dashboard is compiled into the binary with go:embed, listens on localhost only, and refuses cross-site requests to its Run button

Bugs worth remembering

My own monitor got me banned from my own site

The very first run probed Parliament for /.env and /.git — which are honeypot routes I'd built into Parliament to catch scanners. It did its job: my IP was banned within seconds. Sentinel now takes a per-site list of paths it must never request, and the README warns about it in bold.

Telling a real leak from a “soft 404”

Plenty of sites answer a missing /.env with 200 OK and an HTML error page, and a redirect to a login page can look like success too. Each probe now checks for the file's real signature — a git HEAD starts with “ref:”, a .DS_Store with its magic bytes — and uses an HTTP client that never follows redirects.

A recovery that could never be detected

An unreachable site originally reported a “reachable” check that disappeared once the site came back, so there was nothing to compare against and no recovery alert. Reporting it under the “status” check every site always has fixed it — the kind of bug you only find by writing the state-change tests.

It flagged this portfolio too

Sentinel reported that this site was missing all five security headers. GitHub Pages can't set custom headers, so I put Cloudflare in front of it and added them with a response-header rule, including a Content-Security-Policy built from exactly what the site loads. The check has been green since.