LMS platform: a default admin password
October 2026. A learning platform we run for clients, on a single self-hosted server. An outside party got platform-admin access and created accounts inside two client tenants. No one got a shell on the server.
What went wrong
| Weakness | Why it mattered |
|---|---|
| The seed script created the platform admin with a known default password, and re-created it on restart if missing | The first sign-in attempt worked |
| The internal API between the platform and tenants was public, protected only by a shared secret | Once the secret leaked, nothing else stopped it |
| The admin panel and a database admin tool were reachable from the internet | Every scanner could try them |
| The platform ran in development mode in production | Detailed errors helped the attacker |
Impact
- Rogue admin accounts and sessions in two tenants, about 1 MB of tenant data read.
- Tenant settings changed for a few minutes, including a callback address.
- A download link students would use was replaced. It was found and removed before students were affected, as far as logs show.
How we responded
- Backed up the affected rows, then suspended (not deleted) the rogue accounts and revoked their sessions, so evidence was kept.
- Removed the malicious rows in every database and verified none remained.
- Moved the database tool to localhost only, reachable through a tunnel.
- Put the platform admin panel and admin API behind mutual TLS with per-admin client certificates.
- Created new admin accounts, locked the default one, reset passwords.
Still to do at the time of writing: rotate the tenant API secrets, make the internal API internal-only, fix the seed so it never sets a password, find how the secret leaked, and commit the mutual TLS changes so the next deploy does not undo them.
Lessons
Seed scripts never create users with known passwords, and never re-create an admin on restart. Create the first admin by hand with a generated password and MFA.
Server-to-server APIs are unreachable from the internet. A shared secret is a second lock, not the only one.
Changes made by hand during an incident are committed to the repo the same day, or the next deploy reverts them.
- Public certificate logs list every subdomain. Use a wildcard certificate behind a CDN for internal hosts, or keep them off public DNS.
- Respond reversibly first (suspend, back up), then clean up.