DocsHardening the operator console
▸ Tutorial 15

Hardening the operator console

Reading time: 6 minutes. Hands-on time: ~15 minutes for full lockdown. What you’ll have at the end: a /superadmin dashboard that’s invisible to scanners, requires 2FA, and (in production) sits behind Cloudflare Access so only your verified email can even hit the login page.

The operator console at /superadmin/* has root access to every tenant on the install — full read, full write, plus the ability to impersonate, delete tenants, refund subscriptions, and override plan tiers. Treat it like SSH on a production server. Three layers of defence:

  1. Two-factor authentication (in-app TOTP)
  2. IP allowlist (env-var-driven, defence in depth)
  3. Cloudflare Access (production only, IdP-gated reverse proxy)

Use as many as you can. They’re cumulative.


Layer 1 — Two-factor authentication

RFC 6238 TOTP, works with any standards-compliant authenticator app: 1Password, Authy, Google Authenticator, Bitwarden, Microsoft Authenticator, etc.

Enable it

  1. Sign in to the operator console at /superadmin/login.
  2. Go to Operations → Security · 2FA in the sidebar (or hit /superadmin/security directly).
  3. Click 🔐 Enable 2FA.
  4. Scan the QR code with your authenticator app. It’ll show a 6-digit code that rotates every 30 seconds.
  5. Enter the current code into the Step 2 field. The system verifies it against the proposed secret BEFORE saving — that proves your authenticator is wired correctly so you can’t lock yourself out.
  6. Submit. Done.

Sign-in flow with 2FA

  1. Enter username + password as usual.
  2. The form switches to a 6-digit code field.
  3. Enter the current code from your authenticator app.
  4. Submit. Session cookie issued; you’re in.

Behind the scenes, after step 1 the server returns a short-lived (5-minute), HMAC-signed handle that the form passes back with the code. No session cookie until both factors check out.

What if you lose your authenticator device?

You’ll need shell access to the server (you’re the operator, you have it):

cd /path/to/scarif-one
npx tsx scripts/bootstrap-superadmin.mjs --clear-totp <your-username>

The TOTP secret is wiped. Sign in with password only. Re-enable from /superadmin/security against your new device.

If you’ve also lost your password, run the same script with --reset to set a fresh one:

npx tsx scripts/bootstrap-superadmin.mjs <username> <new-password> --reset

Both commands write to the audit log so you can see them in data/superadmin/audit.log.


Layer 2 — IP allowlist

Restrict /superadmin/* to specific IPs at the application level. When configured, requests from non-matching IPs get a generic 404 — the operator console becomes invisible to scanners.

This works in dev (where the operator is just you on localhost) and in production (where you might allow your office IP and your home /24).

Configure

In .env.local or your production environment:

SCARIF_SUPERADMIN_ALLOWED_IPS=localhost,203.0.113.42,198.51.100.0/24

Format:

  • Comma-separated entries
  • Each entry is an exact IPv4/IPv6 address, a CIDR range (a.b.c.d/N), or the literal localhost
  • IPv4-mapped IPv6 (::ffff:1.2.3.4) is normalised automatically
  • Trailing whitespace ignored

After changing, restart the server so the env var reloads.

Find your IP

curl https://ifconfig.me

Or visit https://whatismyip.com. Note that mobile ISPs assign rotating IPs — for a home /24 use nslookup against your dynamic-DNS hostname or just allow your ISP’s consumer block.

What happens if I’m blocked?

The route returns 404. There’s no error message hinting at the existence of /superadmin — to a scanner the path looks identical to any other unknown URL. Sign-in form, login API, bootstrap API, every /superadmin/* page and /api/superadmin/* endpoint share the same gate.

Behind a reverse proxy

The check reads X-Forwarded-For (first IP) or X-Real-IP from the request headers. Make sure your reverse proxy (Cloudflare, Nginx, Fly.io edge) sets these — most do by default. If you see null IPs in the audit log, your proxy is stripping them.


Layer 3 — Cloudflare Access (production only)

The strongest gate: put /superadmin/* behind Cloudflare’s Zero Trust identity provider. Only your verified email gets a Cloudflare One-Time PIN to even reach the login form.

This requires Cloudflare in front of your domain (free Cloudflare DNS works fine). Setup is in the Cloudflare dashboard — no code change.

Setup

  1. Cloudflare dashboardZero TrustAccessApplicationsAdd an application.
  2. Pick Self-hosted.
  3. Application name: Scarif One Operator Console.
  4. Application domain: scarifone.com with Path: /superadmin*.
  5. Identity providers: One-Time PIN is enabled by default — emails you a 6-digit code. You can also add Google / GitHub / Microsoft identity if you prefer.
  6. PoliciesCreate a policy:
    • Action: Allow
    • Configure rules: IncludeEmails → list the email addresses allowed (e.g. tom@scarifone.com).
  7. Save.

That’s it. From now on, anyone hitting scarifone.com/superadmin/* (and any sub-path) sees Cloudflare’s “Sign in with email” page. Wrong email → blocked at the edge before the request ever reaches your server. Correct email → 6-digit PIN sent to your inbox, you enter it, you proceed to your normal /superadmin/login form (which still demands password + TOTP).

Why both Cloudflare Access AND password+TOTP?

Defence in depth. Cloudflare Access can fail (cert renewal, policy misconfig), and someone could compromise your email. Password + TOTP behind that gate means even if both are breached, an attacker needs all three credentials.


Recommended posture for production

EnvironmentLayer 1 (TOTP)Layer 2 (IP allowlist)Layer 3 (Cloudflare Access)
Dev (localhost)✓ Enablelocalhost only
Staging✓ Required✓ Office + home IPsOptional
Production✓ Required✓ Belt-and-braces✓ Required

If you’re self-hosting Scarif One and don’t use Cloudflare in front of your domain, double-down on Layers 1 + 2 and consider running the dashboard only on a Tailscale-private hostname.


Verifying it works

After enabling all three layers in production:

  1. From an unallowed IP (e.g. tether to your phone, or use a VPN):

    • https://scarifone.com/superadmin/login → Cloudflare Access “Sign in with email”.
    • Even if you bypass Cloudflare somehow → 404 from the IP allowlist.
  2. From an allowed IP:

    • https://scarifone.com/superadmin/login → Cloudflare PIN flow → your normal login form → password + TOTP code → operator dashboard.
  3. In the audit log at /superadmin/audit, you should see superadmin.login_success entries with your IP and { totp: true } in the detail blob.

Three barriers, all logged. If a future intrusion gets past one, you’ll see the breach in the audit log because no clean entry exists.


Common issues

“404” even from my office IP

Your reverse proxy isn’t forwarding the real IP. Check that X-Forwarded-For reaches the Next.js process. In proxy.ts, log the headers temporarily.

TOTP code “invalid” despite reading the right number

Authenticator app clock drift. Make sure both your phone and the server are using NTP. The implementation accepts ±30s of drift; if your phone’s clock is more than that off, codes won’t match.

Cloudflare Access prompts every 5 minutes

Lower the Session duration in your Access application policy (default is 24h, can go up to 1 month for trusted operators).

Lost both password AND TOTP

Run both reset commands:

npx tsx scripts/bootstrap-superadmin.mjs <user> <new-pass> --reset
npx tsx scripts/bootstrap-superadmin.mjs --clear-totp <user>

Both audit-logged. If you’re seeing this on a remote server you can’t shell into — that’s a real problem you should fix before you go live.


Need help? Email <a href="mailto:hello@scarifone.com">hello@scarifone.com</a>.