Document the two database users and how an existing deployment moves to them
Sixth section of #23, docs only. docs/production-deployment.md explains why the console connects as two users and what the runtime user may not be (a superuser, an owner of the database or of any object, or a member of any role but console_runtime), how a new deployment creates the runtime login with its password set in psql, how to run migrations as a separate step, and the steps for a deployment that has connected as one login so far: create the runtime login, store its string as a new secret (Docker Swarm secrets cannot be changed), point MC_DB_DSN_FILE at it and MC_DB_MIGRATE_DSN_FILE at the existing secret, deploy, verify the sessions, privileges, memberships, database settings and the functions of public, re-run the reader-role script, and how to roll back. The bootstrap superuser of the official image cannot lose SUPERUSER, so it stays as the migration user. docs/database-management.md runs the reader-role script as the migration user. make lint and TestEnvironmentReferenceDocumentsEveryConfigKey pass; the section changes no code, and the previous section's full suite ran with these files already in the tree.
This commit is contained in:
@@ -241,9 +241,12 @@ login role at all.
|
||||
|
||||
### Running the script
|
||||
|
||||
Run it as the role that owns the console's schemas, which also needs
|
||||
`CREATEROLE`. With the official `postgres` image that is the `POSTGRES_USER`
|
||||
the console connects as, a superuser:
|
||||
Run it as the migration user, the login that owns the console's schemas and
|
||||
that `db-migrate-dsn` names; the script also needs `CREATEROLE`. With the
|
||||
official `postgres` image the migration user is the `POSTGRES_USER`, a
|
||||
superuser. On a managed database, run it as a user that has `CREATEROLE` and
|
||||
owns the console's tables. The runtime user (`db-dsn`) owns nothing and cannot
|
||||
run it:
|
||||
|
||||
psql -d <database> -v ON_ERROR_STOP=1 -f scripts/db-reader-role.sql
|
||||
|
||||
@@ -279,10 +282,10 @@ reach the database port. `console_reader` adds nothing to that exposure, and
|
||||
a password on it would not be asked for; the remedy is `scram-sha-256` in
|
||||
that line. Distribution packages authenticate the socket by `peer`, which
|
||||
admits `psql -U console_reader` only from an operating-system user of that
|
||||
name or through a `pg_ident.conf` map. On a managed database the user the
|
||||
console connects as may lack `CREATEROLE`, and the run fails; run it as a
|
||||
user that has `CREATEROLE` and owns the console's tables, then give the role
|
||||
a password and allow it in the provider's network rules.
|
||||
name or through a `pg_ident.conf` map. On a managed database the
|
||||
migration user may lack `CREATEROLE`, and the run fails; run it as a user
|
||||
that has `CREATEROLE` and owns the console's tables, then give the role a
|
||||
password and allow it in the provider's network rules.
|
||||
|
||||
### Keeping the list current
|
||||
|
||||
|
||||
+143
-10
@@ -119,14 +119,48 @@ sign-in page and the app introduce themselves consistently.
|
||||
## 5. Database
|
||||
|
||||
Follow [database-management.md](database-management.md) for the schema
|
||||
layout and migration conventions. There is nothing extra to run before
|
||||
first boot: **migrations run automatically at startup** (`member-console
|
||||
start` calls `db.ConnectAndMigrate` before serving any request —
|
||||
`cmd/start.go`). A fresh database becomes fully migrated on the first
|
||||
successful boot; there is no separate "run migrations" step required for a
|
||||
new deployment, though `member-console migrate up|down|status` is available
|
||||
for out-of-band migration control (e.g. running the pending migrations before
|
||||
a rolling restart).
|
||||
layout and migration conventions.
|
||||
|
||||
**The console connects as two users.** The runtime user (`db-dsn`) serves
|
||||
every request. It is not a superuser, owns neither the database nor any
|
||||
object in it, and holds no role but `console_runtime`, the group role that
|
||||
carries the privileges the console's writes need. The console refuses to
|
||||
start when the runtime user is a superuser, owns the database or an object, or
|
||||
holds another role, because a user that owned its own tables could grant itself
|
||||
what the migrations revoke. Some hosting providers grant a new login
|
||||
administrative roles by default; revoke them once from the runtime login
|
||||
(`REVOKE <role> FROM <runtime user>;`). The migration user (`db-migrate-dsn`)
|
||||
owns the console's schemas, runs the migrations and grants `console_runtime`
|
||||
to the runtime user. It may be a superuser.
|
||||
|
||||
**Create the two users for a new deployment.** With the official `postgres`
|
||||
image, `POSTGRES_USER` is the migration user, and its connection string is
|
||||
`db-migrate-dsn`. Create the runtime login by hand, as that user, and set its
|
||||
password interactively so that the password reaches neither the shell's
|
||||
history nor the server's log:
|
||||
|
||||
```sql
|
||||
CREATE ROLE <runtime user> LOGIN;
|
||||
\password <runtime user>
|
||||
```
|
||||
|
||||
Set `db-dsn` (`MC_DB_DSN`, or `MC_DB_DSN_FILE`) to the runtime user's
|
||||
connection string and `db-migrate-dsn` (`MC_DB_MIGRATE_DSN`, or
|
||||
`MC_DB_MIGRATE_DSN_FILE`) to the migration user's. At startup, `member-console
|
||||
start` migrates only on the `db-migrate-dsn` connection, grants
|
||||
`console_runtime` to the runtime user there, closes that connection, checks
|
||||
the runtime user, and only then serves. A fresh database becomes fully
|
||||
migrated on the first successful boot. `member-console migrate up|down|status`
|
||||
also uses `db-migrate-dsn`.
|
||||
|
||||
**Run migrations as a separate step.** To keep the migration password out of
|
||||
the running process, leave `db-migrate-dsn` off the service and run
|
||||
`member-console migrate up` with it set as a one-shot step before each
|
||||
deploy. With `db-migrate-dsn` unset, `start` runs no migration, and it
|
||||
refuses to start while the schema is behind the code. After the first run of
|
||||
`migrate up`, run, as the migration user, the `GRANT console_runtime TO
|
||||
<runtime user> WITH INHERIT TRUE, SET FALSE;` statement that the refusal names, because
|
||||
only a boot with `db-migrate-dsn` set grants it.
|
||||
|
||||
**Check configuration before restarting.** `member-console config validate`
|
||||
connects to the database, parses every stored override and the resolved
|
||||
@@ -174,7 +208,7 @@ and the member's row would stay Pending.
|
||||
**Retry a failed desired-state delivery by hand.** The relay stops
|
||||
retrying a row of `core.desired_state_outbox` after five failed attempts,
|
||||
marks it `failed` and logs its subject and error. No page lists these rows
|
||||
yet. Connected with the application's database role, list them:
|
||||
yet. Connected as the runtime user, list them:
|
||||
|
||||
```sql
|
||||
SELECT id, subject_kind, subject_id, resource_key, attempts, last_error, created_at
|
||||
@@ -192,6 +226,102 @@ SET status = 'pending', attempts = 0, claimed_until = NULL, last_error = NULL
|
||||
WHERE id = '<row id>' AND status = 'failed';
|
||||
```
|
||||
|
||||
### Move an existing deployment to the two users
|
||||
|
||||
A deployment that has run a release before this change connects as one
|
||||
login, which is the bootstrap superuser of the official image. Postgres does
|
||||
not let that user lose `SUPERUSER`, so it stays as it is and becomes the
|
||||
migration user. The steps below add the runtime user and are the same for any
|
||||
host; angle brackets mark values you choose.
|
||||
|
||||
1. Connected as the user the console connects as today, create the runtime
|
||||
login, then set its password in `psql`:
|
||||
|
||||
```sql
|
||||
CREATE ROLE <runtime user> LOGIN;
|
||||
\password <runtime user>
|
||||
```
|
||||
|
||||
`\password` prompts for the password, so it appears in neither the
|
||||
shell's history nor the server's log.
|
||||
2. Store the runtime user's connection string as a new secret. Docker Swarm
|
||||
secrets cannot be changed once created, so the new string is a secret
|
||||
with a new name. Create it from standard input, type the string, and end
|
||||
with Ctrl-D:
|
||||
|
||||
```sh
|
||||
docker secret create <new secret> -
|
||||
```
|
||||
3. In the service definition, point `MC_DB_DSN_FILE` at the new secret. Add
|
||||
`MC_DB_MIGRATE_DSN_FILE` pointing at the existing secret, which now holds
|
||||
the migration user's string. Declare the new secret on the service and at
|
||||
the top level of the stack file.
|
||||
4. Deploy the release. Its first boot migrates, grants `console_runtime` to
|
||||
the runtime user, closes the migration connection and checks the runtime
|
||||
user.
|
||||
5. Verify, connected as the migration user:
|
||||
- The boot log has a `runtime database user checked` line that names the
|
||||
runtime user.
|
||||
- The console's sessions run under the runtime user:
|
||||
|
||||
```sql
|
||||
SELECT usename, count(*) FROM pg_stat_activity
|
||||
WHERE datname = current_database() GROUP BY usename;
|
||||
```
|
||||
- The runtime user cannot insert into a table the console fences, so this
|
||||
returns `false`:
|
||||
|
||||
```sql
|
||||
SELECT has_table_privilege('<runtime user>', 'core.pool_provisions', 'INSERT');
|
||||
```
|
||||
- The runtime user holds no role but `console_runtime`, so this lists that
|
||||
one row:
|
||||
|
||||
```sql
|
||||
SELECT g.rolname FROM pg_auth_members m
|
||||
JOIN pg_roles g ON g.oid = m.roleid
|
||||
WHERE m.member = '<runtime user>'::regrole;
|
||||
```
|
||||
- Database-level settings list only what you set on purpose. A setting
|
||||
attached to the database, for example by a user that owned it once,
|
||||
survives a change of owner and applies to every later session. The
|
||||
console's own sessions ignore a `search_path` or `role` set this way,
|
||||
but another superuser session in the database does not:
|
||||
|
||||
```sql
|
||||
SELECT setconfig FROM pg_db_role_setting
|
||||
WHERE setdatabase = (SELECT oid FROM pg_database WHERE datname = current_database());
|
||||
```
|
||||
- The functions in `public` that no extension owns are only
|
||||
`update_updated_at_column()`. A function another user created in
|
||||
`public` whose argument types match a catalog function's (for example
|
||||
`public.quote_ident(name)`) would capture unqualified calls in later
|
||||
migrations, which run as the migration user; once its owner is
|
||||
reassigned to the migration user, the console's own check no longer
|
||||
reports it:
|
||||
|
||||
```sql
|
||||
SELECT p.oid::regprocedure FROM pg_catalog.pg_proc p
|
||||
WHERE p.pronamespace = 'public'::regnamespace
|
||||
AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d
|
||||
WHERE d.objid = p.oid AND d.deptype = 'e');
|
||||
```
|
||||
6. Run `scripts/db-reader-role.sql` as the migration user, as after every
|
||||
deploy that migrates
|
||||
([database-management.md](database-management.md#running-the-script)).
|
||||
7. To roll back, deploy the previous compose file and image tag. The previous
|
||||
release connects as the migration user again. `console_runtime`, its
|
||||
memberships, the function that takes the plan-act obligation lock and the
|
||||
ownership of `core.webhook_events` stay, and the previous release does not
|
||||
use them. Its migration run applies nothing, because it knows only
|
||||
versions that are already applied. Partitions that the previous release
|
||||
creates meanwhile belong to the migration user; after rolling forward
|
||||
again, give each to `core_owner`:
|
||||
|
||||
```sql
|
||||
ALTER TABLE core.webhook_events_<YYYY_MM> OWNER TO core_owner;
|
||||
```
|
||||
|
||||
## 6. Integrations
|
||||
|
||||
Configure whichever of these your deployment actually offers. Each is
|
||||
@@ -239,7 +369,10 @@ they live, what the console shows members, and what the API key can read.
|
||||
With the database reachable, `base-url` set, the session store's password
|
||||
configured, and the OIDC keys pointed at your realm, start the process (`member-console
|
||||
start`, or your image's equivalent entrypoint). At startup it, in order:
|
||||
runs pending migrations, validates the aggregated configuration (failing
|
||||
opens the runtime connection; when `db-migrate-dsn` is set, opens the
|
||||
migration connection, runs pending migrations, grants `console_runtime` to
|
||||
the runtime user and closes the migration connection; checks the runtime user
|
||||
and logs `runtime database user checked`; validates the aggregated configuration (failing
|
||||
fast with every problem named at once if something required is missing —
|
||||
see [environment-reference.md](environment-reference.md) for what's
|
||||
required), connects to Temporal if configured, and starts serving.
|
||||
|
||||
@@ -206,9 +206,9 @@ This section's `make test` passes only after the documented stack respin of item
|
||||
|
||||
## 6. Deployment docs and the procedure for an existing deployment
|
||||
|
||||
- [ ] 6.1 `docs/production-deployment.md`, step 5 and step 7, and the procedure for an existing deployment, as design D17 writes them, with placeholders only, including that the runtime login owns neither the database nor any object and holds no role but `console_runtime`, and that the verification reads `pg_db_role_setting` and the functions of `public` (design D17 step 5). "Retry a failed desired-state delivery by hand" says "the runtime user".
|
||||
- [ ] 6.2 `docs/database-management.md`, "Running the script" and "Logging in": the script runs as the migration user (`POSTGRES_USER` with the official image, a superuser); on a managed database, as a user with `CREATEROLE` that owns the console's tables.
|
||||
- [ ] 6.3 No code changes; `make test` passes.
|
||||
- [x] 6.1 `docs/production-deployment.md`, step 5 and step 7, and the procedure for an existing deployment, as design D17 writes them, with placeholders only, including that the runtime login owns neither the database nor any object and holds no role but `console_runtime`, and that the verification reads `pg_db_role_setting` and the functions of `public` (design D17 step 5). "Retry a failed desired-state delivery by hand" says "the runtime user".
|
||||
- [x] 6.2 `docs/database-management.md`, "Running the script" and "Logging in": the script runs as the migration user (`POSTGRES_USER` with the official image, a superuser); on a managed database, as a user with `CREATEROLE` that owns the console's tables.
|
||||
- [x] 6.3 No code changes; `make test` passes.
|
||||
|
||||
## 7. Landing
|
||||
|
||||
|
||||
Reference in New Issue
Block a user