fix(security): keep database passwords out of every argv

MariaDB dumps and restores read the password from MYSQL_PWD, and docker exec forwards env variables by name only, so no process listing shows a password. baudolo-restore gains --db-password-stdin; --db-password still works but warns.

🎯 Fix: 96 %
💥 Break: 3 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: ~100 %
🏆 Score: 93
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-11-05 18:37:04 +00:00
1 parent d672388f63
commit 328520169b
15 files changed
+320 -67

No files matched your search

+11
View File
@@ -13,7 +13,17 @@ def run(
capture: bool = True,
check: bool = True,
cwd: str | None = None,
stdin_text: str | None = None,
) -> subprocess.CompletedProcess:
"""
Args:
cmd: argv, the program first.
capture: keep stdout and stderr on the result.
check: raise on a non-zero exit.
cwd: working directory.
stdin_text: text fed to the command's stdin, e.g. a password for
``--db-password-stdin``.
"""
try:
return subprocess.run(
cmd,
@@ -21,6 +31,7 @@ def run(
cwd=cwd,
text=True,
capture_output=capture,
input=stdin_text,
)
except subprocess.CalledProcessError as e:
# Print captured output so failing E2E tests are "live" / debuggable in CI logs
+3 -3
View File
@@ -137,10 +137,10 @@ class TestE2EMariaDBFull(unittest.TestCase):
cls.db_name,
"--db-user",
cls.db_user,
"--db-password",
cls.db_password,
"--db-password-stdin",
"--empty",
]
],
stdin_text=cls.db_password,
)
@classmethod
+3 -25
View File
@@ -89,33 +89,12 @@ class TestE2EPostgresClusterRestore(unittest.TestCase):
/ f"{cls.pg_container}.cluster.backup.sql"
)
run(
[
"baudolo-restore",
"cluster",
cls.pg_volume,
cls.hash,
cls.version,
"--backups-dir",
cls.backups_dir,
"--repo-name",
cls.repo_name,
"--container",
cls.pg_container,
"--instance",
cls.pg_container,
"--db-user",
"postgres",
"--db-password",
"pgpw",
"--empty",
]
)
run(cls._restore_argv(), stdin_text="pgpw")
for statement in SIBLING_SQL:
cls._psql("postgres", statement)
cls._psql("sibling", SIBLING_PAYLOAD)
cls.refused = run(cls._restore_argv(), check=False)
cls.refused = run(cls._restore_argv(), check=False, stdin_text="pgpw")
@classmethod
def _restore_argv(cls) -> list:
@@ -135,8 +114,7 @@ class TestE2EPostgresClusterRestore(unittest.TestCase):
cls.pg_container,
"--db-user",
"postgres",
"--db-password",
"pgpw",
"--db-password-stdin",
"--empty",
]
+4 -1
View File
@@ -94,7 +94,7 @@ class TestE2EPostgresFull(unittest.TestCase):
)
# Restore
run(
cls.restored = run(
[
"baudolo-restore",
"postgres",
@@ -129,6 +129,9 @@ class TestE2EPostgresFull(unittest.TestCase):
)
self.assertTrue(p.is_file(), f"Expected dump file at: {p}")
def test_the_deprecated_password_argument_still_restores_but_warns(self) -> None:
self.assertIn("visible in the process list", self.restored.stderr)
def test_data_restored(self) -> None:
p = run(
[
+2 -2
View File
@@ -73,12 +73,12 @@ class GateCase:
cls.db_name,
"--db-user",
cls.db_user,
"--db-password",
cls.db_password,
"--db-password-stdin",
"--empty",
*extra,
],
check=False,
stdin_text=cls.db_password,
)
@classmethod