From 72462105f568914a603aa223cc1dfc3277cc7c15 Mon Sep 17 00:00:00 2001 From: latte Date: Wed, 14 Oct 2026 16:11:46 +0200 Subject: [PATCH] fix: keep the harness admin password out of the docker exec argv --- tests/integration/harness.py | 33 +++++++++++++++++++------------ tests/integration/test_harness.py | 24 ++++++++++++++++++++++ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/tests/integration/harness.py b/tests/integration/harness.py index c0cae5e..1ef14e7 100644 --- a/tests/integration/harness.py +++ b/tests/integration/harness.py @@ -38,6 +38,7 @@ NAME_PREFIX = "aegis-it-" GITEA_PORT = 3000 ADMIN_LOGIN = "aegis-admin" ADMIN_EMAIL = "aegis-admin@example.test" +ADMIN_PASSWORD_ENV = "GITEA_ADMIN_PASSWORD" READY_TIMEOUT_SECONDS = 90.0 NETWORK_MODE_ENV = "AEGIS_IT_NETWORK_MODE" HTTP_TIMEOUT_SECONDS = 30.0 @@ -53,12 +54,16 @@ class HarnessError(RuntimeError): def run_docker( - *args: str, check: bool = True, timeout: float = 120.0 + *args: str, + check: bool = True, + timeout: float = 120.0, + env: Mapping[str, str] | None = None, ) -> subprocess.CompletedProcess[str]: """Run ``docker `` and return the completed process. - On failure the error names only the first two arguments (never the full - argv, which may carry a password) plus the command's stderr. + Secrets must not go into ``args`` (argv is visible to every local user): pass them + in ``env`` and forward them with ``docker exec -e NAME``. On failure the error names + only the first two arguments plus the command's stderr. """ try: proc = subprocess.run( @@ -67,6 +72,7 @@ def run_docker( text=True, timeout=timeout, check=False, + env={**os.environ, **env} if env else None, ) except (FileNotFoundError, PermissionError) as exc: raise HarnessError("the 'docker' CLI was not found or is not executable") from exc @@ -480,23 +486,24 @@ def start_gitea() -> GiteaInstance: res.attached_container = job_container _wait_ready(base_url, res.container) admin_password = secrets.token_urlsafe(24) + # The password travels in the environment of the docker CLI process and is forwarded + # with ``-e NAME`` (no value), so it never appears in any argv on the host. Inside + # the container it is read from the shell environment, not from the command line. run_docker( "exec", + "-e", + ADMIN_PASSWORD_ENV, "-u", "git", res.container, - "gitea", - "admin", - "user", - "create", - "--admin", - "--username", + "sh", + "-c", + 'exec gitea admin user create --admin --username "$1" --email "$2" ' + f'--password "${ADMIN_PASSWORD_ENV}" --must-change-password=false', + "sh", ADMIN_LOGIN, - "--password", - admin_password, - "--email", ADMIN_EMAIL, - "--must-change-password=false", + env={ADMIN_PASSWORD_ENV: admin_password}, ) instance = GiteaInstance( base_url=base_url, diff --git a/tests/integration/test_harness.py b/tests/integration/test_harness.py index 3c6bfc3..7635842 100644 --- a/tests/integration/test_harness.py +++ b/tests/integration/test_harness.py @@ -157,3 +157,27 @@ def test_container_id_candidates_fall_back_to_proc(tmp_path: Path) -> None: def test_container_id_candidates_tolerate_missing_files(tmp_path: Path) -> None: assert harness.container_id_candidates({}, proc_root=tmp_path) == [] + + +def test_run_docker_passes_secrets_through_the_environment_not_argv( + monkeypatch: pytest.MonkeyPatch, +) -> None: + seen: dict[str, object] = {} + + def fake_run(argv: list[str], **kwargs: object) -> object: + seen["argv"] = argv + seen["env"] = kwargs.get("env") + + class Done: + returncode = 0 + stdout = "" + stderr = "" + + return Done() + + monkeypatch.setattr(harness.subprocess, "run", fake_run) + harness.run_docker("exec", "-e", "SECRET_VAR", "box", env={"SECRET_VAR": "s3cret-value"}) + + assert "s3cret-value" not in " ".join(seen["argv"]) # type: ignore[call-overload] + assert seen["env"]["SECRET_VAR"] == "s3cret-value" # type: ignore[index] + assert "PATH" in seen["env"] # type: ignore[operator]