feat(db): add optional least-privilege serving roles - #1575
feat(db): add optional least-privilege serving roles#1575Moustafa-Moustafa wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds opt-in support for running schema migrations with a separate (more privileged) PostgreSQL connection, and introduces “logical” admin/runtime group roles intended to enable future least-privilege serving modes for distributed deployments (RFC #1375). The new behavior is gated on NEBRASKA_MIGRATIONS_DB_URL and only activates when it names a different DB user than NEBRASKA_DB_URL.
Changes:
- Add
NEBRASKA_MIGRATIONS_DB_URLsupport to run DDL/migrations over a short-lived, single-connection pool. - Provision
nebraska_adminandnebraska_runtimeNOLOGIN roles (when opted-in) and apply a grant policy from an embeddedgrants.sql. - Add tests validating table classification and privilege enforcement for the serving roles.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| CHANGELOG.md | Documents the opt-in migrations connection and serving-role provisioning behavior. |
| backend/pkg/api/api.go | Introduces migrations-connection plumbing (withMigrationsDB) and routes migrations/init/down-migrations through it. |
| backend/pkg/api/dbroles.go | Implements logical role provisioning, serving-user membership grant, and application of embedded grants. |
| backend/pkg/api/dbroles_test.go | Adds coverage for table classification, grants, enforcement, and opt-in provisioning wiring. |
| backend/pkg/api/db/grants.sql | Defines the SQL policy for read/write/sequence grants for admin/runtime serving roles. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Nebraska connects to Postgres with a single role that does everything: runs migrations, writes publisher metadata, and writes telemetry. The distributed topology in RFC flatcar#1375 needs an edge node that physically cannot write the admin tables, so that a bug or a bypassed handler guard fails at the database instead of corrupting the control plane. A role like that cannot own the schema, so migrations need credentials of their own. NEBRASKA_MIGRATIONS_DB_URL, when set, is used for every DDL operation over a short-lived connection that is closed again once the schema is up to date. When it also names a different user than NEBRASKA_DB_URL, Nebraska ensures the nebraska_admin_<database> and nebraska_runtime_<database> group roles exist, attaches the admin and runtime table privileges to them from db/grants.sql, and makes the serving user a member of the admin role. The operator provisions the two login roles and writes no GRANT of their own. The runtime role is provisioned but nothing selects it yet; choosing it needs the instance mode flag, which follows. This commit ships the machinery and no privilege separation. Deployments that leave NEBRASKA_MIGRATIONS_DB_URL unset are untouched: migrations run on the serving connection exactly as before, no roles are created, and no table privileges change. Refs: flatcar#1375 Signed-off-by: Moustafa Moustafa <momousta@microsoft.com>
dd38641 to
ec9a613
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (2)
backend/pkg/api/dbroles.go:109
- setupServingRoles grants the serving user membership in the admin role before applyGrants runs. If applyGrants then fails (e.g., migrations role doesn't own all tables), Nebraska exits but leaves behind a partially-applied state (role membership granted without table privileges), which can be confusing for operators.
Applying grants first and only granting membership after a successful applyGrants keeps provisioning atomic from the operator’s perspective.
if err := grantServingRole(migrationsDB, servingUser, roles.admin); err != nil {
return err
}
l.Info().Str("role", roles.admin).Str("user", servingUser).Msg("granted the serving database role")
backend/pkg/api/dbroles.go:40
- Role names are derived by concatenating the database name (current_database()) directly onto the prefix. PostgreSQL identifiers are limited (NAMEDATALEN-1, typically 63 bytes), so long database names will be truncated by Postgres at CREATE ROLE time, making subsequent lookups/creates use a different string and causing provisioning to fail with "role already exists"/"does not exist" mismatches.
Consider enforcing a stable <=63-byte derived name (e.g., truncate with a hash suffix) and apply the same derivation in db/grants.sql and tests.
return servingRoles{
admin: adminRolePrefix + database,
runtime: runtimeRolePrefix + database,
}, nil
feat(db): add optional least-privilege serving roles
Nebraska connects to Postgres with a single role that does everything: runs migrations, writes publisher metadata, and writes telemetry. The distributed topology in RFC #1375 needs an edge node that physically cannot write the admin tables, so that a bug or a bypassed handler guard fails at the database instead of corrupting the control plane. A role like that cannot own the schema, so migrations need credentials of their own.
NEBRASKA_MIGRATIONS_DB_URL, when set, is used for every DDL operation over a short-lived connection that is closed again once the schema is up to date. When it also names a different user than NEBRASKA_DB_URL, Nebraska ensures the nebraska_admin and nebraska_runtime group roles exist, attaches the admin and runtime table privileges to them from db/grants.sql, and makes the serving user a member of nebraska_admin. The operator provisions one bare login role and writes no GRANT of their own.
The runtime role is provisioned but nothing selects it yet; choosing it needs the instance mode flag, which follows. This commit ships the machinery and no privilege separation. Deployments that leave NEBRASKA_MIGRATIONS_DB_URL unset are untouched: migrations run on the serving connection exactly as before, no roles are created, and no table privileges change.
Refs: #1375
How to use
No new behavior is introduced for anyone who does not set
NEBRASKA_MIGRATIONS_DB_URL, so the first thing to validate is that asingle-instance deployment is not impacted at all.
To exercise the new path, create one bare login role and point the two variables
at different users:
neb_serverstarts with no privileges of its own; Nebraska grants it what itneeds. To see the database boundary that a later PR will select, revoke the
membership and grant
nebraska_runtimeinstead — no restart is needed, sincerole membership takes effect on open sessions.
Testing done
Smoke test on a local environment with the server connecting as a login role that
owned nothing and had been granted nothing by hand:
changelog/directory (user-facing change, bug fix, security fix, update)/bootand/usrsize, packages, list files for any missing binaries, kernel modules, config files, kernel modules, etc.