Skip to main content

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​

WeaknessWhy it mattered
The seed script created the platform admin with a known default password, and re-created it on restart if missingThe first sign-in attempt worked
The internal API between the platform and tenants was public, protected only by a shared secretOnce the secret leaked, nothing else stopped it
The admin panel and a database admin tool were reachable from the internetEvery scanner could try them
The platform ran in development mode in productionDetailed 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​

  1. Backed up the affected rows, then suspended (not deleted) the rogue accounts and revoked their sessions, so evidence was kept.
  2. Removed the malicious rows in every database and verified none remained.
  3. Moved the database tool to localhost only, reachable through a tunnel.
  4. Put the platform admin panel and admin API behind mutual TLS with per-admin client certificates.
  5. 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​

Rule

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.

Rule

Server-to-server APIs are unreachable from the internet. A shared secret is a second lock, not the only one.

Rule

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.