Internal admin panels start as “just a page to refund that order” and become the highest-privilege service you run—often with weaker auth than customer login. Attackers know this. So do bored employees. A minimal admin can be safe if you treat it as production security work, not a weekend UI.
Why unsafe admins ship
- Prod DB credentials in a personal script
- Shared basic-auth password in chat
- No audit trail for money-moving actions
- CSRF-free cookie sessions on the open internet
- Raw SQL box “for emergencies”
Speed is not an excuse for a second production control plane without controls.
Threat model (one page)
| Threat | Example | Control |
|---|---|---|
| External attacker | stolen session / exposed URL | SSO, private network, short sessions |
| Malicious insider | silent refunds | RBAC + audit + dual control |
| Honest mistake | wipe user | confirmations, soft delete |
| Dependency | XSS in admin | strict CSP, no HTML from users |
| Supply chain | compromised npm in admin app | lockfiles, minimal deps |
AuthN / AuthZ model
AuthN: company SSO (OIDC/SAML) or equivalent. No local shared passwords.
AuthZ: server-side roles, for example:
support_readsupport_refundops_reprocessadmin_roles(break-glass)
Check roles on every mutating request, not only in the React router.
Audit log fields (minimum)
ts, actor_id, actor_email, role, action, target_type, target_id,
before_json, after_json, request_id, ip, user_agent, result
Money and PII actions are incomplete without before/after. Keep audit storage append-only from the app’s perspective.
Destructive actions
- Require re-auth or typed confirm (
DELETE) for irreversible ops - Dual control for high-impact refunds above threshold
- Rate limit privileged POSTs
- Prefer soft flags (
disabled_at) over hard deletes in v1
Minimal v1 feature set
Ship only:
- Search entity by id/email
- View status + recent events
- One or two mutations with audit
- Health of the admin app itself
Deliberately omit from v1:
- Arbitrary SQL console
- Shell access
- Bulk CSV update without dry-run
- Impersonation without banner + audit
Checklist file: content/labs/internal-admin-safely/checklist.md.
Implementation notes
- Separate hostname (
admin.internal.example) and auth policy - No admin routes mounted on the public API process if avoidable
- CSRF tokens for browser forms; same-site cookies
- Never echo secrets back to the UI
- Paginate everything; admins love
SELECT *accidents
Review before production
- [ ] SSO enforced in non-dev
- [ ] Role matrix written
- [ ] Audit verified with a test mutation
- [ ] Network restriction documented
- [ ] On-call knows how to revoke access
- [ ] Data retention for audit logs agreed
Summary
A safe internal admin is small, authenticated, authorized, audited, and boring. Add power only when the control plane already logs who did what. The breach headline writes itself when refund buttons ship faster than access reviews.