#5037 "Secrets at rest and in run output: webhook URL and git token encrypted, askpass via env, secret values scrubbed from logs". - webhook.url joins SECRET_KEYS; existing values are sealed at startup by migrate_plaintext_secrets. Settings -> Notifications no longer echoes the URL: it names the saved host, keeps it when left blank, and has a Remove box. - Git source http_token is sealed inside ansible.sources on save and at startup, opened in get_sources, and counted by the undecryptable-secrets scan as ansible.sources[<name>].http_token. - The askpass script holds no token: git runs it with STEWARD_GIT_TOKEN in its environment and printf prints it as data. - The executor masks every secret the run is handed (become/vault passwords, bootstrap passwords, sensitive run vars, and the per-host agent tokens the host_agent plugin now passes as redact) in each line before it is broadcast, logged or stored. - Bundled "Write agent config" tasks are no_log; a test fails if any bundled task templating steward_token lacks it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
58 lines
2.4 KiB
Python
58 lines
2.4 KiB
Python
"""A run's secrets never reach its output: each line is masked before it is
|
|
broadcast, written to the log artifact or stored on the run."""
|
|
from steward.ansible.executor import MASK, run_secrets, scrub
|
|
|
|
|
|
def test_run_secrets_gathers_everything_the_run_is_handed():
|
|
secrets = run_secrets(
|
|
{"become_password": "become-pw", "vault_password": "vault-pw", "ssh_user": "steward"},
|
|
{"user": "root", "password": "boot-pw"},
|
|
{"db": {"password": "nested-pw"}, "api_keys": ["key-one"], "port": 5432},
|
|
["agent-token-abc"],
|
|
)
|
|
assert set(secrets) == {"become-pw", "vault-pw", "boot-pw", "nested-pw", "key-one",
|
|
"agent-token-abc"}
|
|
assert "steward" not in secrets and "root" not in secrets
|
|
|
|
|
|
def test_short_values_are_not_masked():
|
|
assert run_secrets(None, None, {"flag": "on", "n": "1"}, None) == []
|
|
|
|
|
|
def test_scrub_masks_every_occurrence_longest_first():
|
|
secrets = run_secrets(None, None, {"a": "hunter2", "b": "hunter2-extended"}, None)
|
|
line = 'fatal: [h]: FAILED! => {"msg": "hunter2-extended / hunter2"}'
|
|
assert scrub(line, secrets) == f'fatal: [h]: FAILED! => {{"msg": "{MASK} / {MASK}"}}'
|
|
|
|
|
|
def test_scrub_leaves_clean_lines_alone():
|
|
assert scrub("ok: [web1]", ["agent-token-abc"]) == "ok: [web1]"
|
|
|
|
|
|
def test_bundled_tasks_that_template_the_agent_token_are_no_log():
|
|
"""Every bundled task whose args mention steward_token sets no_log
|
|
(assert-style checks excepted: they print a fixed fail_msg)."""
|
|
import pathlib
|
|
import yaml
|
|
import steward.ansible
|
|
|
|
def missing(plays) -> list[str]:
|
|
out = []
|
|
for play in plays or []:
|
|
for task in (play.get("tasks") or []) + (play.get("handlers") or []):
|
|
body = {k: v for k, v in task.items() if k not in ("name", "no_log")}
|
|
if "ansible.builtin.assert" in body or "assert" in body:
|
|
continue
|
|
if "steward_token" in yaml.safe_dump(body) and not task.get("no_log"):
|
|
out.append(task.get("name", "?"))
|
|
return out
|
|
|
|
# The check can fail.
|
|
assert missing([{"tasks": [{"name": "leaky", "copy": {"content": "{{ steward_token }}"}}]}]) == ["leaky"]
|
|
|
|
bundled = pathlib.Path(steward.ansible.__file__).parent / "bundled"
|
|
files = sorted(bundled.rglob("*.yml"))
|
|
assert files
|
|
leaks = [f"{f.name}: {t}" for f in files for t in missing(yaml.safe_load(f.read_text()))]
|
|
assert not leaks, leaks
|