feat(secrets): webhook URL and git tokens sealed; run output masks its secrets (#5037)
#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:
@@ -11,7 +11,7 @@ from steward.core.settings import (
|
||||
get_all_settings, set_setting, delete_setting,
|
||||
get_stored_plugin_names, find_orphaned_plugins,
|
||||
to_smtp_cfg, to_webhook_cfg, to_ansible_cfg, to_plugins_cfg,
|
||||
to_oidc_cfg, to_ldap_cfg, to_thresholds_cfg, to_access_cfg, DEFAULTS,
|
||||
to_oidc_cfg, to_ldap_cfg, to_thresholds_cfg, to_access_cfg, DEFAULTS, seal_source_tokens,
|
||||
)
|
||||
|
||||
settings_bp = Blueprint("settings", __name__, url_prefix="/settings")
|
||||
@@ -208,7 +208,10 @@ async def save_thresholds():
|
||||
async def notifications():
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
settings = await get_all_settings(db)
|
||||
return await render_template("settings/notifications.html", settings=settings)
|
||||
from urllib.parse import urlsplit
|
||||
webhook_host = urlsplit(settings.get("webhook.url") or "").hostname or ""
|
||||
return await render_template("settings/notifications.html", settings=settings,
|
||||
webhook_host=webhook_host)
|
||||
|
||||
|
||||
@settings_bp.post("/notifications/")
|
||||
@@ -226,7 +229,10 @@ async def save_notifications():
|
||||
if form.get("smtp.password"):
|
||||
await set_setting(db, "smtp.password", form.get("smtp.password"))
|
||||
await set_setting(db, "smtp.recipients", recipients)
|
||||
await set_setting(db, "webhook.url", form.get("webhook.url", ""))
|
||||
if "webhook.remove" in form:
|
||||
await set_setting(db, "webhook.url", "")
|
||||
elif form.get("webhook.url", "").strip():
|
||||
await set_setting(db, "webhook.url", form.get("webhook.url").strip())
|
||||
await set_setting(db, "webhook.template", form.get("webhook.template", ""))
|
||||
await _reload_app_config()
|
||||
await log_audit(current_app, session.get("user_id"), session.get("username", ""),
|
||||
@@ -246,7 +252,7 @@ async def _get_ansible_sources() -> list[dict]:
|
||||
async def _save_ansible_sources(sources: list[dict]) -> None:
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
async with db.begin():
|
||||
await set_setting(db, "ansible.sources", sources)
|
||||
await set_setting(db, "ansible.sources", seal_source_tokens(sources)[0])
|
||||
await _reload_app_config()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user