mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
fix: make the DB endpoint the only path to the engine (#830)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user