diff --git a/.superpowers/sdd/task-7-report.md b/.superpowers/sdd/task-7-report.md index a25de52c..ca384282 100644 --- a/.superpowers/sdd/task-7-report.md +++ b/.superpowers/sdd/task-7-report.md @@ -66,3 +66,14 @@ An operator must still choose the three reviewed legacy IDs, materialize real ru secrets, deploy Task 4 and Task 5 together in a maintenance window, apply the one-shot migrator, and run the documented authenticated smoke. The guarded helper has not been invoked with `--delete`. + +## P1 correction — migrator TLS validation + +The original migrator Compose command interpolated `THT_SESSION_DB_SSLMODE` into its URL without +checking it. `docker/session-migrate.sh` now rejects every value except `verify-ca` and +`verify-full` before reading the password file or building that URL; the Compose service invokes +this helper. `docker/session-migrate.test.sh` first established RED because the helper did not +exist, then verified that `prefer` is rejected before `tht` can run and that `verify-full` reaches +a fake `tht` binary with the expected TLS URL. The helper and test pass `bash -n`; the focused +backend config/health suite remains green, and the base-plus-overlay Compose configuration renders +with temporary empty secret files. diff --git a/README.md b/README.md index cb3b79d9..7d23e7fa 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,8 @@ The overlay mounts the runtime password at `/run/secrets/session_runtime_passwor It mounts `session_migrator_password` only to `session-migrate`. The backend refuses a server session store without upstream authentication, direct DB host/name/runtime user/password-file, `verify-ca` or `verify-full`, and an absolute CA path. +The migrator independently rejects every other TLS mode before reading its password secret or +constructing a database URL. Perform the cutover in one maintenance window, with the Task 4 portal proxy headers and Task 5 backend principal parser deployed together. Neither change is safe to deploy independently: Task diff --git a/deploy/compose.session-server.yaml.example b/deploy/compose.session-server.yaml.example index afae607c..fbb1bc4f 100644 --- a/deploy/compose.session-server.yaml.example +++ b/deploy/compose.session-server.yaml.example @@ -27,18 +27,15 @@ services: session-migrate: image: thothii-core:local profiles: [session-migrate] - entrypoint: [/bin/sh, -ec] - command: >- - export PGPASSWORD="$$(cat /run/secrets/session_migrator_password)"; - exec /opt/venv/bin/tht session migrate --database-url - "postgresql+psycopg2://$${THT_SESSION_MIGRATOR_USER}@$${THT_SESSION_DB_HOST}:$${THT_SESSION_DB_PORT}/$${THT_SESSION_DB_NAME}?sslmode=$${THT_SESSION_DB_SSLMODE}&sslrootcert=/run/secrets/session_ca.pem" - --json + entrypoint: [/app/docker/session-migrate.sh] environment: THT_SESSION_DB_HOST: ${THT_SESSION_DB_HOST:?set THT_SESSION_DB_HOST} THT_SESSION_DB_PORT: ${THT_SESSION_DB_PORT:-5432} THT_SESSION_DB_NAME: ${THT_SESSION_DB_NAME:?set THT_SESSION_DB_NAME} THT_SESSION_MIGRATOR_USER: ${THT_SESSION_MIGRATOR_USER:?set THT_SESSION_MIGRATOR_USER} + THT_SESSION_MIGRATOR_PASSWORD_FILE: /run/secrets/session_migrator_password THT_SESSION_DB_SSLMODE: ${THT_SESSION_DB_SSLMODE:-verify-full} + THT_SESSION_DB_SSLROOTCERT: /run/secrets/session_ca.pem secrets: - source: session_migrator_password target: session_migrator_password diff --git a/docker/core.Dockerfile b/docker/core.Dockerfile index 7abb1088..db75fe6d 100644 --- a/docker/core.Dockerfile +++ b/docker/core.Dockerfile @@ -63,8 +63,8 @@ ENV PATH="/opt/venv/bin:/usr/local/bin:$PATH" \ PI_BIN=pi \ HOME=/home/thoth -COPY docker/core-entrypoint.sh docker/ensure-pi-trust.mjs /app/docker/ -RUN chmod +x /app/docker/core-entrypoint.sh +COPY docker/core-entrypoint.sh docker/session-migrate.sh docker/ensure-pi-trust.mjs /app/docker/ +RUN chmod +x /app/docker/core-entrypoint.sh /app/docker/session-migrate.sh WORKDIR /app/backend USER thoth diff --git a/docker/session-migrate.sh b/docker/session-migrate.sh new file mode 100755 index 00000000..7e040ce7 --- /dev/null +++ b/docker/session-migrate.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# One-shot schema migration only. Validate TLS before reading a secret or composing a URL. +set -eu + +case "${THT_SESSION_DB_SSLMODE:-}" in + verify-ca|verify-full) ;; + *) + echo "THT_SESSION_DB_SSLMODE must be verify-ca or verify-full" >&2 + exit 2 + ;; +esac + +require_env() { + eval "value=\${$1:-}" + if [ -z "$value" ]; then + echo "missing required session migrator configuration" >&2 + exit 2 + fi +} + +require_env THT_SESSION_DB_HOST +require_env THT_SESSION_DB_PORT +require_env THT_SESSION_DB_NAME +require_env THT_SESSION_MIGRATOR_USER +require_env THT_SESSION_MIGRATOR_PASSWORD_FILE +require_env THT_SESSION_DB_SSLROOTCERT + +if [ ! -r "$THT_SESSION_MIGRATOR_PASSWORD_FILE" ]; then + echo "session migrator credential is unavailable" >&2 + exit 2 +fi +export PGPASSWORD="$(cat "$THT_SESSION_MIGRATOR_PASSWORD_FILE")" +if [ -z "$PGPASSWORD" ]; then + echo "session migrator credential is unavailable" >&2 + exit 2 +fi + +exec "${THT_BIN:-/opt/venv/bin/tht}" session migrate --database-url \ + "postgresql+psycopg2://${THT_SESSION_MIGRATOR_USER}@${THT_SESSION_DB_HOST}:${THT_SESSION_DB_PORT}/${THT_SESSION_DB_NAME}?sslmode=${THT_SESSION_DB_SSLMODE}&sslrootcert=${THT_SESSION_DB_SSLROOTCERT}" \ + --json diff --git a/docker/session-migrate.test.sh b/docker/session-migrate.test.sh new file mode 100755 index 00000000..1891d21f --- /dev/null +++ b/docker/session-migrate.test.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +root=$(cd "$(dirname "$0")/.." && pwd) +helper="$root/docker/session-migrate.sh" +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT HUP INT TERM + +if THT_SESSION_DB_SSLMODE=prefer sh "$helper" >"$tmp/invalid.out" 2>&1; then + echo "session migrator accepted an unverified TLS mode" >&2 + exit 1 +fi +grep -qx 'THT_SESSION_DB_SSLMODE must be verify-ca or verify-full' "$tmp/invalid.out" + +printf '%s' password >"$tmp/password" +cat >"$tmp/tht" <<'EOF' +#!/bin/sh +printf '%s\n' "$@" >"$ARGS_FILE" +EOF +chmod +x "$tmp/tht" + +ARGS_FILE="$tmp/args" \ +THT_BIN="$tmp/tht" \ +THT_SESSION_DB_HOST=sessions-db.internal \ +THT_SESSION_DB_PORT=5432 \ +THT_SESSION_DB_NAME=thoth \ +THT_SESSION_MIGRATOR_USER=thoth_sessions_migrate \ +THT_SESSION_MIGRATOR_PASSWORD_FILE="$tmp/password" \ +THT_SESSION_DB_SSLMODE=verify-full \ +THT_SESSION_DB_SSLROOTCERT=/run/secrets/session_ca.pem \ +sh "$helper" + +grep -qx 'session' "$tmp/args" +grep -qx 'migrate' "$tmp/args" +grep -qx -- '--database-url' "$tmp/args" +grep -Fxq 'postgresql+psycopg2://thoth_sessions_migrate@sessions-db.internal:5432/thoth?sslmode=verify-full&sslrootcert=/run/secrets/session_ca.pem' "$tmp/args" + +echo "session migrator TLS contract passed."