Skip to content
All builds

GatehouseGated Admin Panel

What it does for you

An internal admin panel behind an email allowlist, one-time email codes and hand-written TOTP, with roles, step-up, lockout and a hash-chained audit log.

Gatehouse screenshot

Screenshots

Gatehouse screenshot 1
Gatehouse screenshot 2

In more detail

Gatehouse is a private admin panel for a fictional studio. You only get in if your email is on a list, you enter a one-time code sent to your "email," and then you pass a code from an authenticator app. Inside are the controls a real internal tool needs: roles, active sessions you can revoke, a tamper-evident audit log, a sample dataset with CSV export, and a fresh code required for anything sensitive.

Honest limits

There is no real email delivery by design; codes land in a local outbox folder. It was never run behind a real TLS proxy. The step-up prompt and export download were checked in code and API tests, not by hand. DB insert timing still differs slightly for unlisted emails.

How I built it

This row started on the Codex list. On the last day I asked Claude Code to build everything left, and it took Codex rows from the bottom up, so Gatehouse ended up a Claude Code build. It runs on Node 24 with node:http and the built-in node:sqlite, and zero npm packages.

I set the spec, the threat list, and the rules: no inline script, identical responses for listed and unlisted emails, demo mode only behind a flag. Claude Code wrote the pure security core and its tests first, including TOTP written by hand and checked against the RFC test vectors, then the database, server, and UI. The first version shipped with 452 checks, including live runs that boot the real server.

Then an independent security review read the finished code and found what my tests missed: three medium findings, two low ones and one informational note. Behind a reverse proxy, every "per-IP" limit would have collapsed into one global limit. Five wrong codes against someone's email would lock that named person out. And a leftover enrollment session could swap out an enrolled authenticator.

We fixed all of them, each with a regression test. --trust-proxy now takes the rightmost X-Forwarded-For entry. Failure counters belong to the attempt's session and IP, not the email. Enrollment returns 409 once a user is enrolled and ends their other sessions. Smaller fixes went in too, including a Host allowlist against DNS rebinding. The suite went from 452 to 503 checks.

What I learned

  • I tested each defense, not how an attacker could turn it against a real user. That is exactly the gap the review found.
  • TOTP replay protection has to be an atomic UPDATE ... WHERE last_step < ?, not read-then-write. My own screenshot runs proved it: repeated demo sign-ins inside one minute got refused.
  • "Can't enumerate the allowlist" means same status, same body, and the same work, so the code hash is computed for unlisted emails too.
  • A hash chain catches edits, deletions, and reorders, but not someone cutting off the newest rows. That needs an anchor outside the database.
  • A strict CSP with no inline script shapes the whole frontend, and a static test keeps it that way.

What carried forward

The authenticator QR codes come from my #11 encoder, imported, not copied. Going the other way, #61 and #62 import Gatehouse's tokens, TOTP, throttle, headers, and audit chain through one lib/reuse.js file, so fixes here flow there. #61 applied the review's lesson from the start: failures count per email plus IP, never per email alone.

If I took it further

I would anchor the audit head hash outside the database and add passkeys as a second factor next to TOTP.

#075 Built with Claude Code September 24, 2026 503 automated checks

Want one like this?