Secure admin panels
Aikido's "How to build a secure admin panel for your SaaS app" lists the recurring mistakes that turn customer-success tooling into a breach: admin endpoints stitched into the same surface as the public app, shared support accounts with no audit trail, single-factor authentication, and no Content-Security-Policy to contain injected JavaScript. Daloy doesn't ship an admin panel, but every primitive you need to follow that checklist is already in the framework. This page maps each rule to the helper that enforces it.
1. Don't mix admin routes into your public app
The first rule is the most important: admin endpoints should not be reachable from the same hostname as the public API, and they should not leak into client-side bundles where attackers can probe them. Daloy gives you two tools that compose into a clean "private API only" posture:
internal: trueon a route hides it from the public listener entirely — it is only reachable viaapp.inject()(server-to-server) or through an adapter that explicitly mounts internal routes on a separate socket / hostname / port.subdomains()lets you mount the admin sub-app onadmin.example.comwhile the public API stays onapi.example.com, so a critical issue in the admin code can be taken offline (firewall, DNS, deploy) without affecting the customer-facing app.
Front the internal listener with a VPN, a Cloudflare Access tunnel, or a private load balancer. The combination of internal: true + ipRestriction() means misconfigured DNS or a routing accident cannot expose the admin surface to the public internet by default.
2. Per-admin accounts with an audit log
Aikido's second rule is to ban shared support@app.io logins so every sensitive change is attributable. Daloy doesn't ship an identity provider — pick one (Auth0, Clerk, Cognito, Keycloak, or your own JWT issuer) and verify per-admin tokens with bearerAuth() or the JWT helpers. The framework gives you the audit-log primitives:
requestId()stamps every request with a propagatedx-request-idso the same identifier appears in every downstream log line.- The built-in
loggeremits structured JSON; attach the authenticated admin's subject claim in yourhooksso "who did what, when, from where" falls out for free. tracing()ties the same request id into OpenTelemetry spans for long-term retention in your observability stack.
3. Enforce 2FA (or 3FA) for admin auth
Daloy doesn't implement TOTP / WebAuthn itself — that belongs in your identity provider — but it gives you three layers that compose with whatever 2FA your IdP enforces, so a stolen password alone is not enough:
- Network factor.
ipRestriction()with a tight CIDR allow-list (corporate VPN, Cloudflare WARP egress, office gateway) means a credential leak from outside that range is rejected before authentication even runs. - Login-throttle factor.
rateLimit({ windowMs, max, groupId: "admin-auth" })shares one bucket across/admin/login,/admin/otp, and/admin/recoveryso password spraying and OTP guessing are both throttled by the same counter. - Session factor.
session()withcookieOptions: { secure: true, sameSite: "strict" }pluscsrf()on every mutating route closes the state-changing-request loophole even if a cookie escapes the admin subdomain.
4. Block unknown JavaScript with CSP
Aikido's last rule — and the one that would have prevented the "Apple email injection" case they cite — is a strict Content-Security-Policy on the admin HTML. Daloy's secureHeaders() already emits a CSP, and it can mint a fresh per-request nonce so legitimate inline bootstrap scripts run while any injected <script> from a future XSS is silently dropped by the browser.
Pair this with app.cspReportRoute()if you want Daloy to terminate the report endpoint itself (size-capped, with optional body redaction so reported URLs don't leak PII into logs).
Checklist — Aikido rule → Daloy primitive
| Rule | What Daloy gives you |
|---|---|
| Admin panel is not built into the public app | internal: true routes + app.inject(), optional subdomains() mount on a separate host |
| Admin reachable only from trusted networks | ipRestriction({ allow: [...] }) with CIDR support, fails closed when no peer address is available |
| Per-admin authentication, no shared accounts | bearerAuth(), basicAuth(), JWT helpers,session()— each ties a request to an identifiable subject |
| Action audit log | requestId() + logger structured JSON + tracing() spans |
| 2FA / 3FA: throttle login + OTP + recovery together | rateLimit({ groupId: "admin-auth" }) shared bucket across all auth routes |
| State-changing routes can't be cross-site triggered | csrf() (double-submit or fetch-metadata) + session() with SameSite=Strict; Secure; HttpOnly |
| Block unknown JavaScript (CSP) | secureHeaders({ contentSecurityPolicy: { …, nonce: true, trustedTypes }, hsts, … }) + app.cspReportRoute() |
| Take admin offline without taking the app offline | Separate internal listener / subdomain mount — flip a feature flag or firewall rule, leave the public API running |
What Daloy intentionally does not do
- Implement TOTP, WebAuthn, or SSO — use an identity provider. Daloy verifies the resulting bearer tokens / JWTs.
- Render an admin UI. The framework is API-first; pair it with any admin framework (Refine, AdminJS, Retool, internal Next.js) and point that UI at the internal-only Daloy routes.
- Decide your network perimeter.
ipRestriction()enforces a CIDR list, but you still own the firewall, VPN, or zero-trust access layer that determines which addresses are trustworthy in the first place.