fix: make the DB endpoint the only path to the engine (#830)

This commit is contained in:
Julian Sommer
2026-08-19 11:04:21 +10:00
committed by GitHub
parent 0e08b9d256
commit bdc79108e9
30 changed files with 1317 additions and 58 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ start_pre() {
# shellcheck disable=SC1090
. "${AGENT_ENV}"
export RDS_GATEWAY_URL RDS_GATEWAY_CA RDS_REGION RDS_DB_INSTANCE_IDENTIFIER \
RDS_ENGINE RDS_ENGINE_VERSION RDS_HANDOFF_DIR RDS_ENGINE_HOST RDS_ENGINE_PORT \
RDS_ENGINE RDS_ENGINE_VERSION RDS_HANDOFF_DIR RDS_ENGINE_PORT \
RDS_POLL_WAIT RDS_DATA_MOUNT
fi
}
+70 -2
View File
@@ -14,6 +14,7 @@
# RDS_MASTER_PASSWORD=<present only when RDS_MODE=initialize>
# RDS_DB_NAME=<initial database; optional, as on AWS>
# RDS_PORT=<listen port; defaults to 3306>
# RDS_LISTEN_ADDRESS=<the customer ENI address the engine binds>
# RDS_DATA_VOLUME_ID / RDS_DATA_VOLUME_SERIAL=<expected volume identity>
# RDS_VM_GENERATION / RDS_FORMAT_AUTHORIZED=<create-only format grant>
# RDS_BOOTSTRAP_PENDING / RDS_PAYLOAD_ID=<the control plane is still
@@ -120,6 +121,11 @@ SECURE_FILE_DIR="${RDS_SECURE_FILE_DIR:-/var/lib/mysql-files}"
MOUNTS_FILE="${RDS_MOUNTS_FILE:-/proc/mounts}"
# How long the bind address may take to appear on a local interface. The lease
# is taken in the boot runlevel on a 600s budget and this script runs in the
# default one, so the wait is a guard rather than an expected delay.
LISTEN_ADDRESS_TIMEOUT="${RDS_LISTEN_ADDRESS_TIMEOUT:-120}"
# How long the private bootstrap server gets to open its socket, as probes one
# BOOTSTRAP_POLL apart. InnoDB creates its redo log and system tablespace on this
# first start, so it is not instant.
@@ -138,6 +144,50 @@ CREATE_DATABASE_PROCEDURE="create_database"
log() { echo "[rds-init] $*"; }
fail() { echo "[rds-init] ERROR: $*" >&2; exit 1; }
# A dotted-quad IPv4 literal. bind_address would also take a hostname, but the
# control plane sends an address, and accepting a name would reintroduce the
# resolution step the bind exists to remove.
is_ipv4() {
case "$1" in
'' | *[!0-9.]*) return 1 ;;
esac
_rest="$1"
_octets=0
while [ -n "${_rest}" ]; do
_octet="${_rest%%.*}"
case "${_rest}" in
*.*) _rest="${_rest#*.}" ;;
*) _rest="" ;;
esac
# A leading zero reads as octal to some parsers and decimal to others,
# so the two would disagree about which address was bound.
case "${_octet}" in
0 | [1-9] | [1-9][0-9] | [1-9][0-9][0-9]) ;;
*) return 1 ;;
esac
[ "${_octet}" -le 255 ] || return 1
_octets=$((_octets + 1))
done
[ "${_octets}" = 4 ]
}
# The engine binds one address and refuses to start if it is not there yet, and
# an engine that will not start for an unexplained reason is the worst way to
# learn that DHCP lost its race. Bounded, then fatal with the address named.
wait_for_listen_address() {
_deadline=$(($(date +%s) + LISTEN_ADDRESS_TIMEOUT))
while :; do
if ip -4 -o addr show 2>/dev/null |
awk -v want="${RDS_LISTEN_ADDRESS}" \
'{ split($4, addr, "/"); if (addr[1] == want) found = 1 } END { exit !found }'; then
return 0
fi
[ "$(date +%s)" -lt "${_deadline}" ] || break
sleep 1
done
fail "the endpoint address ${RDS_LISTEN_ADDRESS} did not appear on any interface within ${LISTEN_ADDRESS_TIMEOUT}s; the engine binds it and nothing else, so refusing to start"
}
# Between these two, the datadir carries a mysql system database but no usable
# master user. _server_pid is the private bootstrap server, if one is running.
_created_datadir=0
@@ -199,6 +249,7 @@ load_handoff() {
RDS_MASTER_PASSWORD="${RDS_MASTER_PASSWORD:-}"
RDS_DB_NAME="${RDS_DB_NAME:-}"
RDS_PORT="${RDS_PORT:-3306}"
RDS_LISTEN_ADDRESS="${RDS_LISTEN_ADDRESS:-}"
RDS_VM_GENERATION="${RDS_VM_GENERATION:-}"
# Absent from an agent that predates the receipt protocol, which reads as
# "nothing is pending" and leaves every path below unchanged.
@@ -252,6 +303,14 @@ load_handoff() {
'' | *[!0-9]*) fail "bootstrap config has a non-numeric RDS_PORT: ${RDS_PORT}" ;;
esac
# No fallback to a wildcard. The engine is reachable over the customer ENI
# and nowhere else, and a handoff that cannot say which address that is has
# no safe reading — the DB VM's other two NICs carry no security group the
# customer can see, let alone set.
[ -n "${RDS_LISTEN_ADDRESS}" ] || fail "bootstrap config has no RDS_LISTEN_ADDRESS; refusing to start an engine that would answer on every interface"
is_ipv4 "${RDS_LISTEN_ADDRESS}" ||
fail "bootstrap config has a non-IPv4 RDS_LISTEN_ADDRESS: ${RDS_LISTEN_ADDRESS}"
# redact_stream is line-oriented, so a password spanning a line boundary
# would match neither half and reach /dev/console in the clear. The control
# plane rejects one; refusing here too keeps that an asserted property of
@@ -537,7 +596,8 @@ EOF
# The 90- prefix sorts last within the include directory and MariaDB takes the
# last occurrence of a setting, so a resolved parameter can never move the
# endpoint's port, redirect the datadir, turn off TLS or lower the protocol floor.
# endpoint's port, widen the bind, redirect the datadir, turn off TLS or lower
# the protocol floor.
#
# innodb_buffer_pool_chunk_size and innodb_buffer_pool_instances are deliberately
# absent although they are platform-owned: 11.8 removed the second outright, so
@@ -554,7 +614,14 @@ write_platform_config() {
port = ${RDS_PORT}
datadir = ${DATADIR}
socket = ${SOCKET}
bind_address = 0.0.0.0
# The customer ENI and nothing else. The DB VM's other two NICs sit on the
# shared RDS system VPC and the host's management bridge, neither of which any
# customer security group governs, so a socket bound there is reachable across
# accounts. MariaDB's analogue of a scoped pg_hba is the host pattern on a
# grant, which lives in mysql.user on the data volume and would survive a
# restore into another VPC — so for this engine the bind is the boundary.
bind_address = ${RDS_LISTEN_ADDRESS}
# InnoDB is the only storage engine the snapshot guarantee holds for: it keeps a
# redo log and recovers from a crash-consistent snapshot, and Aria and MyISAM do
@@ -963,6 +1030,7 @@ consume_secrets() {
}
load_handoff
wait_for_listen_address
require_data_volume
require_engine_stamp
+66 -3
View File
@@ -201,6 +201,18 @@ EOF
# an installed receipt and an armed sweep coexist. Matched on the receipt
# directory by name: the engine stamp syncs its directory too, long before the
# traps are armed, and killing there would leave the window untested.
# ip stub: stands in for `ip -4 -o addr show`, printing one line per address in
# IP_ADDR_LIST in the format the bind-address wait parses.
cat > "${STUBBIN}/ip" <<'EOF'
#!/bin/sh
n=1
for cidr in ${IP_ADDR_LIST:-}; do
echo "${n}: eth$((n - 1)) inet ${cidr} scope global eth$((n - 1))"
n=$((n + 1))
done
exit 0
EOF
cat > "${STUBBIN}/sync" <<'EOF'
#!/bin/sh
if [ "${SYNC_KILL_PARENT:-0}" = "1" ]; then
@@ -236,6 +248,9 @@ SECURE_FILE_DIR="${WORK}/mysql-files"
LOG_DIR="${WORK}/log"
ENGINE_LOG="${LOG_DIR}/error.log"
MOUNTS="${WORK}/mounts"
# The customer ENI address the engine binds, as the control plane delivers it.
LISTEN_ADDRESS_DEFAULT="10.30.1.15"
KILL_PID_FILE="${WORK}/rds-init.pid"
MARIADBD_TEST_PID_FILE="${WORK}/mariadbd.pid"
WORKDIR="${WORK}"
@@ -266,6 +281,13 @@ write_handoff() {
[ -n "$3" ] && echo "RDS_DB_NAME='$3'"
[ -n "${PENDING:-}" ] && echo "RDS_BOOTSTRAP_PENDING=${PENDING}"
[ -n "${PAYLOAD_ID:-}" ] && echo "RDS_PAYLOAD_ID=${PAYLOAD_ID}"
# Written unset rather than defaulted when the case is about a handoff
# that omits it, which rds-init has to refuse rather than fill in.
[ "${OMIT_LISTEN_ADDRESS:-0}" = "1" ] ||
echo "RDS_LISTEN_ADDRESS=${LISTEN_ADDRESS:-${LISTEN_ADDRESS_DEFAULT}}"
# Delivered but unread here: MariaDB's analogue of a scoped pg_hba is a
# grant host pattern on the data volume, which a restore would carry.
echo "RDS_CLIENT_CIDR=10.30.0.0/16"
# Last, and unconditional: an optional line failing its test would make
# the whole group's status non-zero and `set -e` would end the run here.
echo "RDS_PORT=6543"
@@ -327,6 +349,10 @@ reset_state() {
unset PENDING PAYLOAD_ID DB_ID RECEIPT_DIR_OVERRIDE CLIENT_STATUS_CORRUPT STAMP_OVERRIDE || true
unset MARIADBD_START_FAIL MARIADBD_KILL_PARENT MARIADBD_IGNORE_TERM CLIENT_KILL_PARENT || true
unset ADMIN_SHUTDOWN_FAIL SYNC_KILL_PARENT || true
unset LISTEN_ADDRESS OMIT_LISTEN_ADDRESS || true
# The customer ENI carries its address by the time rds-init runs, which is
# the ordinary case; the cases about a late lease drop it themselves.
IP_ADDR_LIST="${LISTEN_ADDRESS_DEFAULT}/24 10.99.0.7/24"
rm -f "${MARIADBD_TEST_PID_FILE}"
write_tls
}
@@ -346,6 +372,8 @@ run_env() {
RDS_RECEIPT_DIR="${RECEIPT_DIR_OVERRIDE:-${RECEIPT_DIR}}" \
RDS_ENGINE_STAMP="${STAMP_OVERRIDE:-${STAMP}}" \
RDS_ALLOW_LOCAL_DATADIR="${RDS_ALLOW_LOCAL_DATADIR:-0}" \
RDS_LISTEN_ADDRESS_TIMEOUT="${LISTEN_TIMEOUT:-0}" \
IP_ADDR_LIST="${IP_ADDR_LIST:-}" \
RDS_BOOTSTRAP_POLL=0.05 \
RDS_BOOTSTRAP_PROBES=200 \
RDS_BOOTSTRAP_STOP_GRACE=0.1 \
@@ -493,9 +521,12 @@ if run_ok "initialize"; then
&& pass "initialize: delivered port applied" || fail "initialize: port not applied"
grep -q "^datadir = ${DATADIR}" "${PLATFORM_FILE}" \
&& pass "initialize: datadir pinned by the platform" || fail "initialize: no datadir"
grep -q "^bind_address = 0.0.0.0" "${PLATFORM_FILE}" \
&& pass "initialize: the engine listens for the customer ENI" \
|| fail "initialize: no bind_address"
grep -q "^bind_address = ${LISTEN_ADDRESS_DEFAULT}$" "${PLATFORM_FILE}" \
&& pass "initialize: the engine binds the customer ENI" \
|| fail "initialize: the engine does not bind the delivered address"
grep -qE '^bind_address = (0\.0\.0\.0|\*|::)$' "${PLATFORM_FILE}" \
&& fail "initialize: the engine still answers on every interface" \
|| pass "initialize: no wildcard bind"
grep -q "^default_storage_engine = InnoDB" "${PLATFORM_FILE}" \
&& pass "initialize: tables land in the engine snapshots recover" \
|| fail "initialize: default_storage_engine not pinned"
@@ -1233,6 +1264,38 @@ grep -q 'engine stamp' "${WORK}/out" \
&& pass "stamp-unwritable: refusal names the stamp" || fail "stamp-unwritable: no refusal message"
unset STAMP_OVERRIDE
# --- Case 13: a handoff that cannot say what to bind is refused ---
# The refusal is before the one-shot password is spent, so the create is still
# retryable. The only reading rds-init could reach on its own is the wildcard
# the bind exists to remove.
for _case in omit-listen bad-listen leading-zero short-quad; do
reset_state
case "${_case}" in
omit-listen) OMIT_LISTEN_ADDRESS=1 ;;
bad-listen) LISTEN_ADDRESS="db.internal" ;;
leading-zero) LISTEN_ADDRESS="010.30.1.15" ;;
short-quad) LISTEN_ADDRESS="10.30.1" ;;
esac
write_handoff initialize 's3cr3t' appdb
run_fails "${_case}"
[ -s "${INSTALLDB_CALLS}" ] \
&& fail "${_case}: initialised on a handoff that could not be read" \
|| pass "${_case}: refused before the datadir was created"
done
# --- Case 13a: the bind address must be on an interface before the engine starts ---
# An engine asked to bind an address that is not there refuses to start, which is
# the worst way to learn that DHCP lost its race.
reset_state
IP_ADDR_LIST="10.99.0.7/24"
write_handoff initialize 's3cr3t' appdb
run_fails "bind-wait"
grep -q "${LISTEN_ADDRESS_DEFAULT}" "${WORK}/out" \
&& pass "bind-wait: the refusal names the address" || fail "bind-wait: the refusal does not name the address"
[ -s "${INSTALLDB_CALLS}" ] \
&& fail "bind-wait: initialised before the address existed" \
|| pass "bind-wait: refused before the datadir was created"
if [ "${FAILS}" -eq 0 ]; then
echo "PASS: all rds-init cases"
exit 0
+1 -1
View File
@@ -35,7 +35,7 @@ start_pre() {
# shellcheck disable=SC1090
. "${AGENT_ENV}"
export RDS_GATEWAY_URL RDS_GATEWAY_CA RDS_REGION RDS_DB_INSTANCE_IDENTIFIER \
RDS_ENGINE RDS_ENGINE_VERSION RDS_HANDOFF_DIR RDS_ENGINE_HOST RDS_ENGINE_PORT \
RDS_ENGINE RDS_ENGINE_VERSION RDS_HANDOFF_DIR RDS_ENGINE_PORT \
RDS_POLL_WAIT RDS_DATA_MOUNT
fi
}
+93 -8
View File
@@ -14,6 +14,8 @@
# RDS_MASTER_PASSWORD=<present only when RDS_MODE=initialize>
# RDS_DB_NAME=<initial database; optional, as on AWS>
# RDS_PORT=<listen port; defaults to 5432>
# RDS_LISTEN_ADDRESS=<the customer ENI address the engine binds>
# RDS_CLIENT_CIDR=<the customer VPC range client authentication admits>
# RDS_DATA_VOLUME_ID / RDS_DATA_VOLUME_SERIAL=<expected volume identity>
# RDS_VM_GENERATION / RDS_FORMAT_AUTHORIZED=<create-only format grant>
# RDS_BOOTSTRAP_PENDING / RDS_PAYLOAD_ID=<the control plane is still
@@ -93,6 +95,11 @@ BOOTSTRAP_DIR="${RDS_BOOTSTRAP_DIR:-/run/rds-init}"
MOUNTS_FILE="${RDS_MOUNTS_FILE:-/proc/mounts}"
# How long the bind address may take to appear on a local interface. The lease
# is taken in the boot runlevel on a 600s budget and this script runs in the
# default one, so the wait is a guard rather than an expected delay.
LISTEN_ADDRESS_TIMEOUT="${RDS_LISTEN_ADDRESS_TIMEOUT:-120}"
# 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"
@@ -134,6 +141,63 @@ clear_unfinished_datadir() {
# The server refuses to run as root.
as_pg() { su "${PG_USER}" -c "$1"; }
# A dotted-quad IPv4 literal. postgresql.conf would also take a hostname here,
# but the control plane sends an address, and accepting a name would reintroduce
# the resolution step the bind exists to remove.
is_ipv4() {
case "$1" in
'' | *[!0-9.]*) return 1 ;;
esac
_rest="$1"
_octets=0
while [ -n "${_rest}" ]; do
_octet="${_rest%%.*}"
case "${_rest}" in
*.*) _rest="${_rest#*.}" ;;
*) _rest="" ;;
esac
# A leading zero reads as octal to some parsers and decimal to others,
# so the two would disagree about which address was bound.
case "${_octet}" in
0 | [1-9] | [1-9][0-9] | [1-9][0-9][0-9]) ;;
*) return 1 ;;
esac
[ "${_octet}" -le 255 ] || return 1
_octets=$((_octets + 1))
done
[ "${_octets}" = 4 ]
}
is_ipv4_cidr() {
case "$1" in
*/*) ;;
*) return 1 ;;
esac
_prefix="${1#*/}"
case "${_prefix}" in
'' | *[!0-9]*) return 1 ;;
esac
[ "${#_prefix}" -le 2 ] && [ "${_prefix}" -le 32 ] || return 1
is_ipv4 "${1%%/*}"
}
# The engine binds one address and fails to start if it is not there yet, and an
# engine that will not start for an unexplained reason is the worst way to learn
# that DHCP lost its race. Bounded, then fatal with the address in the message.
wait_for_listen_address() {
_deadline=$(($(date +%s) + LISTEN_ADDRESS_TIMEOUT))
while :; do
if ip -4 -o addr show 2>/dev/null |
awk -v want="${RDS_LISTEN_ADDRESS}" \
'{ split($4, addr, "/"); if (addr[1] == want) found = 1 } END { exit !found }'; then
return 0
fi
[ "$(date +%s)" -lt "${_deadline}" ] || break
sleep 1
done
fail "the endpoint address ${RDS_LISTEN_ADDRESS} did not appear on any interface within ${LISTEN_ADDRESS_TIMEOUT}s; the engine binds it and nothing else, so refusing to start"
}
load_handoff() {
[ -f "${ENV_FILE}" ] || fail "no bootstrap handoff at ${ENV_FILE} — rds-agent has not delivered the bootstrap config"
@@ -146,6 +210,8 @@ load_handoff() {
RDS_MASTER_PASSWORD="${RDS_MASTER_PASSWORD:-}"
RDS_DB_NAME="${RDS_DB_NAME:-}"
RDS_PORT="${RDS_PORT:-5432}"
RDS_LISTEN_ADDRESS="${RDS_LISTEN_ADDRESS:-}"
RDS_CLIENT_CIDR="${RDS_CLIENT_CIDR:-}"
RDS_VM_GENERATION="${RDS_VM_GENERATION:-}"
# Absent from an agent that predates the receipt protocol, which reads as
# "nothing is pending" and leaves every path below unchanged.
@@ -192,6 +258,17 @@ load_handoff() {
'' | *[!0-9]*) fail "bootstrap config has a non-numeric RDS_PORT: ${RDS_PORT}" ;;
esac
# Neither falls back to a wildcard. The engine is reachable over the customer
# ENI and nowhere else, and a handoff that cannot say which address that is
# has no safe reading — the DB VM's other two NICs carry no security group
# the customer can see, let alone set.
[ -n "${RDS_LISTEN_ADDRESS}" ] || fail "bootstrap config has no RDS_LISTEN_ADDRESS; refusing to start an engine that would answer on every interface"
is_ipv4 "${RDS_LISTEN_ADDRESS}" ||
fail "bootstrap config has a non-IPv4 RDS_LISTEN_ADDRESS: ${RDS_LISTEN_ADDRESS}"
[ -n "${RDS_CLIENT_CIDR}" ] || fail "bootstrap config has no RDS_CLIENT_CIDR; refusing to start an engine that would accept clients from anywhere"
is_ipv4_cidr "${RDS_CLIENT_CIDR}" ||
fail "bootstrap config has a non-IPv4 RDS_CLIENT_CIDR: ${RDS_CLIENT_CIDR}"
# redact_stream is line-oriented, so a password spanning a line boundary
# would match neither half and reach /dev/console in the clear. The control
# plane rejects one; refusing here too keeps that an asserted property of
@@ -443,13 +520,15 @@ write_pg_hba() {
# Managed by rds-init — regenerated on every boot. Edits here are lost.
#
# Clients reach the instance over its customer ENI, gated by the DB security
# group as on AWS RDS, and authenticate with scram-sha-256. The catch-alls cover
# loopback too, so initdb's own 127.0.0.1 and ::1 lines are not carried over.
# The local socket is how rds-init and rds-agent run privileged SQL.
# group as on AWS RDS, and authenticate with scram-sha-256. The host rule is
# scoped to the customer VPC rather than the DB subnet: an app subnet reaching a
# DB subnet arrives carrying its own address, with no NAT in between. The engine
# does not bind loopback, so initdb's own 127.0.0.1 and ::1 lines govern nothing
# and are not carried over. The local socket is how rds-init and rds-agent run
# privileged SQL.
local all all peer
include_if_exists "${HBA_FORCE_SSL_INCLUDE}"
host all all 0.0.0.0/0 scram-sha-256
host all all ::/0 scram-sha-256
host all all ${RDS_CLIENT_CIDR} scram-sha-256
EOF
chown "${PG_USER}:${PG_USER}" "${_tmp}"
chmod 0600 "${_tmp}"
@@ -466,8 +545,13 @@ EOF
}
# The 90- prefix sorts last within include_dir and the last setting wins, so a
# resolved parameter can never move the endpoint's port, turn off TLS or lower
# the protocol floor.
# resolved parameter can never move the endpoint's port, widen the bind, turn off
# TLS or lower the protocol floor.
#
# listen_addresses is the customer ENI and nothing else — not the wildcard, and
# not loopback either. The DB VM's other two NICs sit on the shared RDS system
# VPC and the host's management bridge, neither of which any customer security
# group governs, so a socket bound there is reachable across accounts.
#
# The floor is written whether or not a cert was delivered: it costs nothing with
# ssl off, and making it conditional would mean a boot without a cert leaves the
@@ -478,7 +562,7 @@ write_rds_conf() {
cat > "${_tmp}" <<EOF
# Managed by rds-init — regenerated on every boot from the bootstrap config.
# Edits here are lost; instance parameters belong in the parameter group.
listen_addresses = '*'
listen_addresses = '${RDS_LISTEN_ADDRESS}'
port = ${RDS_PORT}
unix_socket_directories = '${SOCKET_DIR}'
ssl = ${_ssl}
@@ -794,6 +878,7 @@ consume_secrets() {
}
load_handoff
wait_for_listen_address
require_data_volume
require_engine_stamp
+113 -5
View File
@@ -65,6 +65,18 @@ cat > "${STUBBIN}/ls" <<'EOF'
exec "${REAL_LS}" "$@"
EOF
# ip stub: stands in for `ip -4 -o addr show`, printing one line per address in
# IP_ADDR_LIST in the format the bind-address wait parses.
cat > "${STUBBIN}/ip" <<'EOF'
#!/bin/sh
n=1
for cidr in ${IP_ADDR_LIST:-}; do
echo "${n}: eth$((n - 1)) inet ${cidr} scope global eth$((n - 1))"
n=$((n + 1))
done
exit 0
EOF
# su stub: `su postgres -c "cmd"` runs cmd in the harness user's shell.
cat > "${STUBBIN}/su" <<'EOF'
#!/bin/sh
@@ -205,6 +217,11 @@ RECEIPT="${RECEIPT_DIR}/receipt.env"
STAMP="${DATA_MOUNT}/.spinifex-rds/engine"
HANDOFF="${WORK}/run/spinifex-rds"
MOUNTS="${WORK}/mounts"
# The customer ENI address the engine binds and the VPC range its client
# authentication admits, as the control plane delivers them.
LISTEN_ADDRESS_DEFAULT="10.30.1.15"
CLIENT_CIDR_DEFAULT="10.30.0.0/16"
KILL_PID_FILE="${WORK}/rds-init.pid"
export KILL_PID_FILE
@@ -234,6 +251,12 @@ write_handoff() {
[ -n "$3" ] && echo "RDS_DB_NAME='$3'"
[ -n "${PENDING:-}" ] && echo "RDS_BOOTSTRAP_PENDING=${PENDING}"
[ -n "${PAYLOAD_ID:-}" ] && echo "RDS_PAYLOAD_ID=${PAYLOAD_ID}"
# Written unset rather than defaulted when the case is about a handoff
# that omits one, which rds-init has to refuse rather than fill in.
[ "${OMIT_LISTEN_ADDRESS:-0}" = "1" ] ||
echo "RDS_LISTEN_ADDRESS=${LISTEN_ADDRESS:-${LISTEN_ADDRESS_DEFAULT}}"
[ "${OMIT_CLIENT_CIDR:-0}" = "1" ] ||
echo "RDS_CLIENT_CIDR=${CLIENT_CIDR:-${CLIENT_CIDR_DEFAULT}}"
# Last, and unconditional: an optional line failing its test would make
# the whole group's status non-zero and `set -e` would end the run here.
echo "RDS_PORT=6543"
@@ -270,6 +293,10 @@ reset_state() {
: > "${PSQL_CALLS}"
unset INITDB_FAIL PG_HBA_AS_DIR PSQL_FAIL LS_FAIL RDS_ALLOW_LOCAL_DATADIR MASTER_USER || true
unset PENDING PAYLOAD_ID DB_ID RECEIPT_DIR_OVERRIDE PSQL_STATUS_CORRUPT STAMP_OVERRIDE || true
unset LISTEN_ADDRESS CLIENT_CIDR OMIT_LISTEN_ADDRESS OMIT_CLIENT_CIDR || true
# The customer ENI carries its address by the time rds-init runs, which is
# the ordinary case; the cases about a late lease drop it themselves.
IP_ADDR_LIST="${LISTEN_ADDRESS_DEFAULT}/24 10.99.0.7/24"
write_tls
}
@@ -293,6 +320,8 @@ run_env() {
RDS_RECEIPT_DIR="${RECEIPT_DIR_OVERRIDE:-${RECEIPT_DIR}}" \
RDS_ENGINE_STAMP="${STAMP_OVERRIDE:-${STAMP}}" \
RDS_ALLOW_LOCAL_DATADIR="${RDS_ALLOW_LOCAL_DATADIR:-0}" \
RDS_LISTEN_ADDRESS_TIMEOUT="${LISTEN_TIMEOUT:-0}" \
IP_ADDR_LIST="${IP_ADDR_LIST:-}" \
INITDB_FAIL="${INITDB_FAIL:-0}" \
PG_HBA_AS_DIR="${PG_HBA_AS_DIR:-0}" \
PSQL_FAIL="${PSQL_FAIL:-0}" \
@@ -328,14 +357,24 @@ if run_ok "initialize"; then
grep -q 'initdb' "${INITDB_CALLS}" && pass "initialize: initdb ran" || fail "initialize: no initdb"
grep -q -- '--data-checksums' "${INITDB_CALLS}" \
&& pass "initialize: data checksums on" || fail "initialize: no --data-checksums"
grep -q 'host all all 0.0.0.0/0 scram-sha-256' "${PGDATA}/pg_hba.conf" \
&& pass "initialize: remote scram hba rule written" || fail "initialize: no hba rule"
grep -q "host all all ${CLIENT_CIDR_DEFAULT} scram-sha-256" "${PGDATA}/pg_hba.conf" \
&& pass "initialize: the scram hba rule is scoped to the customer VPC" || fail "initialize: no hba rule"
# The scope is the whole boundary this file carries: a catch-all here admits
# the system VPC and the management bridge, which no customer security group
# governs. IPv6 never appears — the platform's VPCs are IPv4-only.
grep -qE 'host .*(0\.0\.0\.0/0|::/0)' "${PGDATA}/pg_hba.conf" \
&& fail "initialize: a catch-all host rule survived the VPC scoping" \
|| pass "initialize: no catch-all host rule"
[ "$(grep -c '^host ' "${PGDATA}/pg_hba.conf")" = 1 ] \
&& pass "initialize: exactly one host rule" \
|| fail "initialize: more than one host rule, so the scope is not the boundary"
# pg_hba is first-match-wins, so initdb's loopback rules sorting above the
# catch-all would shadow anything the platform appends below them. rds-init
# owns the whole file; the catch-alls cover loopback themselves.
# platform's would shadow it. rds-init owns the whole file, and the engine
# does not bind loopback at all.
grep -qE '127\.0\.0\.1/32|::1/128' "${PGDATA}/pg_hba.conf" \
&& fail "initialize: initdb's loopback TCP rules survived above the catch-all" \
&& fail "initialize: initdb's loopback TCP rules survived" \
|| pass "initialize: initdb's loopback TCP rules are gone"
grep -q 'replication' "${PGDATA}/pg_hba.conf" \
&& fail "initialize: initdb's replication rules survived" \
@@ -358,6 +397,12 @@ if run_ok "initialize"; then
&& pass "initialize: include_dir hooked" || fail "initialize: no include_dir"
grep -q '^port = 6543' "${PGDATA}/conf.d/90-rds-init.conf" \
&& pass "initialize: delivered port applied" || fail "initialize: port not applied"
grep -q "^listen_addresses = '${LISTEN_ADDRESS_DEFAULT}'$" "${PGDATA}/conf.d/90-rds-init.conf" \
&& pass "initialize: the engine binds the customer ENI" \
|| fail "initialize: the engine does not bind the delivered address"
grep -q "^listen_addresses = '\*'" "${PGDATA}/conf.d/90-rds-init.conf" \
&& fail "initialize: the engine still answers on every interface" \
|| pass "initialize: no wildcard bind"
grep -q '^ssl = on' "${PGDATA}/conf.d/90-rds-init.conf" \
&& pass "initialize: ssl enabled" || fail "initialize: ssl not enabled"
grep -q "^ssl_min_protocol_version = 'TLSv1.3'$" "${PGDATA}/conf.d/90-rds-init.conf" \
@@ -1159,6 +1204,69 @@ grep -q 'engine stamp' "${WORK}/out" \
&& pass "stamp-unwritable: refusal names the stamp" || fail "stamp-unwritable: no refusal message"
unset STAMP_OVERRIDE
# --- Case 12: a handoff that cannot say what to bind is refused before initdb ---
# The refusal is before the one-shot password is spent, so the create is still
# retryable. The only readings rds-init could reach on its own are the wildcards
# the bind exists to remove.
for _case in omit-listen bad-listen omit-cidr bad-cidr; do
reset_state
case "${_case}" in
omit-listen) OMIT_LISTEN_ADDRESS=1 ;;
bad-listen) LISTEN_ADDRESS="db.internal" ;;
omit-cidr) OMIT_CLIENT_CIDR=1 ;;
bad-cidr) CLIENT_CIDR="10.30.0.0/33" ;;
esac
write_handoff initialize 's3cr3t' appdb
run_fails "${_case}"
[ -s "${INITDB_CALLS}" ] \
&& fail "${_case}: initdb ran on a handoff that could not be read" \
|| pass "${_case}: refused before initdb"
done
# --- Case 12a: an octet-shaped address that is not one is refused ---
# 10.30.1.256 and 010.30.1.15 both parse as addresses to a permissive check, and
# the second binds a different host to anything reading the leading zero as octal.
for _addr in 10.30.1.256 010.30.1.15 10.30.1 10.30.1.15.1; do
reset_state
LISTEN_ADDRESS="${_addr}"
write_handoff initialize 's3cr3t' appdb
run_fails "listen-${_addr}"
done
# --- Case 13: the bind address must be on an interface before the engine starts ---
# An engine asked to bind an address that is not there exits with `could not
# bind`, which is the worst way to learn that DHCP lost its race.
reset_state
IP_ADDR_LIST="10.99.0.7/24"
write_handoff initialize 's3cr3t' appdb
run_fails "bind-wait"
grep -q "${LISTEN_ADDRESS_DEFAULT}" "${WORK}/out" \
&& pass "bind-wait: the refusal names the address" || fail "bind-wait: the refusal does not name the address"
[ -s "${INITDB_CALLS}" ] \
&& fail "bind-wait: initdb ran before the address existed" \
|| pass "bind-wait: refused before initdb"
# --- Case 13a: a restored volume takes the destination VPC's scope ---
# A restore attaches a data volume carrying the source instance's pg_hba into a
# possibly different VPC. Regenerating the whole file every boot is what stops
# the restored instance locking out its own VPC's clients.
reset_state
write_handoff initialize 's3cr3t' appdb
write_parameters
if run_ok "restore-scope-setup"; then
CLIENT_CIDR="172.31.0.0/16"
write_handoff attach '' appdb
write_parameters
if run_ok "restore-scope"; then
grep -q 'host all all 172.31.0.0/16 scram-sha-256' "${PGDATA}/pg_hba.conf" \
&& pass "restore-scope: the destination VPC's range is admitted" \
|| fail "restore-scope: the restored instance did not take its own VPC's scope"
grep -q "${CLIENT_CIDR_DEFAULT}" "${PGDATA}/pg_hba.conf" \
&& fail "restore-scope: the source VPC's range survived the restore" \
|| pass "restore-scope: the source VPC's range is gone"
fi
fi
if [ "${FAILS}" -eq 0 ]; then
echo "PASS: all rds-init cases"
exit 0