Skip to content

feat(db): add optional least-privilege serving roles - #1575

Open
Moustafa-Moustafa wants to merge 1 commit into
flatcar:mainfrom
Moustafa-Moustafa:feat/db-serving-roles
Open

feat(db): add optional least-privilege serving roles#1575
Moustafa-Moustafa wants to merge 1 commit into
flatcar:mainfrom
Moustafa-Moustafa:feat/db-serving-roles

Conversation

@Moustafa-Moustafa

Copy link
Copy Markdown
Contributor

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 a
single-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:

$ psql -c "create role neb_server login password '…';"

$ export NEBRASKA_MIGRATIONS_DB_URL="postgres://postgres:…@host/nebraska?sslmode=disable"
$ export NEBRASKA_DB_URL="postgres://neb_server:…@host/nebraska?sslmode=disable"
$ ./bin/nebraska --auth-mode noop
… INF granted the serving database role role=nebraska_admin user=neb_server

neb_server starts with no privileges of its own; Nebraska grants it what it
needs. To see the database boundary that a later PR will select, revoke the
membership and grant nebraska_runtime instead — no restart is needed, since
role membership takes effect on open sessions.

Testing done

$ cd backend
$ make code-checks
go build ./...
./tools/check_pkg_test.sh
NEBRASKA_SKIP_TESTS=1 go test ./... >/dev/null
./tools/golangci-lint run --fix
0 issues.
go mod tidy
$ make check-backend-with-container

... spins up postgres via docker-compose.test.yaml, runs the full backend suite
with NEBRASKA_RUN_SERVER_TESTS=1, then tears down.
ok  github.com/flatcar/nebraska/backend/pkg/api                    28.221s
ok  github.com/flatcar/nebraska/backend/pkg/api/admin              0.281s
ok  github.com/flatcar/nebraska/backend/pkg/api/runtime            2.815s
ok  github.com/flatcar/nebraska/backend/pkg/auth                   0.012s
ok  github.com/flatcar/nebraska/backend/pkg/middleware             0.008s
ok  github.com/flatcar/nebraska/backend/pkg/omaha                  4.170s
ok  github.com/flatcar/nebraska/backend/pkg/random                 0.003s
ok  github.com/flatcar/nebraska/backend/pkg/sessions               0.005s
ok  github.com/flatcar/nebraska/backend/pkg/sessions/memcache      0.004s
ok  github.com/flatcar/nebraska/backend/pkg/sessions/memcache/gob  0.006s
ok  github.com/flatcar/nebraska/backend/pkg/syncer                 5.838s
ok  github.com/flatcar/nebraska/backend/test/api                   22.741s
ok  github.com/flatcar/nebraska/backend/test/auth/oidc             3.457s

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 entries added in the respective changelog/ directory (user-facing change, bug fix, security fix, update)
  • Inspected CI output for image differences: /boot and /usr size, packages, list files for any missing binaries, kernel modules, config files, kernel modules, etc.

Copilot AI lite review requested due to automatic review settings August 12, 2026 21:14
@Moustafa-Moustafa
Moustafa-Moustafa requested a review from a team as a code owner August 12, 2026 21:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_URL support to run DDL/migrations over a short-lived, single-connection pool.
  • Provision nebraska_admin and nebraska_runtime NOLOGIN roles (when opted-in) and apply a grant policy from an embedded grants.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.

Comment thread backend/pkg/api/db/grants.sql Outdated
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>
Copilot AI review requested due to automatic review settings August 13, 2026 19:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants