#!/usr/bin/env bash # Rotate PostgreSQL password on a running Docker Compose stack (existing volume safe). # # The Postgres image only applies POSTGRES_PASSWORD on first init; for existing data # you must ALTER USER inside the running database, then update .env and restart backend. # # Usage (on server in repo root, with backup/snapshot taken): # ./scripts/rotate-postgres-password.sh # ./scripts/rotate-postgres-password.sh --app-user daagbok # optional: dedicated app role # # Writes the new credentials once to .postgres-credentials. (mode 600). set -euo pipefail ENV_FILE="${ENV_FILE:-.env}" COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" DB_CONTAINER="${DB_CONTAINER:-daagbox-prod-db}" BACKEND_CONTAINER="${BACKEND_CONTAINER:-daagbox-prod-backend}" CREATE_APP_USER="" APP_USER_NAME="daagbok" while [ $# -gt 0 ]; do case "$1" in --app-user) CREATE_APP_USER=1 APP_USER_NAME="${2:-daagbok}" shift 2 ;; -h|--help) sed -n '2,12p' "$0" exit 0 ;; *) echo "Unknown option: $1" >&2 exit 1 ;; esac done if [ ! -f "$ENV_FILE" ]; then echo "Error: $ENV_FILE not found" >&2 exit 1 fi # shellcheck disable=SC1090 set -a source "$ENV_FILE" set +a POSTGRES_USER="${POSTGRES_USER:-postgres}" POSTGRES_DB="${POSTGRES_DB:-daagbox}" OLD_PASSWORD="${POSTGRES_PASSWORD:-}" if [ -z "$OLD_PASSWORD" ]; then echo "Error: POSTGRES_PASSWORD not set in $ENV_FILE" >&2 exit 1 fi NEW_PASSWORD="$(openssl rand -hex 24)" NEW_APP_PASSWORD="" if [ -n "$CREATE_APP_USER" ]; then NEW_APP_PASSWORD="$(openssl rand -hex 24)" fi BACKUP_ENV="${ENV_FILE}.bak.pg-rotate.$(date +%Y%m%d-%H%M%S)" cp "$ENV_FILE" "$BACKUP_ENV" echo "Backed up $ENV_FILE → $BACKUP_ENV" echo "Rotating password for PostgreSQL role: $POSTGRES_USER (database: $POSTGRES_DB)" # Escape single quotes for SQL string literals sql_escape() { printf "%s" "$1" | sed "s/'/''/g" } NEW_PW_SQL="$(sql_escape "$NEW_PASSWORD")" export PGPASSWORD="$OLD_PASSWORD" if ! docker exec -e PGPASSWORD="$OLD_PASSWORD" "$DB_CONTAINER" \ psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 \ -c "ALTER USER \"${POSTGRES_USER}\" WITH PASSWORD '${NEW_PW_SQL}';" >/dev/null; then echo "Error: ALTER USER failed. Is POSTGRES_PASSWORD in .env still correct?" >&2 exit 1 fi unset PGPASSWORD TARGET_USER="$POSTGRES_USER" TARGET_PASSWORD="$NEW_PASSWORD" if [ -n "$CREATE_APP_USER" ]; then APP_PW_SQL="$(sql_escape "$NEW_APP_PASSWORD")" export PGPASSWORD="$NEW_PASSWORD" docker exec -e PGPASSWORD="$NEW_PASSWORD" "$DB_CONTAINER" psql -U postgres -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 < str: pattern = rf"^{re.escape(name)}=.*$" line = f"{name}={value}" if re.search(pattern, content, flags=re.M): return re.sub(pattern, line, content, count=1, flags=re.M) return content.rstrip() + "\n" + line + "\n" text = set_var("POSTGRES_USER", target_user, text) text = set_var("POSTGRES_PASSWORD", target_password, text) text = set_var("POSTGRES_DB", "daagbox", text) if "POSTGRES_DB=" not in text else text if use_app_user: text = set_var("POSTGRES_ADMIN_PASSWORD", postgres_password, text) path.write_text(text, encoding="utf-8") PY CREDS_FILE=".postgres-credentials.$(date +%Y%m%d-%H%M%S)" umask 077 { echo "# Generated $(date -Iseconds) — store in password manager, then delete this file." echo "POSTGRES_USER=$TARGET_USER" echo "POSTGRES_PASSWORD=$TARGET_PASSWORD" echo "POSTGRES_DB=$POSTGRES_DB" if [ -n "$CREATE_APP_USER" ]; then echo "POSTGRES_ADMIN_USER=postgres" echo "POSTGRES_ADMIN_PASSWORD=$NEW_PASSWORD" fi } > "$CREDS_FILE" chmod 600 "$CREDS_FILE" echo "Credentials written to $CREDS_FILE (chmod 600)" echo "Recreating backend (and db if compose env changed) to pick up DATABASE_URL..." docker compose -f "$COMPOSE_FILE" up -d --force-recreate backend echo "Waiting for backend health..." for _ in $(seq 1 45); do status="$(docker inspect --format='{{.State.Health.Status}}' "$BACKEND_CONTAINER" 2>/dev/null || echo missing)" if [ "$status" = healthy ]; then break fi sleep 1 done export PGPASSWORD="$TARGET_PASSWORD" docker exec -e PGPASSWORD="$TARGET_PASSWORD" "$DB_CONTAINER" \ psql -U "$TARGET_USER" -d "$POSTGRES_DB" -tAc 'SELECT count(*) FROM "User";' >/dev/null unset PGPASSWORD if curl -sf http://127.0.0.1/api/health | grep -q '"status":"ok"'; then echo "OK: /api/health and DB connection verified." else echo "Warning: health check failed — see: docker compose logs backend" >&2 exit 1 fi echo "Done. Remove $CREDS_FILE after saving credentials securely."