feat(svc-db-postgres): robust superuser password rotation via script with explicit auth verification

- replace Python helper with Bash script executed via ansible.script
- wait for postgres readiness inside container (pg_isready)
- explicitly verify password auth via temporary pg_hba override
- distinguish CHANGED / UNCHANGED / ALREADY OK states
- map script result deterministically to Ansible changed/failed logic
- remove legacy postgres_set_superuser_password.py

https://chatgpt.com/share/69754a74-9d18-800f-89a6-304b083fbe50
This commit is contained in:
kevinveenbirkenbach committed 2026-01-24 23:40:47 +01:00
1 parent 9b567a2f0e
commit 383a59e9a1
3 files changed
+107 -110

No files matched your search

@@ -0,0 +1,87 @@
#!/usr/bin/env bash
set -euo pipefail
# Usage:
# tmp.sh <container_name> <new_postgres_password>
container="${1:-}"
new_pw="${2:-}"
if [[ -z "$container" || -z "$new_pw" ]]; then
echo "Usage: $0 <container_name> <new_postgres_password>" >&2
exit 2
fi
# Wait until PostgreSQL is ready (socket)
for i in $(seq 1 60); do
if docker exec "$container" bash -lc 'pg_isready -U postgres >/dev/null 2>&1'; then
break
fi
sleep 1
done
docker exec "$container" bash -lc 'pg_isready -U postgres'
# Locate pg_hba.conf
hba_file="$(docker exec "$container" bash -lc "psql -U postgres -d postgres -Atc 'SHOW hba_file;'")"
if [[ -z "$hba_file" ]]; then
echo "[FAIL] Could not determine hba_file" >&2
exit 1
fi
backup="${hba_file}.bak_ansible_pwcheck_$$"
# Backup hba
docker exec "$container" bash -lc "cp -a \"$hba_file\" \"$backup\""
restore_hba() {
# Restore on exit (best effort)
docker exec "$container" bash -lc "if [ -f \"$backup\" ]; then cp -a \"$backup\" \"$hba_file\" && rm -f \"$backup\"; fi" >/dev/null 2>&1 || true
docker exec "$container" bash -lc "psql -U postgres -d postgres -Atc 'SELECT pg_reload_conf();' >/dev/null 2>&1" || true
}
trap restore_hba EXIT
# Force password auth for local TCP (127.0.0.1)
# Prepend a strict rule so it matches first.
docker exec "$container" bash -lc "{
echo 'host all all 127.0.0.1/32 scram-sha-256'
cat \"$backup\"
} > \"$hba_file\""
# Reload config
docker exec "$container" bash -lc "psql -U postgres -d postgres -Atc 'SELECT pg_reload_conf();' >/dev/null"
# Helper: test password auth explicitly over TCP
auth_test() {
docker exec -e PGPASSWORD="$new_pw" "$container" bash -lc \
'psql -h 127.0.0.1 -p 5432 -U postgres -d postgres -Atc "SELECT 1" >/dev/null 2>&1'
}
pre_ok=0
if auth_test; then
pre_ok=1
fi
# Set password (socket, no auth dependency)
docker exec -e NEW_POSTGRES_PASSWORD="$new_pw" "$container" bash -lc '
psql -U postgres -d postgres -v ON_ERROR_STOP=1 -v password="$NEW_POSTGRES_PASSWORD" <<'"'"'SQL'"'"'
ALTER USER postgres WITH PASSWORD :'"'"'password'"'"';
SQL
'
post_ok=0
if auth_test; then
post_ok=1
fi
if [[ "$pre_ok" -eq 0 && "$post_ok" -eq 1 ]]; then
echo "[CHANGED] New password did not work before, and works now."
exit 0
fi
if [[ "$pre_ok" -eq 1 && "$post_ok" -eq 1 ]]; then
echo "[UNCHANGED] New password already worked before."
exit 0
fi
echo "[FAIL] Password auth test failed even after ALTER USER." >&2
exit 1
@@ -1,99 +0,0 @@
#!/usr/bin/env python3
"""
Set the PostgreSQL superuser ('postgres') password inside a Docker container.
Exit codes:
0 = no change (password already valid)
2 = changed (password was updated)
1 = error
"""
from __future__ import annotations
import argparse
import subprocess
import sys
from typing import List
def run(cmd: List[str], env: dict | None = None, input_text: str | None = None):
return subprocess.run(
cmd,
input=input_text,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
check=False,
)
def password_works(container: str, user: str, db: str, password: str) -> bool:
cmd = [
"docker",
"exec",
"-e",
f"PGPASSWORD={password}",
container,
"psql",
"-U",
user,
"-d",
db,
"-c",
"SELECT 1",
]
return run(cmd).returncode == 0
def set_password(container: str, user: str, db: str, password: str) -> None:
sql = "ALTER USER postgres WITH PASSWORD :'new_password';\n"
cmd = [
"docker",
"exec",
"-i",
container,
"psql",
"-U",
user,
"-d",
db,
"-v",
"ON_ERROR_STOP=1",
"-v",
f"new_password={password}",
]
proc = run(cmd, input_text=sql)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip())
def main() -> int:
parser = argparse.ArgumentParser(
description="Ensure postgres superuser password inside a Docker container."
)
parser.add_argument("--container", required=True)
parser.add_argument("--user", default="postgres")
parser.add_argument("--db", default="postgres")
parser.add_argument("--password", required=True)
args = parser.parse_args()
try:
if password_works(args.container, args.user, args.db, args.password):
print("postgres password already valid")
return 0
set_password(args.container, args.user, args.db, args.password)
print("postgres password updated")
return 2
except Exception as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
+20 -11
View File
@@ -20,17 +20,26 @@
package_name: 'psycopg2-binary'
global_install: true
- name: "POSTGRES | Ensure postgres superuser password"
ansible.builtin.script:
cmd: >
scripts/postgres_set_superuser_password.py
--container {{ POSTGRES_CONTAINER }}
--user postgres
--db postgres
--password {{ POSTGRES_PASSWORD }}
register: pg_set_pwd
- name: "POSTGRES | Wait until postgres is ready (inside container)"
ansible.builtin.shell: >
docker exec {{ POSTGRES_CONTAINER }} pg_isready -U postgres
register: postgres_ready
until: postgres_ready.rc == 0
retries: 30
delay: 2
changed_when: false
- name: "POSTGRES | Ensure postgres superuser password matches inventory"
ansible.builtin.script: >
set_postgres_superuser_password.sh
{{ POSTGRES_CONTAINER | quote }}
{{ POSTGRES_PASSWORD | quote }}
register: postgres_pw_result
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
changed_when: pg_set_pwd.rc == 2
failed_when: pg_set_pwd.rc not in [0, 2]
changed_when: >
postgres_pw_result.stdout is search('\\[CHANGED\\]')
failed_when: >
postgres_pw_result.rc != 0 or
postgres_pw_result.stdout is search('\\[FAIL\\]')
- include_tasks: utils/once/flag.yml