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
@@ -0,0 +1,65 @@
|
||||
import contextlib
|
||||
import io
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from baudolo.restore import __main__ as cli
|
||||
|
||||
BASE = [
|
||||
"postgres",
|
||||
"app_vol",
|
||||
"hash",
|
||||
"20260817000000",
|
||||
"--repo-name",
|
||||
"repo",
|
||||
"--container",
|
||||
"db",
|
||||
"--db-name",
|
||||
"app",
|
||||
]
|
||||
|
||||
|
||||
class TestRestorePasswordChannel(unittest.TestCase):
|
||||
def call(self, extra, stdin=""):
|
||||
stderr = io.StringIO()
|
||||
with (
|
||||
patch.object(cli, "restore_postgres_sql") as restore,
|
||||
patch("sys.stdin", io.StringIO(stdin)),
|
||||
contextlib.redirect_stderr(stderr),
|
||||
):
|
||||
code = cli.main([*BASE, *extra])
|
||||
return code, restore, stderr.getvalue()
|
||||
|
||||
def test_stdin_carries_the_password_without_its_newline(self):
|
||||
code, restore, stderr = self.call(["--db-password-stdin"], stdin="s3cret\n")
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(restore.call_args.kwargs["password"], "s3cret")
|
||||
self.assertNotIn("WARNING", stderr)
|
||||
|
||||
def test_the_argument_still_works_but_warns(self):
|
||||
code, restore, stderr = self.call(["--db-password", "s3cret"])
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(restore.call_args.kwargs["password"], "s3cret")
|
||||
self.assertIn("visible in the process list", stderr)
|
||||
|
||||
def test_both_ways_is_a_usage_error(self):
|
||||
with self.assertRaises(SystemExit) as caught:
|
||||
self.call(["--db-password", "x", "--db-password-stdin"], stdin="y")
|
||||
self.assertEqual(caught.exception.code, 2)
|
||||
|
||||
def test_neither_way_is_a_usage_error(self):
|
||||
with self.assertRaises(SystemExit) as caught:
|
||||
self.call([])
|
||||
self.assertEqual(caught.exception.code, 2)
|
||||
|
||||
def test_files_needs_no_password(self):
|
||||
with patch.object(cli, "restore_volume_files", return_value=0) as restore:
|
||||
code = cli.main(
|
||||
["files", "app_vol", "hash", "20260817000000", "--repo-name", "repo"]
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
restore.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user