mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
fix: the RDS master user is no longer a PostgreSQL superuser (#687)
This commit is contained in:
@@ -49,6 +49,10 @@ BOOTSTRAP_DIR="${RDS_BOOTSTRAP_DIR:-/run/rds-init}"
|
||||
|
||||
MOUNTS_FILE="${RDS_MOUNTS_FILE:-/proc/mounts}"
|
||||
|
||||
# The group role the master's administrative privileges hang off. Named as on
|
||||
# AWS, so a customer's `GRANT rds_superuser TO ...` works here too.
|
||||
MASTER_GROUP_ROLE="rds_superuser"
|
||||
|
||||
log() { echo "[rds-init] $*"; }
|
||||
fail() { echo "[rds-init] ERROR: $*" >&2; exit 1; }
|
||||
|
||||
@@ -85,6 +89,17 @@ load_handoff() {
|
||||
RDS_PORT="${RDS_PORT:-5432}"
|
||||
|
||||
[ -n "${RDS_MASTER_USERNAME}" ] || fail "bootstrap config has no RDS_MASTER_USERNAME"
|
||||
|
||||
# The master role is created NOSUPERUSER, so a master named after the
|
||||
# bootstrap superuser would leave the cluster without one and strip rds-agent
|
||||
# of the privileged SQL it reaches over peer auth — unrepairable, since the
|
||||
# datadir bootstraps once. The control plane reserves both names already.
|
||||
case "${RDS_MASTER_USERNAME}" in
|
||||
"${PG_USER}" | "${MASTER_GROUP_ROLE}")
|
||||
fail "RDS_MASTER_USERNAME ${RDS_MASTER_USERNAME} is reserved by the platform; the control plane should have rejected it"
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${RDS_PORT}" in
|
||||
'' | *[!0-9]*) fail "bootstrap config has a non-numeric RDS_PORT: ${RDS_PORT}" ;;
|
||||
esac
|
||||
@@ -264,6 +279,7 @@ psql_bootstrap() {
|
||||
RDS_MASTER_USERNAME="${RDS_MASTER_USERNAME}" \
|
||||
RDS_MASTER_PASSWORD="${RDS_MASTER_PASSWORD}" \
|
||||
RDS_DB_NAME="${RDS_DB_NAME}" \
|
||||
RDS_GROUP_ROLE="${MASTER_GROUP_ROLE}" \
|
||||
"${PG_BIN}/psql" --no-psqlrc --quiet --no-align --tuples-only \
|
||||
-v ON_ERROR_STOP=1 \
|
||||
-h "${BOOTSTRAP_DIR}" -p "${RDS_PORT}" -U "${PG_USER}" -d postgres -f -
|
||||
@@ -291,18 +307,32 @@ bootstrap_master() {
|
||||
fi
|
||||
}
|
||||
|
||||
# initdb may already have made it the bootstrap superuser, when the master
|
||||
# username is "postgres".
|
||||
# The master role is administrative but deliberately not a PostgreSQL SUPERUSER,
|
||||
# as on AWS: a superuser reaches outside the database entirely — COPY FROM
|
||||
# PROGRAM, pg_read_file/pg_write_file, untrusted languages — so master
|
||||
# credentials would be a shell in the DB VM rather than a database privilege.
|
||||
# The cluster's own superuser stays the bootstrap role, which only rds-init and
|
||||
# rds-agent reach, over the unix socket under peer auth.
|
||||
apply_master_role() {
|
||||
log "setting the master password for role ${RDS_MASTER_USERNAME}"
|
||||
log "creating the master role ${RDS_MASTER_USERNAME}"
|
||||
# PG16 hardened CREATEROLE: it can neither grant itself SUPERUSER nor take
|
||||
# membership in a predefined role it does not administer, so CREATEROLE here
|
||||
# is not a route back to the privileges above.
|
||||
psql_bootstrap <<'SQL'
|
||||
\getenv master RDS_MASTER_USERNAME
|
||||
\getenv password RDS_MASTER_PASSWORD
|
||||
\getenv grp RDS_GROUP_ROLE
|
||||
SELECT NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'grp') AS create_group \gset
|
||||
\if :create_group
|
||||
CREATE ROLE :"grp" NOLOGIN;
|
||||
\endif
|
||||
GRANT pg_monitor, pg_signal_backend, pg_checkpoint TO :"grp";
|
||||
SELECT NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'master') AS create_role \gset
|
||||
\if :create_role
|
||||
CREATE ROLE :"master" WITH LOGIN SUPERUSER;
|
||||
CREATE ROLE :"master" WITH LOGIN NOSUPERUSER CREATEDB CREATEROLE;
|
||||
\endif
|
||||
ALTER ROLE :"master" WITH LOGIN SUPERUSER PASSWORD :'password';
|
||||
ALTER ROLE :"master" WITH LOGIN NOSUPERUSER CREATEDB CREATEROLE PASSWORD :'password';
|
||||
GRANT :"grp" TO :"master" WITH ADMIN OPTION;
|
||||
SQL
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ EOF
|
||||
cat > "${STUBBIN}/psql" <<'EOF'
|
||||
#!/bin/sh
|
||||
{
|
||||
echo "--- psql $* [master=${RDS_MASTER_USERNAME:-} password=${RDS_MASTER_PASSWORD:-} db=${RDS_DB_NAME:-}]"
|
||||
echo "--- psql $* [master=${RDS_MASTER_USERNAME:-} password=${RDS_MASTER_PASSWORD:-} db=${RDS_DB_NAME:-} grp=${RDS_GROUP_ROLE:-}]"
|
||||
cat
|
||||
} >> "${PSQL_CALLS}"
|
||||
# ON_ERROR_STOP makes a real psql exit non-zero on any SQL failure; the stub
|
||||
@@ -140,11 +140,12 @@ export INITDB_CALLS PGCTL_CALLS PSQL_CALLS
|
||||
|
||||
# write_handoff <mode> <password> <dbname>: lay down the bootstrap.env rds-agent
|
||||
# would have written. An empty password omits the field, as `attach` does.
|
||||
# MASTER_USER overrides the master role name, for the reserved-name cases.
|
||||
write_handoff() {
|
||||
mkdir -p "${HANDOFF}"
|
||||
{
|
||||
echo "RDS_MODE=$1"
|
||||
echo "RDS_MASTER_USERNAME=mulgamaster"
|
||||
echo "RDS_MASTER_USERNAME=${MASTER_USER:-mulgamaster}"
|
||||
[ -n "$2" ] && echo "RDS_MASTER_PASSWORD=$2"
|
||||
[ -n "$3" ] && echo "RDS_DB_NAME=$3"
|
||||
echo "RDS_PORT=6543"
|
||||
@@ -171,7 +172,7 @@ reset_state() {
|
||||
: > "${INITDB_CALLS}"
|
||||
: > "${PGCTL_CALLS}"
|
||||
: > "${PSQL_CALLS}"
|
||||
unset INITDB_FAIL PG_HBA_AS_DIR PSQL_FAIL LS_FAIL RDS_ALLOW_LOCAL_DATADIR || true
|
||||
unset INITDB_FAIL PG_HBA_AS_DIR PSQL_FAIL LS_FAIL RDS_ALLOW_LOCAL_DATADIR MASTER_USER || true
|
||||
}
|
||||
|
||||
# run: invoke rds-init with every path knob pointed into the temp dir.
|
||||
@@ -213,12 +214,31 @@ if run_ok "initialize"; then
|
||||
&& pass "initialize: ssl enabled" || fail "initialize: ssl not enabled"
|
||||
grep -q 'shared_buffers = 128MB' "${PGDATA}/conf.d/10-rds-parameters.conf" \
|
||||
&& pass "initialize: resolved parameters installed" || fail "initialize: no parameter include"
|
||||
grep -q 'ALTER ROLE :"master" WITH LOGIN SUPERUSER PASSWORD' "${PSQL_CALLS}" \
|
||||
grep -q 'ALTER ROLE :"master" WITH LOGIN NOSUPERUSER CREATEDB CREATEROLE PASSWORD' "${PSQL_CALLS}" \
|
||||
&& pass "initialize: master password applied" || fail "initialize: no ALTER ROLE"
|
||||
grep -q 'password=s3cr3t' "${PSQL_CALLS}" \
|
||||
&& pass "initialize: password passed through the environment" || fail "initialize: password not in psql env"
|
||||
grep -q 'CREATE DATABASE' "${PSQL_CALLS}" \
|
||||
&& pass "initialize: initial database created" || fail "initialize: no CREATE DATABASE"
|
||||
|
||||
# The master role is administrative but not a PostgreSQL superuser: a
|
||||
# superuser reaches outside the database (COPY FROM PROGRAM, pg_read_file,
|
||||
# untrusted languages), so master credentials would be a shell in the DB VM.
|
||||
grep -qE '(^|[^O])SUPERUSER' "${PSQL_CALLS}" \
|
||||
&& fail "initialize: bootstrap SQL still grants SUPERUSER" \
|
||||
|| pass "initialize: master role is not a superuser"
|
||||
grep -q 'GRANT :"grp" TO :"master" WITH ADMIN OPTION' "${PSQL_CALLS}" \
|
||||
&& pass "initialize: master holds the administrative group role" \
|
||||
|| fail "initialize: master never granted the group role"
|
||||
grep -q 'grp=rds_superuser' "${PSQL_CALLS}" \
|
||||
&& pass "initialize: the group role is named as on AWS" || fail "initialize: group role name not delivered"
|
||||
grep -q 'GRANT pg_monitor, pg_signal_backend, pg_checkpoint TO :"grp"' "${PSQL_CALLS}" \
|
||||
&& pass "initialize: monitoring/signal/checkpoint granted" || fail "initialize: group role has no privileges"
|
||||
|
||||
# The three predefined roles that hand back exactly what SUPERUSER gave away.
|
||||
grep -qE 'pg_(read|write)_server_files|pg_execute_server_program' "${PSQL_CALLS}" \
|
||||
&& fail "initialize: granted a role that restores file or program access" \
|
||||
|| pass "initialize: no server-file or program-execution roles granted"
|
||||
grep -q "listen_addresses=''" "${PGCTL_CALLS}" \
|
||||
&& pass "initialize: bootstrap server is socket-only" || fail "initialize: bootstrap server listened on TCP"
|
||||
grep -q 'stop' "${PGCTL_CALLS}" \
|
||||
@@ -400,6 +420,21 @@ reset_state
|
||||
rm -rf "${HANDOFF}"
|
||||
run_fails "no-handoff"
|
||||
|
||||
# --- Case 9: a master username the platform reserves ---
|
||||
# The master role is created NOSUPERUSER, so one named after the bootstrap
|
||||
# superuser would leave the cluster without a superuser at all and strip
|
||||
# rds-agent of the privileged SQL it needs — on a datadir that bootstraps once.
|
||||
for reserved in postgres rds_superuser; do
|
||||
reset_state
|
||||
MASTER_USER="${reserved}"
|
||||
write_handoff initialize 's3cr3t' appdb
|
||||
run_fails "reserved-master-${reserved}"
|
||||
[ -s "${INITDB_CALLS}" ] \
|
||||
&& fail "reserved-master-${reserved}: initdb ran before the name was refused" \
|
||||
|| pass "reserved-master-${reserved}: refused before initdb"
|
||||
unset MASTER_USER
|
||||
done
|
||||
|
||||
if [ "${FAILS}" -eq 0 ]; then
|
||||
echo "PASS: all rds-init cases"
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user