provision-env-db: revoke PUBLIC connect on the OWNER database too
All checks were successful
PR build (required check) / changes (pull_request) Successful in 6s
secrets-guard / encrypted (pull_request) Successful in 5s
PR build (required check) / build-backend (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Has been skipped
shell-lint / shellcheck (pull_request) Successful in 6s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / gate (pull_request) Successful in 1s

Revoking PUBLIC's CONNECT on the guest's own database is the half that does not
matter. A fresh Postgres database carries '=Tc' for PUBLIC — TEMP and CONNECT —
so as soon as a second role exists on the instance it can open a session against
every database still holding the default.

Provisioning beta and stopping there left thermograph_beta able to connect to
prod's database. The runbook's isolation check caught it on the live cutover;
this makes the script produce the state that check expects instead of a false
sense of it.

Grants the owner's read-only role an explicit CONNECT before the revoke, so the
ad-hoc query path (ops/dbq.sh, which connects as thermograph_ro) cannot lose
access to prod. The owner's app role owns the database and is a superuser, so it
is unaffected either way.
This commit is contained in:
Emi Griffith 2026-07-26 00:12:25 -07:00
parent 7583258509
commit 1aeea6bdc5
2 changed files with 50 additions and 2 deletions

View file

@ -149,8 +149,16 @@ it. That guard is why the superuser check above should never fail; run it
anyway. anyway.
If the first command SUCCEEDS, stop. `CONNECT` was not revoked and beta would be If the first command SUCCEEDS, stop. `CONNECT` was not revoked and beta would be
able to read production data; re-run the provisioning script and re-check before able to open a session against the production database; re-run the provisioning
going further. script and re-check before going further.
This is not hypothetical — it happened on the live cutover. The first version of
the script revoked `PUBLIC` connect on the *guest's* database only, and a fresh
Postgres database carries `=Tc` (TEMP **and** CONNECT) for `PUBLIC`, so
`thermograph_beta` could still reach `thermograph`. The script now hardens the
owner's database too, granting the owner's read-only role an explicit `CONNECT`
first so the revoke cannot strip `ops/dbq.sh` of its access. The check above is
what caught it.
**Rollback:** `DROP DATABASE thermograph_beta; DROP ROLE thermograph_beta;` **Rollback:** `DROP DATABASE thermograph_beta; DROP ROLE thermograph_beta;`
prod is unaffected either way. prod is unaffected either way.

View file

@ -156,6 +156,46 @@ ALTER DEFAULT PRIVILEGES FOR ROLE :"role" IN SCHEMA public
GRANT SELECT ON SEQUENCES TO :"rorole"; GRANT SELECT ON SEQUENCES TO :"rorole";
SQL SQL
# --- harden the OWNER environment's database too -------------------------------
#
# Revoking CONNECT from PUBLIC on the guest's own database is only half the job,
# and the missing half is the half that matters. A fresh Postgres database
# carries `=Tc` for PUBLIC — TEMP *and* CONNECT — so the moment a second role
# exists on the instance, that role can connect to every database that still has
# the default. Provisioning beta and stopping here left `thermograph_beta` able
# to open a session against prod's database; caught live by the runbook's own
# isolation check, which is why that check exists.
#
# Order matters: grant the owner's read-only role an EXPLICIT connect first, so
# the revoke below cannot strip the ad-hoc query path (ops/dbq.sh) of its access.
# The owner's app role is the database owner and a superuser, so it is unaffected
# either way.
OWNER_DB=$(
# shellcheck disable=SC1090 # sourced above; re-resolving prod in a subshell
thermograph_topology prod >/dev/null 2>&1
printf '%s' "$TG_DB_NAME"
)
OWNER_RO=$(
thermograph_topology prod >/dev/null 2>&1
printf '%s_ro' "$TG_DB_USER"
)
# Restore this run's environment — thermograph_topology overwrote the TG_* vars.
thermograph_topology "$ENV_NAME"
if [ "$OWNER_DB" != "$TG_DB_NAME" ]; then
echo "==> Hardening the owner database '${OWNER_DB}' against PUBLIC connects"
docker exec -i "$DB_CID" \
psql -v ON_ERROR_STOP=1 -U thermograph -d postgres \
-v ownerdb="$OWNER_DB" -v ownerro="$OWNER_RO" <<'SQL'
-- Only if that read-only role actually exists (it predates this script).
SELECT format('GRANT CONNECT ON DATABASE %I TO %I', :'ownerdb', :'ownerro')
WHERE EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'ownerro')
\gexec
REVOKE CONNECT ON DATABASE :"ownerdb" FROM PUBLIC;
SQL
fi
echo "==> OK: ${TG_DB_USER} owns ${TG_DB_NAME} (timescaledb enabled, PUBLIC revoked)" echo "==> OK: ${TG_DB_USER} owns ${TG_DB_NAME} (timescaledb enabled, PUBLIC revoked)"
echo " Verify isolation:" echo " Verify isolation:"
echo " docker exec $DB_CID psql -U ${TG_DB_USER} -d thermograph -c 'select 1' # must FAIL" echo " docker exec $DB_CID psql -U ${TG_DB_USER} -d thermograph -c 'select 1' # must FAIL"