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:
1 parent
d672388f63
commit
328520169b
15 files changed
+320
-67
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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(
|
||||
[
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user