WARDEN-WP-0026 finish Strand A (T04/T05/T07)
Promote railiance-backup-offsite-lane to active/resolvable after capabilities-safe re-verify. Add catalog risk=high, agent read-boundary (exit 7 + OpenBao policy companion), EXPOSED taint via warden taint, and close WP-0026.
This commit is contained in:
parent
7d0c7c7684
commit
b971403dad
16 changed files with 689 additions and 31 deletions
|
|
@ -56,6 +56,20 @@ stdout unless you pass `--unsafe-stdout` (interactive human sessions only):
|
|||
| **Env (exec)** | `warden access <need> --exec -- <cmd>` | value injected into the child process env only |
|
||||
| **Wrapping token** | `warden access <need> --wrap` | a single-use, short-TTL OpenBao wrapping token to `bao unwrap` in your own context |
|
||||
|
||||
### Agent read-boundary on high-risk lanes (WP-0026 T04)
|
||||
|
||||
When `WARDEN_AGENT_ID` is set and the catalog lane is `risk: high`, raw value
|
||||
streaming is refused (exit 7). Use `--out` / `--exec` / `--wrap` / `--fingerprint`
|
||||
only. OpenBao policy `agent-high-risk-boundary` denies data-read on those paths
|
||||
for agent tokens (metadata/capabilities only). See
|
||||
`wiki/playbooks/agent-read-boundary.md`.
|
||||
|
||||
### EXPOSED taint (WP-0026 T05)
|
||||
|
||||
`warden taint <catalog-id>` reports KV v2 `custom_metadata` (`exposed_at`,
|
||||
`exposed_version`, …) without reading secret data. Convention:
|
||||
`wiki/playbooks/exposed-taint.md`.
|
||||
|
||||
### Other capabilities (reuse-surface)
|
||||
|
||||
Non-credential capabilities are usually discovered through **reuse-surface** federation
|
||||
|
|
|
|||
|
|
@ -148,6 +148,15 @@ Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run wa
|
|||
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
|
||||
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
|
||||
- Pasting secrets into Git, State Hub, workplans, logs, or chat
|
||||
- **Reading a secret value onto a captured stdout.** Prefer `bao token capabilities`
|
||||
for verify, and `warden access … --out` / `--exec` / `--wrap` for use (WP-0026).
|
||||
|
||||
### Agent read-boundary + EXPOSED taint (WP-0026 T04/T05)
|
||||
|
||||
- High-risk lanes (`risk: high` in catalog): with `WARDEN_AGENT_ID` set, raw value
|
||||
streaming is refused. Use sanctioned transports only.
|
||||
- `warden taint <id>` reports EXPOSED metadata without reading secret data.
|
||||
- Playbooks: `wiki/playbooks/agent-read-boundary.md`, `wiki/playbooks/exposed-taint.md`.
|
||||
|
||||
### Other capabilities (reuse-surface)
|
||||
|
||||
|
|
|
|||
|
|
@ -290,6 +290,8 @@ entries:
|
|||
canon_ref: net-kingdom/docs/platform-identity-security-architecture.md
|
||||
reviewed: "2026-07-02"
|
||||
status: active
|
||||
# High-risk: provider API key with spend impact + prompt-adjacent (WP-0026 T04).
|
||||
risk: high
|
||||
# Concrete, owner-confirmed lane — railiance-platform CCR-2026-0003 / RAILIANCE-WP-0010
|
||||
# (promoted 2026-07-02): policy workload-kv-read-llm-connect-provider-secrets and k8s
|
||||
# auth role external-secrets-activity-core applied; ExternalSecret
|
||||
|
|
@ -321,13 +323,20 @@ entries:
|
|||
warden_executes: false
|
||||
wiki_ref: wiki/playbooks/railiance-backup-offsite-lane.md#worker-checklist
|
||||
canon_ref: railiance-platform/docs/workload-kv-access-lanes.md
|
||||
reviewed: "2026-07-07"
|
||||
status: draft
|
||||
reviewed: "2026-07-16"
|
||||
status: active
|
||||
# High-risk: WebDAV upload token + AGE recovery escrow (WP-0026 T04).
|
||||
risk: high
|
||||
# CCR-2026-0004: policy + OIDC role applied; values provisioned 2026-07-07.
|
||||
# Promote to active after positive/negative caller verification.
|
||||
# Capabilities-safe re-verify 2026-07-16 (WP-0026 T07): lane-policy token
|
||||
# capabilities=read on data path; default-policy and agent-high-risk-boundary = deny;
|
||||
# field keys present (NC_WEBDAV_TOKEN, NC_WEBDAV_URL, AGE_PRIVATE_KEY) via metadata
|
||||
# lengths only — no value read. Primary fetch field is NC_WEBDAV_TOKEN (AGE is
|
||||
# recovery escrow; fetch only for restore drills with --field not required —
|
||||
# use bao as caller or extend fetch). EXPOSED taint set on version 2 (T05).
|
||||
auth_method: "caller's own OpenBao token (OIDC netkingdom role railiance-backup-workload-kv-read)"
|
||||
path_template: "platform/workloads/railiance/backup/offsite-lane"
|
||||
fetch_command: "bao kv get -field=<FIELD> platform/workloads/railiance/backup/offsite-lane"
|
||||
fetch_command: "bao kv get -field=NC_WEBDAV_TOKEN platform/workloads/railiance/backup/offsite-lane"
|
||||
exec_capable: true
|
||||
lane: secret
|
||||
# Mixed lane: NC_WEBDAV_TOKEN rotates (provider re-mint); AGE_PRIVATE_KEY
|
||||
|
|
@ -344,6 +353,7 @@ entries:
|
|||
- "Re-establish AGE_PRIVATE_KEY: generate a new keypair (`age-keygen`), decrypt existing offsite artifacts with the old key and re-encrypt to the new recipient, then `bao kv put ... AGE_PRIVATE_KEY=@file` (mode-0600 file, shred after)."
|
||||
- "Because AGE_PRIVATE_KEY is recovery escrow, retain the old key offline until re-encryption of all retained backups is confirmed."
|
||||
- "Verify capabilities-safe on the data path (`bao token capabilities`); run a restore drill against a re-encrypted artifact."
|
||||
- "After rotation, clear EXPOSED taint: remove custom_metadata exposed_at/exposed_version (see `warden taint railiance-backup-offsite-lane`)."
|
||||
|
||||
- id: forgejo-admin-api-token
|
||||
title: Forgejo operator/admin API token (PAT)
|
||||
|
|
@ -355,7 +365,8 @@ entries:
|
|||
canon_ref: railiance-platform/docs/workload-kv-access-lanes.md
|
||||
reviewed: "2026-07-13"
|
||||
status: active
|
||||
resolvable: true
|
||||
# High-risk: site-admin PAT (WP-0026 T04).
|
||||
risk: high
|
||||
# CCR-2026-0006: approved by platform-operator 2026-07-12; policy
|
||||
# workload-kv-read-forgejo-admin + OIDC role forgejo-admin-workload-kv-read live on
|
||||
# bao.coulomb.social; PAT attended-minted and stored under field API_TOKEN at
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Annotated, List, Optional
|
||||
|
|
@ -630,6 +631,9 @@ def _entry_summary(entry) -> dict:
|
|||
"canon_ref": entry.canon_ref,
|
||||
"reviewed": entry.reviewed,
|
||||
"status": entry.status,
|
||||
# Agent read-boundary (WP-0026 T04) — high-risk lanes deny raw agent data reads.
|
||||
"risk": entry.risk,
|
||||
"high_risk": entry.is_high_risk,
|
||||
# Renewal guidance (WP-0026 T06) — advisory, no secret values. `has_rotation`
|
||||
# lets a caller gate before asking for the full block via `warden rotate-guide`.
|
||||
"has_rotation": entry.has_rotation,
|
||||
|
|
@ -803,6 +807,63 @@ def route_show(
|
|||
)
|
||||
|
||||
|
||||
@app.command("taint")
|
||||
def taint_show(
|
||||
entry_id: Annotated[str, typer.Argument(help="Catalog entry id (see `warden route list`)")],
|
||||
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
|
||||
) -> None:
|
||||
"""Report whether a lane's OpenBao secret is marked EXPOSED (WP-0026 T05).
|
||||
|
||||
Reads KV v2 *metadata only* (custom_metadata: exposed_at, exposed_version, …).
|
||||
Never reads secret data. Advisory — does not rotate or clear taint.
|
||||
"""
|
||||
from warden.taint import TaintError, fetch_taint_status
|
||||
|
||||
catalog = _load_catalog()
|
||||
entry = catalog.get(entry_id)
|
||||
if entry is None:
|
||||
# Drafts are findable by exact id via get even when not listed.
|
||||
err.print(
|
||||
f"[red]Unknown routing id {entry_id!r}.[/red] Try: warden route find {entry_id!r} --all"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
try:
|
||||
status = fetch_taint_status(entry)
|
||||
except TaintError as e:
|
||||
err.print(f"[red]taint status unavailable:[/red] {e}")
|
||||
raise typer.Exit(2)
|
||||
|
||||
if output_json:
|
||||
print(json.dumps(status.to_dict(), indent=2))
|
||||
return
|
||||
|
||||
console.print(f"[bold]Taint status — {entry.title}[/bold] ([cyan]{entry.id}[/cyan])")
|
||||
console.print(f" path : {status.path}")
|
||||
if status.error:
|
||||
console.print(f" [yellow]query error[/yellow] : {status.error}")
|
||||
console.print(
|
||||
" [dim]Need caller OpenBao auth with metadata-read on the path "
|
||||
"(agent-high-risk-boundary allows metadata; workload-kv-read allows both).[/dim]"
|
||||
)
|
||||
raise typer.Exit(3)
|
||||
if status.tainted:
|
||||
console.print(" tainted : [red]yes (EXPOSED)[/red]")
|
||||
console.print(f" exposed_at : {status.exposed_at}")
|
||||
console.print(f" exposed_version : {status.exposed_version}")
|
||||
console.print(f" exposed_reason : {status.exposed_reason}")
|
||||
console.print(f" exposed_ref : {status.exposed_ref}")
|
||||
console.print(f" current_version : {status.current_version}")
|
||||
console.print(
|
||||
"\n[yellow]Advisory:[/yellow] rotate/re-establish per "
|
||||
f"`warden rotate-guide {entry.id}` then clear custom_metadata keys "
|
||||
"(exposed_at, exposed_version, …). No auto-rotation (Strand B)."
|
||||
)
|
||||
else:
|
||||
console.print(" tainted : [green]no[/green]")
|
||||
console.print(f" current_version : {status.current_version}")
|
||||
|
||||
|
||||
@app.command("rotate-guide")
|
||||
def rotate_guide(
|
||||
entry_id: Annotated[str, typer.Argument(help="Catalog entry id (see `warden route list`)")],
|
||||
|
|
@ -1059,11 +1120,30 @@ def _access_proxy(
|
|||
err.print(f"[red]{e}[/red]")
|
||||
raise typer.Exit(2)
|
||||
|
||||
# T04 — agent identity on a high-risk lane: never stream raw secret data.
|
||||
# Agents may use sanctioned transports (--out / --exec / --wrap / --fingerprint).
|
||||
agent_id = os.environ.get("WARDEN_AGENT_ID", "").strip()
|
||||
raw_value_stream = (
|
||||
not is_login and not do_exec and not wrap and not out_path and not fingerprint
|
||||
)
|
||||
if raw_value_stream and entry.is_high_risk and agent_id:
|
||||
err.print(
|
||||
f"[red]Agent read-boundary:[/red] {entry.id!r} is risk=high; "
|
||||
f"agent identity {agent_id!r} must not stream raw secret data.\n"
|
||||
"Use a sanctioned transport (value stays off the session transcript):\n"
|
||||
" --out FILE write to a mode-0600 file\n"
|
||||
" --exec -- CMD inject into a child process env only\n"
|
||||
" --wrap single-use OpenBao wrapping token (unwrap out-of-band)\n"
|
||||
" --fingerprint masked presence/length/hash only\n"
|
||||
"OpenBao policy `agent-high-risk-boundary` also denies data-read for agents."
|
||||
)
|
||||
raise typer.Exit(7)
|
||||
|
||||
# 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 and not fingerprint:
|
||||
if raw_value_stream:
|
||||
import sys as _sys
|
||||
|
||||
if not _sys.stdout.isatty() and not unsafe_stdout:
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from typing import List, Optional
|
|||
|
||||
import yaml
|
||||
|
||||
from warden.routing.models import RotationGuide, RouteEntry
|
||||
from warden.routing.models import VALID_RISK, RotationGuide, RouteEntry
|
||||
|
||||
# Structured handoff string fields (WP-0014) — templates and pointers only.
|
||||
# Every one is scanned for accidental secret material; see _assert_no_secret_material.
|
||||
|
|
@ -303,6 +303,12 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
|
|||
f"entry {entry_id!r} has invalid lane {lane!r} (expected one of {_VALID_LANES})"
|
||||
)
|
||||
|
||||
risk = str(raw.get("risk", "standard")).strip() or "standard"
|
||||
if risk not in VALID_RISK:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} has invalid risk {risk!r} (expected one of {VALID_RISK})"
|
||||
)
|
||||
|
||||
return RouteEntry(
|
||||
id=entry_id,
|
||||
title=str(raw["title"]),
|
||||
|
|
@ -326,6 +332,7 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
|
|||
exec_command=handoff["exec_command"],
|
||||
pointer_command=handoff["pointer_command"],
|
||||
rotation=_parse_rotation(entry_id, raw.get("rotation")),
|
||||
risk=risk,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@ class RotationGuide:
|
|||
automatable: bool = False
|
||||
|
||||
|
||||
# Risk classes for agent read-boundary (WARDEN-WP-0026 T04).
|
||||
# high — recovery escrow, upload tokens, admin PATs, high-spend provider keys.
|
||||
# Agent identities must not hold raw data-read (metadata/capabilities only).
|
||||
# standard — ordinary workload secrets (ESO-fed, non-escrow); normal least-privilege.
|
||||
VALID_RISK = ("standard", "high")
|
||||
|
||||
|
||||
@dataclass
|
||||
class RouteEntry:
|
||||
id: str
|
||||
|
|
@ -69,11 +76,18 @@ class RouteEntry:
|
|||
pointer_command: Optional[str] = None # e.g. "secrets-engine route <id> --json"
|
||||
# Rotation / re-establishment guidance (WP-0026 T06) — advisory, no secret values.
|
||||
rotation: Optional[RotationGuide] = None
|
||||
# Agent read-boundary risk class (WP-0026 T04). high → agents use wrap/out/exec only.
|
||||
risk: str = "standard" # "standard" | "high"
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
return self.status == "active"
|
||||
|
||||
@property
|
||||
def is_high_risk(self) -> bool:
|
||||
"""True when this lane is on the agent raw-read deny list (WP-0026 T04)."""
|
||||
return self.risk == "high"
|
||||
|
||||
@property
|
||||
def has_rotation(self) -> bool:
|
||||
"""True when this lane carries renewal guidance (WP-0026 T06)."""
|
||||
|
|
|
|||
149
src/warden/taint.py
Normal file
149
src/warden/taint.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
"""EXPOSED taint convention for OpenBao KV secrets (WARDEN-WP-0026 T05).
|
||||
|
||||
Convention (KV v2 ``custom_metadata`` on the secret, never on secret *data*):
|
||||
|
||||
* ``exposed_at`` — ISO-8601 UTC datetime when disclosure was recognized
|
||||
* ``exposed_version`` — KV version that was (or may have been) disclosed
|
||||
* ``exposed_reason`` — short machine-safe reason slug (optional)
|
||||
* ``exposed_ref`` — pointer to a lessons note / CCR / incident doc (optional)
|
||||
|
||||
A lane is **tainted** when ``exposed_at`` is set and non-empty. Clearing taint
|
||||
(after rotation) is an operator action: remove those keys from custom_metadata.
|
||||
ops-warden only *reports* taint — it never auto-rotates (Strand B / WP-0027).
|
||||
|
||||
This module only shells out to ``bao kv metadata get`` (or equivalent). It never
|
||||
reads secret data values.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
from warden.routing.models import RouteEntry
|
||||
|
||||
# Canonical custom_metadata keys (WP-0026 T05).
|
||||
EXPOSED_AT = "exposed_at"
|
||||
EXPOSED_VERSION = "exposed_version"
|
||||
EXPOSED_REASON = "exposed_reason"
|
||||
EXPOSED_REF = "exposed_ref"
|
||||
|
||||
_TAINT_KEYS = (EXPOSED_AT, EXPOSED_VERSION, EXPOSED_REASON, EXPOSED_REF)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaintStatus:
|
||||
"""Advisory taint view for a lane — no secret values."""
|
||||
|
||||
lane_id: str
|
||||
path: str
|
||||
tainted: bool
|
||||
exposed_at: Optional[str] = None
|
||||
exposed_version: Optional[str] = None
|
||||
exposed_reason: Optional[str] = None
|
||||
exposed_ref: Optional[str] = None
|
||||
current_version: Optional[int] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.lane_id,
|
||||
"path": self.path,
|
||||
"tainted": self.tainted,
|
||||
"exposed_at": self.exposed_at,
|
||||
"exposed_version": self.exposed_version,
|
||||
"exposed_reason": self.exposed_reason,
|
||||
"exposed_ref": self.exposed_ref,
|
||||
"current_version": self.current_version,
|
||||
**({"error": self.error} if self.error else {}),
|
||||
}
|
||||
|
||||
|
||||
class TaintError(Exception):
|
||||
"""Raised when taint status cannot be determined (auth, path, tool)."""
|
||||
|
||||
|
||||
def kv_metadata_path(path_template: str) -> str:
|
||||
"""Return the logical KV path suitable for ``bao kv metadata get``.
|
||||
|
||||
Catalog paths are logical (``platform/workloads/...``), not API data paths.
|
||||
"""
|
||||
return path_template.strip().strip("/")
|
||||
|
||||
|
||||
def parse_custom_metadata(meta: dict[str, Any]) -> TaintStatus:
|
||||
"""Build a TaintStatus from a ``bao kv metadata get -format=json`` data blob.
|
||||
|
||||
``meta`` is the ``data`` object (with ``custom_metadata``, ``current_version``).
|
||||
"""
|
||||
custom = meta.get("custom_metadata") or {}
|
||||
if not isinstance(custom, dict):
|
||||
custom = {}
|
||||
exposed_at = (custom.get(EXPOSED_AT) or "").strip() or None
|
||||
return TaintStatus(
|
||||
lane_id="",
|
||||
path="",
|
||||
tainted=bool(exposed_at),
|
||||
exposed_at=exposed_at,
|
||||
exposed_version=(custom.get(EXPOSED_VERSION) or "").strip() or None,
|
||||
exposed_reason=(custom.get(EXPOSED_REASON) or "").strip() or None,
|
||||
exposed_ref=(custom.get(EXPOSED_REF) or "").strip() or None,
|
||||
current_version=meta.get("current_version"),
|
||||
)
|
||||
|
||||
|
||||
def fetch_taint_status(entry: RouteEntry, *, bao_bin: str = "bao") -> TaintStatus:
|
||||
"""Query OpenBao metadata for a catalog entry (never reads secret data).
|
||||
|
||||
Uses the caller's ``BAO_TOKEN`` / ``VAULT_TOKEN`` / ``~/.vault-token`` — same
|
||||
G1 rule as the access proxy. Requires ``path_template`` on the entry.
|
||||
"""
|
||||
if not entry.path_template or "<" in entry.path_template:
|
||||
raise TaintError(
|
||||
f"{entry.id!r} has no concrete path_template — cannot query taint metadata."
|
||||
)
|
||||
path = kv_metadata_path(entry.path_template)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[bao_bin, "kv", "metadata", "get", "-format=json", path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=os.environ.copy(),
|
||||
check=False,
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
raise TaintError(f"{bao_bin!r} not found on PATH") from e
|
||||
|
||||
if proc.returncode != 0:
|
||||
err = (proc.stderr or proc.stdout or "metadata get failed").strip().splitlines()
|
||||
# Never echo tokens if somehow present.
|
||||
safe = " ".join(err[:3])[:300]
|
||||
return TaintStatus(
|
||||
lane_id=entry.id,
|
||||
path=path,
|
||||
tainted=False,
|
||||
error=safe or f"bao exit {proc.returncode}",
|
||||
)
|
||||
|
||||
try:
|
||||
payload = json.loads(proc.stdout)
|
||||
except json.JSONDecodeError as e:
|
||||
raise TaintError(f"invalid JSON from bao metadata get: {e}") from e
|
||||
|
||||
data = payload.get("data") if isinstance(payload, dict) else None
|
||||
if not isinstance(data, dict):
|
||||
raise TaintError("bao metadata response missing data object")
|
||||
|
||||
status = parse_custom_metadata(data)
|
||||
return TaintStatus(
|
||||
lane_id=entry.id,
|
||||
path=path,
|
||||
tainted=status.tainted,
|
||||
exposed_at=status.exposed_at,
|
||||
exposed_version=status.exposed_version,
|
||||
exposed_reason=status.exposed_reason,
|
||||
exposed_ref=status.exposed_ref,
|
||||
current_version=status.current_version,
|
||||
)
|
||||
|
|
@ -383,3 +383,39 @@ def test_access_fingerprint_masks_and_bypasses_stdout_guard(monkeypatch, tmp_pat
|
|||
assert r.exit_code == 0
|
||||
assert "top-secret-token-value" not in r.output # value never shown
|
||||
assert "hidden" in r.output and "sha256:" in r.output
|
||||
|
||||
|
||||
def test_access_agent_high_risk_raw_stream_refused(tmp_path, monkeypatch):
|
||||
"""WP-0026 T04: WARDEN_AGENT_ID + risk=high refuses raw value stream (exit 7)."""
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller-token")
|
||||
monkeypatch.setenv("WARDEN_AGENT_ID", "grok")
|
||||
# Prefer high-risk lane; use --unsafe-stdout so T02 would allow if T04 failed.
|
||||
r = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"access", "railiance-backup-offsite-lane",
|
||||
"--fetch", "--no-policy", "--unsafe-stdout",
|
||||
],
|
||||
)
|
||||
assert r.exit_code == 7, r.output
|
||||
assert "agent read-boundary" in r.output.lower() or "risk=high" in r.output.lower()
|
||||
|
||||
|
||||
def test_access_agent_high_risk_fingerprint_allowed(tmp_path, monkeypatch):
|
||||
"""Agents may use --fingerprint on high-risk lanes (no raw value)."""
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller-token")
|
||||
monkeypatch.setenv("WARDEN_AGENT_ID", "grok")
|
||||
|
||||
class _Fake:
|
||||
returncode = 0
|
||||
stdout = "should-not-appear"
|
||||
|
||||
monkeypatch.setattr("warden.proxy.subprocess.run", lambda *a, **k: _Fake())
|
||||
r = runner.invoke(
|
||||
app,
|
||||
["access", "railiance-backup-offsite-lane", "--fingerprint", "--no-policy"],
|
||||
)
|
||||
assert r.exit_code == 0, r.output
|
||||
assert "should-not-appear" not in r.output
|
||||
|
|
|
|||
|
|
@ -531,3 +531,47 @@ def test_rotate_guide_cli_ssh_lane_is_graceful():
|
|||
# SSH renewal is re-issuance, not a static rotation — exit 0, not an error.
|
||||
result = runner.invoke(app, ["rotate-guide", "ssh-cert-host-access"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent read-boundary + risk class (WARDEN-WP-0026 T04)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_high_risk_lanes_classified():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
high = {e.id for e in catalog.entries if e.is_high_risk}
|
||||
assert "railiance-backup-offsite-lane" in high
|
||||
assert "forgejo-admin-api-token" in high
|
||||
assert "openrouter-llm-connect" in high
|
||||
# Ordinary workload secrets stay standard unless reclassified.
|
||||
assert catalog.get("issue-core-ingestion-api-key").is_high_risk is False
|
||||
|
||||
|
||||
def test_invalid_risk_rejected(tmp_path):
|
||||
bad = dict(ROUTED_ENTRY, risk="critical")
|
||||
with pytest.raises(CatalogError, match="risk"):
|
||||
load_catalog(_write_catalog(tmp_path, [SSH_ENTRY, bad]))
|
||||
|
||||
|
||||
def test_backup_lane_promoted_and_resolvable():
|
||||
"""WP-0026 T07 — CCR-2026-0004 lane is active, resolvable, high-risk, has rotation."""
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
e = catalog.get("railiance-backup-offsite-lane")
|
||||
assert e is not None
|
||||
assert e.status == "active"
|
||||
assert e.resolvable is True
|
||||
assert e.is_high_risk is True
|
||||
assert e.has_rotation is True
|
||||
assert e.rotation.method == "re-establish"
|
||||
assert "NC_WEBDAV_TOKEN" in (e.fetch_command or "")
|
||||
assert "<" not in (e.fetch_command or "")
|
||||
|
||||
|
||||
def test_route_show_json_includes_risk():
|
||||
result = runner.invoke(app, ["route", "show", "railiance-backup-offsite-lane", "--json"])
|
||||
assert result.exit_code == 0
|
||||
payload = json.loads(result.stdout)
|
||||
assert payload["risk"] == "high"
|
||||
assert payload["high_risk"] is True
|
||||
assert payload["resolvable"] is True
|
||||
assert payload["status"] == "active"
|
||||
|
|
|
|||
70
tests/test_taint.py
Normal file
70
tests/test_taint.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"""Tests for EXPOSED taint convention (WARDEN-WP-0026 T05)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from warden.cli import app
|
||||
from warden.taint import (
|
||||
EXPOSED_AT,
|
||||
TaintStatus,
|
||||
parse_custom_metadata,
|
||||
kv_metadata_path,
|
||||
)
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_parse_custom_metadata_tainted():
|
||||
status = parse_custom_metadata({
|
||||
"custom_metadata": {
|
||||
EXPOSED_AT: "2026-07-16T00:00:00Z",
|
||||
"exposed_version": "2",
|
||||
"exposed_reason": "test",
|
||||
"exposed_ref": "history/x.md",
|
||||
},
|
||||
"current_version": 2,
|
||||
})
|
||||
assert status.tainted is True
|
||||
assert status.exposed_at == "2026-07-16T00:00:00Z"
|
||||
assert status.exposed_version == "2"
|
||||
assert status.current_version == 2
|
||||
|
||||
|
||||
def test_parse_custom_metadata_clean():
|
||||
status = parse_custom_metadata({"custom_metadata": None, "current_version": 1})
|
||||
assert status.tainted is False
|
||||
assert status.exposed_at is None
|
||||
|
||||
|
||||
def test_parse_empty_exposed_at_not_tainted():
|
||||
status = parse_custom_metadata({"custom_metadata": {EXPOSED_AT: " "}, "current_version": 1})
|
||||
assert status.tainted is False
|
||||
|
||||
|
||||
def test_kv_metadata_path_strips():
|
||||
assert kv_metadata_path(" platform/workloads/x ") == "platform/workloads/x"
|
||||
|
||||
|
||||
def test_taint_status_to_dict():
|
||||
s = TaintStatus(
|
||||
lane_id="x", path="p", tainted=True,
|
||||
exposed_at="t", exposed_version="1", current_version=1,
|
||||
)
|
||||
d = s.to_dict()
|
||||
assert d["tainted"] is True
|
||||
assert d["id"] == "x"
|
||||
|
||||
|
||||
def test_taint_cli_unknown_id():
|
||||
result = runner.invoke(app, ["taint", "no-such-lane-xyz"])
|
||||
assert result.exit_code == 1
|
||||
|
||||
|
||||
def test_taint_cli_template_lane_errors():
|
||||
"""openbao-api-key has <placeholders> — cannot query taint."""
|
||||
result = runner.invoke(app, ["taint", "openbao-api-key", "--json"])
|
||||
assert result.exit_code == 2
|
||||
|
|
@ -95,10 +95,14 @@ run the owner's tool as the caller and preserve owner custody.
|
|||
| `activity-core-issue-sink` | "activity-core + issue-core own emission — pair `ISSUE_CORE_*` env vars" | See `wiki/playbooks/activity-core-issue-sink.md` |
|
||||
| `inter-hub-bootstrap-ssh` | "Inter-Hub bootstrap SSH envelope — attended vs unattended branches" | See `wiki/InterHubBootstrapAccessLane.md` |
|
||||
| `issue-core-ingestion-api-key` | "railiance-platform OpenBao KV + ESO deliver `ISSUE_CORE_API_KEY` — here is the path" | ESO consumes in-cluster; `warden access issue-core-ingestion-api-key --fetch ISSUE_CORE_API_KEY` as yourself |
|
||||
| `openrouter-llm-connect` | "railiance-platform OpenBao KV + ESO deliver `OPENROUTER_API_KEY` to activity-core" | ESO consumes in-cluster; `warden access openrouter-llm-connect --fetch OPENROUTER_API_KEY` as yourself |
|
||||
| `openrouter-llm-connect` | "railiance-platform OpenBao KV + ESO deliver `OPENROUTER_API_KEY` to activity-core" | ESO consumes in-cluster; `warden access openrouter-llm-connect --fetch OPENROUTER_API_KEY` as yourself (`risk: high`) |
|
||||
| `reuse-surface-hub-write-token` | "railiance-platform OpenBao KV + ESO deliver `REUSE_SURFACE_TOKEN` to reuse-surface" | ESO consumes in-cluster; `warden access reuse-surface-hub-write-token --fetch` as yourself |
|
||||
| `railiance-backup-offsite-lane` | "railiance-platform OpenBao KV for Nextcloud WebDAV + age recovery" | `warden access railiance-backup-offsite-lane --out FILE` (`risk: high`; agents: no raw stream) |
|
||||
| `forgejo-admin-api-token` | "railiance-platform OpenBao KV for Forgejo admin PAT" | `warden access forgejo-admin-api-token --out FILE` (`risk: high`) |
|
||||
|
||||
Promotion criteria: `wiki/playbooks/catalog-lane-promotion.md`.
|
||||
Promotion criteria: `wiki/playbooks/catalog-lane-promotion.md`.
|
||||
High-risk / agent boundary: `wiki/playbooks/agent-read-boundary.md`.
|
||||
EXPOSED taint: `wiki/playbooks/exposed-taint.md` · `warden taint <id>`.
|
||||
|
||||
**Draft** (hidden from default lookup until owner path ships — `warden route list --all`):
|
||||
|
||||
|
|
|
|||
77
wiki/playbooks/agent-read-boundary.md
Normal file
77
wiki/playbooks/agent-read-boundary.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# Agent read-boundary on high-risk lanes
|
||||
|
||||
Date: 2026-07-16
|
||||
Workplan: WARDEN-WP-0026 T04
|
||||
OpenBao policy: `railiance-platform/openbao/policies/agent-high-risk-boundary.hcl`
|
||||
|
||||
Coding agents must not hold **raw data-read** on high-risk secrets. They may
|
||||
inspect **capabilities** and **metadata**, and may receive values only through
|
||||
sanctioned transports (file / exec env / response-wrapping token) under a human
|
||||
operator identity.
|
||||
|
||||
---
|
||||
|
||||
## Risk classification (catalog `risk:`)
|
||||
|
||||
| Class | Criteria | Catalog default |
|
||||
| --- | --- | --- |
|
||||
| `high` | Recovery escrow (e.g. age private keys), upload tokens to external stores, site-admin PATs, high-spend provider keys | explicit `risk: high` |
|
||||
| `standard` | Ordinary workload secrets (ESO-fed API keys without escrow/admin blast radius) | omitted / `standard` |
|
||||
|
||||
**Current high-risk lanes (ops-warden catalog):**
|
||||
|
||||
| Catalog id | Why high |
|
||||
| --- | --- |
|
||||
| `railiance-backup-offsite-lane` | Nextcloud upload + `AGE_PRIVATE_KEY` recovery escrow |
|
||||
| `forgejo-admin-api-token` | Forgejo site-admin PAT |
|
||||
| `openrouter-llm-connect` | Provider key (spend + prompt-adjacent) |
|
||||
|
||||
Keep this table in sync with `risk: high` rows in `registry/routing/catalog.yaml`
|
||||
and path denials in `agent-high-risk-boundary.hcl`.
|
||||
|
||||
---
|
||||
|
||||
## OpenBao side
|
||||
|
||||
1. **Operator OIDC roles** keep `workload-kv-read-*` for the lane (data `read`).
|
||||
2. **Agent identities** attach `agent-high-risk-boundary` (or equivalent) and
|
||||
**must not** also attach the lane's `workload-kv-read-*` policy.
|
||||
3. Verify with capabilities only (never `kv get` for deny tests):
|
||||
|
||||
```bash
|
||||
# Agent-shaped token
|
||||
AGENT=$(bao token create -policy=agent-high-risk-boundary -ttl=5m -field=token)
|
||||
bao token capabilities "$AGENT" platform/data/workloads/railiance/backup/offsite-lane
|
||||
# → deny
|
||||
bao token capabilities "$AGENT" platform/metadata/workloads/railiance/backup/offsite-lane
|
||||
# → read
|
||||
bao token revoke "$AGENT"
|
||||
```
|
||||
|
||||
Wrapped/proxied access for agents: a human operator (or credential broker with
|
||||
response-wrap) fetches under an operator identity and delivers via
|
||||
`warden access … --wrap` / `--out` / `--exec`. Agents do not unwrap into chat.
|
||||
|
||||
---
|
||||
|
||||
## ops-warden side
|
||||
|
||||
When `WARDEN_AGENT_ID` is set and the lane is `risk: high`, `warden access --fetch`
|
||||
**refuses raw value streaming** (exit 7). Use:
|
||||
|
||||
```bash
|
||||
export WARDEN_AGENT_ID=grok # or claude, codex
|
||||
warden access railiance-backup-offsite-lane --no-policy --out /tmp/nc.token
|
||||
warden access railiance-backup-offsite-lane --no-policy --wrap
|
||||
warden access railiance-backup-offsite-lane --no-policy --fingerprint
|
||||
```
|
||||
|
||||
`warden route show <id> --json` includes `"risk"` and `"high_risk"`.
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
- `.claude/rules/credential-routing.md` — safe transports
|
||||
- `wiki/playbooks/exposed-taint.md` — EXPOSED metadata convention
|
||||
- `history/2026-07-16-credential-disclosure-lessons.md`
|
||||
|
|
@ -87,13 +87,17 @@ out on the OpenBao-delivered value, positive + negative verification audit-logge
|
|||
|
||||
---
|
||||
|
||||
## Draft lanes (2026-07-02)
|
||||
## Draft lanes (2026-07-16)
|
||||
|
||||
| Catalog `id` | Blocker |
|
||||
| --- | --- |
|
||||
| `object-storage-sts` | NK-WP-0007 vending path not production-exercised |
|
||||
| `database-dynamic-credentials` | OpenBao database engine role paths TBD per workload |
|
||||
|
||||
**Promoted 2026-07-16:** `railiance-backup-offsite-lane` — CCR-2026-0004
|
||||
capabilities-safe re-verify (WP-0026 T07); primary field `NC_WEBDAV_TOKEN`;
|
||||
`risk: high` + EXPOSED taint on version 2 (operator may rotate optionally).
|
||||
|
||||
Re-run promotion when the owning repo closes the blocker; do not promote on
|
||||
playbook prose alone.
|
||||
|
||||
|
|
|
|||
71
wiki/playbooks/exposed-taint.md
Normal file
71
wiki/playbooks/exposed-taint.md
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# EXPOSED taint convention (OpenBao KV v2)
|
||||
|
||||
Date: 2026-07-16
|
||||
Workplan: WARDEN-WP-0026 T05
|
||||
|
||||
Mark a secret as **EXPOSED** when it may have landed in a logged or shared context
|
||||
(agent transcript, chat, CI log). Taint is **advisory**: it does not revoke access
|
||||
or rotate values. Strand B (`WARDEN-WP-0027`) may later drive rotation of tainted
|
||||
lanes; Strand A only records and reports.
|
||||
|
||||
---
|
||||
|
||||
## Custom metadata keys
|
||||
|
||||
Set on the KV v2 secret **metadata** (never in secret data values):
|
||||
|
||||
| Key | Required | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `exposed_at` | yes | ISO-8601 UTC when disclosure was recognized |
|
||||
| `exposed_version` | recommended | KV version that was (or may have been) disclosed |
|
||||
| `exposed_reason` | optional | short slug, e.g. `agent-session-kv-get-disclosure` |
|
||||
| `exposed_ref` | optional | pointer to lessons note / CCR / incident doc |
|
||||
|
||||
A lane is **tainted** when `exposed_at` is present and non-empty.
|
||||
|
||||
---
|
||||
|
||||
## Mark EXPOSED
|
||||
|
||||
```bash
|
||||
bao kv metadata put \
|
||||
-custom-metadata=exposed_at=2026-07-16T00:00:00Z \
|
||||
-custom-metadata=exposed_version=2 \
|
||||
-custom-metadata=exposed_reason=agent-session-kv-get-disclosure \
|
||||
-custom-metadata=exposed_ref=history/2026-07-16-credential-disclosure-lessons.md \
|
||||
platform/workloads/railiance/backup/offsite-lane
|
||||
```
|
||||
|
||||
## Report taint (no secret values)
|
||||
|
||||
```bash
|
||||
warden taint railiance-backup-offsite-lane
|
||||
warden taint railiance-backup-offsite-lane --json
|
||||
# or:
|
||||
bao kv metadata get platform/workloads/railiance/backup/offsite-lane
|
||||
```
|
||||
|
||||
## Clear taint (after rotation)
|
||||
|
||||
After following `warden rotate-guide <id>` and verifying the new version:
|
||||
|
||||
```bash
|
||||
# Rewrite metadata without the exposed_* keys (preserve any other custom_metadata).
|
||||
bao kv metadata put platform/workloads/<lane-path>
|
||||
# Or put only non-taint keys you still need.
|
||||
warden taint <id> # expect tainted: no
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Semi-automatic candidates
|
||||
|
||||
Reads of high-risk paths from agent/shared identities in the OpenBao audit log
|
||||
are candidates for marking EXPOSED. Marking remains an operator decision; do not
|
||||
auto-taint from noisy audit alone without human review.
|
||||
|
||||
## See also
|
||||
|
||||
- `history/2026-07-16-credential-disclosure-lessons.md`
|
||||
- `wiki/playbooks/catalog-lane-promotion.md` (capabilities-safe verify)
|
||||
- OpenBao policy `agent-high-risk-boundary` (WP-0026 T04)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# Railiance Offsite Backup Lane
|
||||
|
||||
Date: 2026-07-07
|
||||
Catalog: `railiance-backup-offsite-lane` (status `draft`, `resolvable: false` until verified)
|
||||
Date: 2026-07-16
|
||||
Catalog: `railiance-backup-offsite-lane` (status `active`, `resolvable: true`, `risk: high`)
|
||||
Owner: `railiance-platform` (CCR-2026-0004)
|
||||
|
||||
Nextcloud WebDAV upload token and URL for age-encrypted offsite backups (Option A).
|
||||
|
|
@ -15,9 +15,11 @@ Used by `railiance-backup` (workstation) and `forgejo-backup` (platform).
|
|||
| --- | --- |
|
||||
| Mount | `platform` |
|
||||
| Path | `platform/workloads/railiance/backup/offsite-lane` |
|
||||
| Fields | `NC_WEBDAV_TOKEN`, `NC_WEBDAV_URL`, `AGE_PRIVATE_KEY` |
|
||||
| Policy | `workload-kv-read-railiance-backup-offsite-lane` |
|
||||
| Fields | `NC_WEBDAV_TOKEN` (primary fetch), `NC_WEBDAV_URL`, `AGE_PRIVATE_KEY` (recovery escrow) |
|
||||
| Policy | `workload-kv-read-railiance-backup-offsite-lane` (operator OIDC) |
|
||||
| Agent policy | `agent-high-risk-boundary` — **deny** data-read; metadata only |
|
||||
| OIDC role | `railiance-backup-workload-kv-read` (`groups=net-kingdom-admins`) |
|
||||
| Risk | `high` (upload token + age recovery escrow) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -29,7 +31,16 @@ Used by `railiance-backup` (workstation) and `forgejo-backup` (platform).
|
|||
bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read
|
||||
```
|
||||
|
||||
2. **Export for a backup run** (value streams to your shell — never paste into chat):
|
||||
2. **Export for a backup run** (sanctioned transports — never paste into chat):
|
||||
|
||||
```bash
|
||||
# Preferred: file or exec injection via warden
|
||||
warden access railiance-backup-offsite-lane --no-policy --out /tmp/nc.token
|
||||
# or:
|
||||
warden access railiance-backup-offsite-lane --no-policy --exec -- env | grep -v .
|
||||
```
|
||||
|
||||
If you must use raw bao in an interactive human shell (not an agent session):
|
||||
|
||||
```bash
|
||||
export RAILIANCE_BACKUP_NC_TOKEN=$(
|
||||
|
|
@ -40,10 +51,11 @@ Used by `railiance-backup` (workstation) and `forgejo-backup` (platform).
|
|||
)
|
||||
```
|
||||
|
||||
3. **Or proxy via warden access** (after catalog promotion):
|
||||
3. **Proxy via warden access** (catalog active + resolvable):
|
||||
|
||||
```bash
|
||||
warden access railiance-backup-offsite-lane --no-policy --fetch --field NC_WEBDAV_TOKEN
|
||||
warden access railiance-backup-offsite-lane --no-policy --fetch --out /tmp/nc.token
|
||||
# Primary field is NC_WEBDAV_TOKEN. AGE_PRIVATE_KEY is recovery escrow only.
|
||||
```
|
||||
|
||||
4. **Run backup**:
|
||||
|
|
@ -57,6 +69,8 @@ Used by `railiance-backup` (workstation) and `forgejo-backup` (platform).
|
|||
```
|
||||
|
||||
`AGE_PRIVATE_KEY` in the same path is recovery escrow — fetch only for restore drills.
|
||||
**Agents** (`WARDEN_AGENT_ID` set) cannot stream raw high-risk values; use `--out` /
|
||||
`--exec` / `--wrap`. See `wiki/playbooks/agent-read-boundary.md`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -67,17 +81,51 @@ value to "confirm" it triggered the 2026-07-16 disclosure of `NC_WEBDAV_TOKEN` /
|
|||
`NC_WEBDAV_URL` / `AGE_PRIVATE_KEY` (see `history/2026-07-16-credential-disclosure-lessons.md`).
|
||||
|
||||
```bash
|
||||
# Positive: lane OIDC identity can read the data path
|
||||
bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read
|
||||
bao token capabilities "$(bao print token)" platform/data/workloads/railiance/backup/offsite-lane
|
||||
# Positive: lane policy identity can read the data path
|
||||
LANE=$(bao token create -policy=workload-kv-read-railiance-backup-offsite-lane -ttl=2m -field=token)
|
||||
bao token capabilities "$LANE" platform/data/workloads/railiance/backup/offsite-lane
|
||||
# → expect: read
|
||||
bao token revoke "$LANE"
|
||||
|
||||
# Negative: default-only identity is denied (no value is read)
|
||||
DEFAULT_TOKEN=$(bao token create -policy=default -field=token) # if this is denied, that IS the pass — do NOT fall back
|
||||
bao token capabilities "$DEFAULT_TOKEN" platform/data/workloads/railiance/backup/offsite-lane
|
||||
# → expect: deny
|
||||
bao token revoke "$DEFAULT_TOKEN"
|
||||
|
||||
# Agent boundary: data deny, metadata allow
|
||||
AGENT=$(bao token create -policy=agent-high-risk-boundary -ttl=2m -field=token)
|
||||
bao token capabilities "$AGENT" platform/data/workloads/railiance/backup/offsite-lane # deny
|
||||
bao token capabilities "$AGENT" platform/metadata/workloads/railiance/backup/offsite-lane # read
|
||||
bao token revoke "$AGENT"
|
||||
```
|
||||
|
||||
The capability lists contain no secret material — safe to record on
|
||||
`CCR-2026-0004` as promotion evidence. Full pattern:
|
||||
`wiki/playbooks/catalog-lane-promotion.md#capabilities-safe-lane-verification`.
|
||||
`wiki/playbooks/catalog-lane-promotion.md#capabilities-safe-lane-verification`.
|
||||
|
||||
### Evidence recorded 2026-07-16 (WP-0026 T07)
|
||||
|
||||
| Check | Result |
|
||||
| --- | --- |
|
||||
| Policy `workload-kv-read-railiance-backup-offsite-lane` present | pass |
|
||||
| OIDC role `railiance-backup-workload-kv-read` bound to `net-kingdom-admins` + lane policy | pass |
|
||||
| Lane-policy token capabilities on data path | `read` |
|
||||
| Default-policy token capabilities on data path | `deny` |
|
||||
| Agent-boundary token on data / metadata | `deny` / `read` |
|
||||
| Field presence (keys only, lengths; no values) | `NC_WEBDAV_TOKEN`, `NC_WEBDAV_URL`, `AGE_PRIVATE_KEY` present |
|
||||
| EXPOSED taint on version 2 | set (see `warden taint railiance-backup-offsite-lane`) |
|
||||
| Rotation guidance | `warden rotate-guide railiance-backup-offsite-lane` |
|
||||
|
||||
---
|
||||
|
||||
## Taint / rotation
|
||||
|
||||
```bash
|
||||
warden taint railiance-backup-offsite-lane
|
||||
warden rotate-guide railiance-backup-offsite-lane
|
||||
```
|
||||
|
||||
Rotation of exposed values is the **operator's optional call** (buildup mode);
|
||||
promotion is not blocked on rotation. After rotation, clear `exposed_*`
|
||||
custom_metadata keys (`wiki/playbooks/exposed-taint.md`).
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ type: workplan
|
|||
title: "Credential disclosure hygiene + rotation guidance (Strand A)"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: active
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: custodian
|
||||
planning_priority: high
|
||||
|
|
@ -140,11 +140,21 @@ limitation documented.
|
|||
|
||||
```task
|
||||
id: WARDEN-WP-0026-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "f95d4381-f995-4c9d-b438-70b4f0ed90c5"
|
||||
```
|
||||
|
||||
Done 2026-07-16: Catalog `risk: high|standard` (default standard). High-risk:
|
||||
`railiance-backup-offsite-lane`, `forgejo-admin-api-token`, `openrouter-llm-connect`.
|
||||
OpenBao policy `agent-high-risk-boundary` (railiance-platform + live write) grants
|
||||
metadata/capabilities only and **denies** data-read on those paths — verified with
|
||||
minted agent token (data=deny, metadata=read). `warden access` with
|
||||
`WARDEN_AGENT_ID` set refuses raw value stream on high-risk lanes (exit 7);
|
||||
`--out`/`--exec`/`--wrap`/`--fingerprint` remain. Playbook:
|
||||
`wiki/playbooks/agent-read-boundary.md`. Tests in `tests/test_routing.py` +
|
||||
`tests/test_proxy.py`.
|
||||
|
||||
**Repo: railiance-platform (OpenBao policy/roles).** Agent identities receive
|
||||
`capabilities`/metadata and wrapping tokens on high-risk lanes, not raw data
|
||||
reads. Align with the existing credential-routing rule ("ops-warden proxies reads
|
||||
|
|
@ -158,11 +168,19 @@ while still allowing wrapped/proxied access, verified via capabilities checks.
|
|||
|
||||
```task
|
||||
id: WARDEN-WP-0026-T05
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "e989f8f0-930e-4d2f-9037-221e04f34199"
|
||||
```
|
||||
|
||||
Done 2026-07-16: Convention documented in `wiki/playbooks/exposed-taint.md`
|
||||
(`exposed_at`, `exposed_version`, `exposed_reason`, `exposed_ref` on KV v2
|
||||
custom_metadata). First worked mark applied to
|
||||
`platform/workloads/railiance/backup/offsite-lane` version 2 (disclosure
|
||||
incident). `warden taint <id>` (+ `--json`) reports taint via metadata-only
|
||||
`bao kv metadata get` — never secret data (`src/warden/taint.py`). Tests in
|
||||
`tests/test_taint.py`.
|
||||
|
||||
**Repo: railiance-platform (OpenBao) + ops-warden surface.** Establish a KV v2
|
||||
`custom_metadata` convention to mark a tainted secret: `exposed_at=<datetime>`
|
||||
and the affected `version`. Identify semi-automatic candidates from the OpenBao
|
||||
|
|
@ -208,17 +226,19 @@ every active lane, and the scorecard fails if any active lane lacks guidance.
|
|||
|
||||
```task
|
||||
id: WARDEN-WP-0026-T07
|
||||
status: progress
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "9944f46d-3706-43a4-9300-7f63bf87c9ff"
|
||||
```
|
||||
|
||||
Lessons-learned note written 2026-07-16:
|
||||
`history/2026-07-16-credential-disclosure-lessons.md` (buildup context, exposure
|
||||
accepted, three root causes). Remaining: capabilities-based live re-verify of
|
||||
CCR-2026-0004 on `bao.coulomb.social` (uses T01 pattern) and its `rotation:` block
|
||||
(depends on T06 registry) so the lane can promote to `resolvable: true` and unblock
|
||||
RAILIANCE-WP-0015.
|
||||
Done 2026-07-16: Lessons note present. Capabilities-safe live re-verify on
|
||||
`bao.coulomb.social`: lane-policy token → `read` on data path; default + agent
|
||||
boundary → `deny`; field keys present (no values printed). Catalog promoted
|
||||
`draft`→`active`, `fetch_command` pinned to `NC_WEBDAV_TOKEN` (no placeholders)
|
||||
so `resolvable: true`; `risk: high`; rotation guidance + EXPOSED taint on v2.
|
||||
CCR-2026-0004 evidence + `access_frontdoor.resolvable: true` / `readiness: ready`.
|
||||
Playbook + `wiki/CredentialRouting.md` updated. Operator may still rotate the
|
||||
exposed values optionally (buildup) — not a promotion blocker.
|
||||
|
||||
Write a short lessons-learned note (buildup context; exposure accepted; the three
|
||||
root causes). Apply T01 + T06 to `CCR-2026-0004` as the first worked lane:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue