feat(secrets): webhook URL and git tokens sealed; run output masks its secrets (#5037)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 47s
CI / unit (push) Successful in 1m5s
CI / publish (push) Successful in 54s

#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>
This commit is contained in:
2026-10-06 11:14:37 -04:00
co-authored by Claude Opus 5.5
parent ddf13110e9
commit faa605cead
11 changed files with 252 additions and 35 deletions
+4 -2
View File
@@ -28,6 +28,7 @@ async def trigger_run(
inventory_content: str | None = None,
connection: dict | None = None,
secret_vars: dict | None = None,
redact: list[str] | None = None,
):
"""Resolve inventory for the scope, persist an AnsibleRun, and launch it.
@@ -39,7 +40,8 @@ async def trigger_run(
first-contact provisioning — passed to the executor but deliberately NOT
stored on the AnsibleRun row (params), so the password never lands in the DB.
secret_vars (sensitive run-time playbook variables) are likewise passed
through to the executor but never persisted.
through to the executor but never persisted. redact names further values
to mask in the run's output (a bespoke inventory's per-host tokens).
Returns (run, source, error): on success error is None; on failure run is
None and error is a short human-readable reason.
"""
@@ -85,7 +87,7 @@ async def trigger_run(
executor.start_run(
app, run_id, playbook_path, inventory_path or "",
source["path"], params or None, inventory_content,
connection=connection, secret_vars=secret_vars,
connection=connection, secret_vars=secret_vars, redact=redact,
)
)
task.add_done_callback(