WARDEN-WP-0026 T02: safe access transports (no secret values on stdout)
- proxy.py: proxy_fetch_to_file (mode-0600 file), build_wrapped_fetch + proxy_fetch_wrapped (single-use OpenBao response-wrapping token), _capture_value helper, is_bao_kv_fetch. - warden access: --out FILE, --wrap [--wrap-ttl], --unsafe-stdout. Raw --fetch to a non-TTY stdout is refused (exit 6) — captured/piped output is the disclosure risk; sanctioned transports are --out / --exec / --wrap. - canon: anti-pattern (secret value onto captured stdout) + transport table in .claude/rules/credential-routing.md; OperatorAccessAssist.md examples + G2 updated. - tests: file/wrap/build + stdout-guard in tests/test_proxy.py. 293 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c749561b75
commit
359ca1bd0e
6 changed files with 281 additions and 11 deletions
|
|
@ -956,6 +956,10 @@ def _access_proxy(
|
|||
do_exec: bool,
|
||||
child_argv: list,
|
||||
no_policy: bool,
|
||||
out_path: Optional[str] = None,
|
||||
wrap: bool = False,
|
||||
wrap_ttl: str = "5m",
|
||||
unsafe_stdout: bool = False,
|
||||
) -> None:
|
||||
"""Proxy a non-SSH credential fetch as the caller (WP-0014 T3).
|
||||
|
||||
|
|
@ -965,9 +969,12 @@ def _access_proxy(
|
|||
"""
|
||||
from warden.proxy import (
|
||||
ProxyError,
|
||||
build_wrapped_fetch,
|
||||
caller_auth_present,
|
||||
proxy_exec,
|
||||
proxy_fetch,
|
||||
proxy_fetch_to_file,
|
||||
proxy_fetch_wrapped,
|
||||
resolve_fetch_command,
|
||||
write_audit,
|
||||
)
|
||||
|
|
@ -1035,11 +1042,38 @@ def _access_proxy(
|
|||
else:
|
||||
err.print("[yellow]Proxying ungated[/yellow] (--no-policy; gate not enforced).")
|
||||
|
||||
try:
|
||||
resolved = resolve_fetch_command(entry, domain=domain, field=field, path=path)
|
||||
except ProxyError as e:
|
||||
err.print(f"[red]{e}[/red]")
|
||||
raise typer.Exit(2)
|
||||
# Wrapping (WP-0026 T02) uses its own command shape; the value-bearing transports
|
||||
# share the resolved fetch command.
|
||||
if wrap and not is_login:
|
||||
try:
|
||||
resolved = build_wrapped_fetch(entry, path=path, ttl=wrap_ttl)
|
||||
except ProxyError as e:
|
||||
err.print(f"[red]{e}[/red]")
|
||||
raise typer.Exit(2)
|
||||
else:
|
||||
try:
|
||||
resolved = resolve_fetch_command(entry, domain=domain, field=field, path=path)
|
||||
except ProxyError as e:
|
||||
err.print(f"[red]{e}[/red]")
|
||||
raise typer.Exit(2)
|
||||
|
||||
# T02 — the sanctioned fetch transports (file / env / wrapping token) never put a
|
||||
# secret value on stdout. Streaming a value to stdout is the documented anti-pattern:
|
||||
# allowed only to an interactive terminal, and only with an explicit acknowledgment
|
||||
# when stdout is captured/piped (the logged-context disclosure risk).
|
||||
if not is_login and not do_exec and not wrap and not out_path:
|
||||
import sys as _sys
|
||||
|
||||
if not _sys.stdout.isatty() and not unsafe_stdout:
|
||||
err.print(
|
||||
"[red]Refusing to stream a secret value to a non-terminal stdout[/red] "
|
||||
"(captured/piped output is a disclosure risk). Use a sanctioned transport:\n"
|
||||
" --out FILE write the value to a mode-0600 file\n"
|
||||
" --exec -- CMD inject it into a child process env\n"
|
||||
" --wrap return a single-use OpenBao wrapping token to unwrap yourself\n"
|
||||
"Override only for an interactive human session: --unsafe-stdout."
|
||||
)
|
||||
raise typer.Exit(6)
|
||||
|
||||
action = "login" if is_login else ("exec" if do_exec else "fetch")
|
||||
err.print(
|
||||
|
|
@ -1052,6 +1086,18 @@ def _access_proxy(
|
|||
err.print("[red]--exec needs a command after `--`[/red], e.g. `-- npm publish`.")
|
||||
raise typer.Exit(2)
|
||||
rc = proxy_exec(resolved, env_var=field or "", child_argv=child_argv)
|
||||
elif wrap:
|
||||
token = proxy_fetch_wrapped(resolved)
|
||||
# The wrapping token is not the secret value — safe to hand back on stdout.
|
||||
print(token)
|
||||
err.print(
|
||||
f"[dim]wrapping token (single-use, ttl {wrap_ttl}) — unwrap in your own "
|
||||
f"context: [bold]bao unwrap {'<token>'}[/bold][/dim]"
|
||||
)
|
||||
rc = 0
|
||||
elif out_path:
|
||||
rc = proxy_fetch_to_file(resolved, Path(out_path))
|
||||
err.print(f"[dim]value written to {out_path} (mode 0600); not shown[/dim]")
|
||||
else:
|
||||
rc = proxy_fetch(resolved)
|
||||
except ProxyError as e:
|
||||
|
|
@ -1087,7 +1133,7 @@ def access(
|
|||
output_json: Annotated[bool, typer.Option("--json", help="Output JSON (stable, secret-free)")] = False,
|
||||
all_entries: Annotated[bool, typer.Option("--all", help="Include draft entries")] = False,
|
||||
do_fetch: Annotated[
|
||||
bool, typer.Option("--fetch", help="Proxy the fetch as the caller; value streams to stdout")
|
||||
bool, typer.Option("--fetch", help="Proxy the fetch as the caller (pair with --out/--wrap; raw stdout is guarded)")
|
||||
] = False,
|
||||
do_exec: Annotated[
|
||||
bool,
|
||||
|
|
@ -1099,6 +1145,21 @@ def access(
|
|||
path: Annotated[
|
||||
Optional[str], typer.Option("--path", help="Override the owner-side path template")
|
||||
] = None,
|
||||
out_path: Annotated[
|
||||
Optional[str],
|
||||
typer.Option("--out", help="Sanctioned transport: write the value to this mode-0600 file, not stdout"),
|
||||
] = None,
|
||||
wrap: Annotated[
|
||||
bool,
|
||||
typer.Option("--wrap", help="Sanctioned transport: return a single-use OpenBao wrapping token (bao unwrap)"),
|
||||
] = False,
|
||||
wrap_ttl: Annotated[
|
||||
str, typer.Option("--wrap-ttl", help="TTL for the --wrap response-wrapping token")
|
||||
] = "5m",
|
||||
unsafe_stdout: Annotated[
|
||||
bool,
|
||||
typer.Option("--unsafe-stdout", help="Acknowledge streaming a value to a captured/piped stdout (anti-pattern)"),
|
||||
] = False,
|
||||
no_policy: Annotated[
|
||||
bool,
|
||||
typer.Option("--no-policy", help="Acknowledge proxying when the flex-auth gate is not enforced"),
|
||||
|
|
@ -1134,7 +1195,7 @@ def access(
|
|||
|
||||
entry = matches[0]
|
||||
|
||||
if do_fetch or do_exec:
|
||||
if do_fetch or do_exec or out_path or wrap:
|
||||
_access_proxy(
|
||||
entry,
|
||||
domain=domain,
|
||||
|
|
@ -1143,6 +1204,10 @@ def access(
|
|||
do_exec=do_exec,
|
||||
child_argv=list(ctx.args),
|
||||
no_policy=no_policy,
|
||||
out_path=out_path,
|
||||
wrap=wrap,
|
||||
wrap_ttl=wrap_ttl,
|
||||
unsafe_stdout=unsafe_stdout,
|
||||
)
|
||||
return
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue