Views use SQL SECURITY INVOKER, never the DEFINER default
Every CREATE VIEW in Sql/*.sql had omitted the SQL SECURITY clause, so MariaDB
defaulted each view to SQL SECURITY DEFINER, pinning it to
DEFINER=<migration_user>@<host the migration ran from>. When the migration runner’s
source IP changed, MariaDB could no longer resolve that definer account, and every view
read by the runtime app user started failing — a production incident that took ~11 hours
to diagnose because the failure surfaced far from its cause. We now require
SQL SECURITY INVOKER on every view, so it runs with the querying user’s own privileges
instead of a frozen creator identity. This applies to any future stored procedure,
function, trigger, or event too — all four carry a DEFINER in MariaDB/MySQL. Triggers
are the worst case, since they fire during someone else’s statement and the failure
surfaces even further from the cause.
Considered options
- Give the migration runner a stable source IP (e.g. routing migrations through a fixed WireGuard tunnel endpoint) so the definer host never changes. Rejected as the primary fix: it only narrows the blast radius and re-breaks on the next infrastructure change (rotated tunnel endpoint, runner rebuild). Worth keeping as defense in depth, not as the fix.
SQL SECURITY INVOKERon every view. Chosen — it removes the dependency on who created the view or where they connected from entirely.
Consequences
Sql/0027.convert_views_to_sql_security_invoker.sqlrecreates all views existing at the time withCREATE OR REPLACE ALGORITHM=UNDEFINED SQL SECURITY INVOKER VIEW ...(verified byte-identicalSELECTbodies against a live MariaDB instance — no behavior change, only the security clause).- CI (
.github/workflows/build.yml) rejects new migration files thatCREATE VIEWwithoutSQL SECURITY INVOKERor that carry an explicitDEFINER=clause, scoped to files added in the current PR (historical migration files are never edited, per this repo’s migration convention, so the gate can’t run against the full history). Tools/db-view-smoke-test.shfails the build if any view’ssecurity_typeisDEFINER, and functionallySELECTs from every view as the connecting user. It runs in CI after migrations (sharedtestuser) and indeploy.ymlafter migrations, authenticated as the actual runtime app user (MYSQL_USER/MYSQL_PASSWORD, distinct from the migration user) — the step that would have caught the original incident before it reached production.- Grants still matter independently:
SQL SECURITY INVOKERmeans the connecting user needs its own privileges on the underlying tables, not just the view.