4ef56aeb8f
Co-authored-by: Cursor <cursoragent@cursor.com>
184 lines
5.9 KiB
Bash
Executable File
184 lines
5.9 KiB
Bash
Executable File
#!/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.<timestamp> (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 <<SQL
|
|
DO \$\$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${APP_USER_NAME}') THEN
|
|
CREATE ROLE ${APP_USER_NAME} LOGIN PASSWORD '${APP_PW_SQL}';
|
|
ELSE
|
|
ALTER ROLE ${APP_USER_NAME} WITH LOGIN PASSWORD '${APP_PW_SQL}';
|
|
END IF;
|
|
END
|
|
\$\$;
|
|
GRANT CONNECT ON DATABASE ${POSTGRES_DB} TO ${APP_USER_NAME};
|
|
GRANT USAGE, CREATE ON SCHEMA public TO ${APP_USER_NAME};
|
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${APP_USER_NAME};
|
|
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${APP_USER_NAME};
|
|
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO ${APP_USER_NAME};
|
|
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO ${APP_USER_NAME};
|
|
SQL
|
|
unset PGPASSWORD
|
|
TARGET_USER="$APP_USER_NAME"
|
|
TARGET_PASSWORD="$NEW_APP_PASSWORD"
|
|
echo "Created/updated application role: $APP_USER_NAME (postgres superuser password also rotated)"
|
|
fi
|
|
|
|
# Update .env without exposing values in process list longer than necessary
|
|
python3 - "$ENV_FILE" "$TARGET_USER" "$TARGET_PASSWORD" "$NEW_PASSWORD" "$CREATE_APP_USER" <<'PY'
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
path = Path(sys.argv[1])
|
|
target_user = sys.argv[2]
|
|
target_password = sys.argv[3]
|
|
postgres_password = sys.argv[4]
|
|
use_app_user = sys.argv[5] == "1"
|
|
|
|
text = path.read_text(encoding="utf-8")
|
|
|
|
def set_var(name: str, value: str, content: str) -> 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."
|