WARDEN: separate fetch evaluator URL for secret-read checks (CUST-WP-0075)
check_fetch_policy now posts only to policy.fetch_flex_auth_url (access-engine pin flex-auth-ops-warden-fetch, workstation 127.0.0.1:19091). Signing keeps policy.flex_auth_url. Unset or unreachable fetch evaluator fails closed with a message naming it; no fallback to the signing pin or to allow. warden access advisory output names the fetch evaluator. Recorded under WARDEN-WP-0039-T03. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 3775@bnt-lap001 Assistant-Session: cfacb6a5-ad5b-4f79-ac67-053e3b0d34a3
This commit is contained in:
parent
30747f84c4
commit
7d4381e887
11 changed files with 292 additions and 28 deletions
|
|
@ -24,6 +24,9 @@ policy:
|
|||
# flex-auth.flex-auth.svc Service does not exist. From a workstation, reach it
|
||||
# through a port-forward or tunnel and point this at that local address.
|
||||
flex_auth_url: http://flex-auth-ops-warden.flex-auth.svc.cluster.local:8080
|
||||
# Fetch-check pin (CUST-WP-0075), used only by `warden access` fetches.
|
||||
# No fallback to flex_auth_url; unset means every fetch is refused.
|
||||
fetch_flex_auth_url: http://flex-auth-ops-warden-fetch.flex-auth.svc.cluster.local.:8080
|
||||
zone_registry_path: registry/flex-auth/production_registry_snapshot.json
|
||||
failure_modes:
|
||||
z0-experimental: fail_open
|
||||
|
|
|
|||
|
|
@ -98,6 +98,10 @@ def policy_gate_status() -> str:
|
|||
cfg = load_config()
|
||||
except ConfigError:
|
||||
return "advisory — no warden.yaml (caller identity; gate not enforced)"
|
||||
if cfg.policy.flex_auth_url:
|
||||
return f"zone-aware — flex-auth at {cfg.policy.flex_auth_url}"
|
||||
return "zone-aware — evaluator unconfigured; unknown-zone fail_open applies"
|
||||
fetch_url = cfg.policy.fetch_flex_auth_url
|
||||
if fetch_url:
|
||||
return f"zone-aware — fetches gated by flex-auth-ops-warden-fetch at {fetch_url}"
|
||||
return (
|
||||
"fail-closed — secret-read evaluator unconfigured "
|
||||
"(policy.fetch_flex_auth_url unset); fetches are refused"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1551,6 +1551,8 @@ def access(
|
|||
)
|
||||
if expanded.policy_ref:
|
||||
console.print(f" policy : {expanded.policy_ref} [dim]({gate})[/dim]")
|
||||
elif entry.exec_capable:
|
||||
console.print(f" gate : [dim]{gate}[/dim]")
|
||||
console.print(f" wiki : {entry.wiki_ref}")
|
||||
console.print(f" canon : {entry.canon_ref}")
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,11 @@ class CallerAuthConfig:
|
|||
|
||||
@dataclass
|
||||
class PolicyConfig:
|
||||
# SSH signing pin (flex-auth-ops-warden). Admits only resource.system ops-warden.
|
||||
flex_auth_url: Optional[str] = None
|
||||
# Secret-read pin (flex-auth-ops-warden-fetch, CUST-WP-0075). Used only by
|
||||
# check_fetch_policy; there is no fallback to flex_auth_url.
|
||||
fetch_flex_auth_url: Optional[str] = None
|
||||
zone_registry_path: Optional[Path] = None
|
||||
failure_modes: Dict[str, str] = field(
|
||||
default_factory=lambda: {
|
||||
|
|
@ -211,8 +215,12 @@ def load_config(path: Optional[Path] = None) -> WardenConfig:
|
|||
)
|
||||
zone_registry_path = policy_raw.get("zone_registry_path")
|
||||
flex_auth_url = str(policy_raw.get("flex_auth_url", "")).strip() or None
|
||||
fetch_flex_auth_url = (
|
||||
str(policy_raw.get("fetch_flex_auth_url", "") or "").strip() or None
|
||||
)
|
||||
policy_cfg = PolicyConfig(
|
||||
flex_auth_url=flex_auth_url,
|
||||
fetch_flex_auth_url=fetch_flex_auth_url,
|
||||
zone_registry_path=(
|
||||
Path(os.path.expanduser(str(zone_registry_path)))
|
||||
if zone_registry_path
|
||||
|
|
|
|||
|
|
@ -191,15 +191,21 @@ def check_fetch_policy(
|
|||
The action is ``read`` on a ``secret`` resource owned by another subsystem —
|
||||
ops-warden is the conduit, not the owner. Unresolved target workload identity
|
||||
selects the explicit ``unknown`` profile; no secret value enters the request.
|
||||
|
||||
The check goes only to ``cfg.fetch_flex_auth_url`` — the secret-read pin
|
||||
(flex-auth-ops-warden-fetch, CUST-WP-0075). It never falls back to the SSH
|
||||
signing pin (``cfg.flex_auth_url``), whose caller binding admits only the
|
||||
``ops-warden`` system. An unset or unreachable fetch evaluator always fails
|
||||
closed, whatever the zone failure mode.
|
||||
"""
|
||||
zone = "unknown"
|
||||
fail_closed = _is_fail_closed(cfg, zone)
|
||||
if cfg.flex_auth_url is None:
|
||||
_evaluator_failure(
|
||||
"flex-auth URL is not configured for security zone 'unknown'",
|
||||
fail_closed=fail_closed,
|
||||
if cfg.fetch_flex_auth_url is None:
|
||||
raise CAError(
|
||||
"secret-read evaluator not configured: set policy.fetch_flex_auth_url "
|
||||
"(flex-auth-ops-warden-fetch pin); fetch checks never use the SSH "
|
||||
"signing pin policy.flex_auth_url"
|
||||
)
|
||||
return None
|
||||
subject_id = os.environ.get(cfg.subject_env, "").strip() or "operator"
|
||||
request = {
|
||||
"subject": {"id": subject_id, "type": "operator", "tenant": cfg.tenant},
|
||||
|
|
@ -213,8 +219,8 @@ def check_fetch_policy(
|
|||
"context": {"need_id": need_id, "owner_repo": owner_repo, "domain": domain},
|
||||
}
|
||||
|
||||
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
|
||||
headers = _caller_headers(cfg, fail_closed=fail_closed)
|
||||
url = cfg.fetch_flex_auth_url.rstrip("/") + "/v1/check"
|
||||
headers = _caller_headers(cfg, fail_closed=True)
|
||||
try:
|
||||
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
|
|
@ -231,12 +237,10 @@ def check_fetch_policy(
|
|||
)
|
||||
return None
|
||||
except httpx.RequestError as e:
|
||||
_evaluator_failure(
|
||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} for security zone 'unknown'",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
)
|
||||
return None
|
||||
raise CAError(
|
||||
"secret-read evaluator (flex-auth-ops-warden-fetch) unreachable at "
|
||||
f"{cfg.fetch_flex_auth_url!r}; fetch refused (no fallback to the signing pin)"
|
||||
) from e
|
||||
|
||||
try:
|
||||
decision = response.json()
|
||||
|
|
|
|||
138
tests/test_policy_fetch_evaluator.py
Normal file
138
tests/test_policy_fetch_evaluator.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
"""Secret-read checks use their own evaluator pin, never the SSH signing pin (CUST-WP-0075)."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from warden.access import policy_gate_status
|
||||
from warden.ca import CAError
|
||||
from warden.config import PolicyConfig, load_config
|
||||
from warden.models import ActorType, CertSpec
|
||||
from warden.policy import check_fetch_policy, check_sign_policy
|
||||
|
||||
SIGN_URL = "http://127.0.0.1:19090"
|
||||
FETCH_URL = "http://127.0.0.1:19091"
|
||||
|
||||
|
||||
def _allow(url, json=None, headers=None, timeout=None):
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={"effect": "allow", "id": "decision:fetch", "reason": "secret_read_lane_matched"},
|
||||
request=httpx.Request("POST", url),
|
||||
)
|
||||
|
||||
|
||||
def _fetch(cfg):
|
||||
return check_fetch_policy(
|
||||
cfg, need_id="forgejo-admin-api-token", owner_repo="railiance-platform", domain=None
|
||||
)
|
||||
|
||||
|
||||
def test_fetch_check_goes_to_fetch_pin_never_sign_pin():
|
||||
cfg = PolicyConfig(flex_auth_url=SIGN_URL, fetch_flex_auth_url=FETCH_URL)
|
||||
with patch("warden.policy.httpx.post", side_effect=_allow) as post:
|
||||
assert _fetch(cfg) == "decision:fetch"
|
||||
urls = [c.args[0] for c in post.call_args_list]
|
||||
assert urls == [f"{FETCH_URL}/v1/check"]
|
||||
body = post.call_args.kwargs["json"]
|
||||
assert body["resource"]["system"] == "railiance-platform"
|
||||
assert body["action"] == "read"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure_mode", ["fail_open", "fail_closed"])
|
||||
def test_unset_fetch_url_fails_closed_without_using_sign_pin(failure_mode):
|
||||
cfg = PolicyConfig(flex_auth_url=SIGN_URL)
|
||||
cfg.failure_modes["unknown"] = failure_mode
|
||||
with patch("warden.policy.httpx.post") as post:
|
||||
with pytest.raises(CAError, match="policy.fetch_flex_auth_url") as err:
|
||||
_fetch(cfg)
|
||||
assert "secret-read evaluator not configured" in str(err.value)
|
||||
post.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure_mode", ["fail_open", "fail_closed"])
|
||||
def test_unreachable_fetch_url_fails_closed(failure_mode):
|
||||
cfg = PolicyConfig(flex_auth_url=SIGN_URL, fetch_flex_auth_url=FETCH_URL)
|
||||
cfg.failure_modes["unknown"] = failure_mode
|
||||
calls = []
|
||||
|
||||
def boom(url, **kwargs):
|
||||
calls.append(url)
|
||||
raise httpx.ConnectError("refused", request=httpx.Request("POST", url))
|
||||
|
||||
with patch("warden.policy.httpx.post", side_effect=boom):
|
||||
with pytest.raises(CAError, match="flex-auth-ops-warden-fetch\\) unreachable"):
|
||||
_fetch(cfg)
|
||||
assert calls == [f"{FETCH_URL}/v1/check"]
|
||||
|
||||
|
||||
def test_fetch_403_keeps_existing_refusal_text():
|
||||
cfg = PolicyConfig(flex_auth_url=SIGN_URL, fetch_flex_auth_url=FETCH_URL)
|
||||
response = httpx.Response(403, request=httpx.Request("POST", f"{FETCH_URL}/v1/check"))
|
||||
with patch("warden.policy.httpx.post", return_value=response):
|
||||
with pytest.raises(CAError) as err:
|
||||
_fetch(cfg)
|
||||
assert str(err.value) == (
|
||||
"flex-auth refused fetch policy check (HTTP 403); "
|
||||
"caller authentication or system binding was rejected"
|
||||
)
|
||||
|
||||
|
||||
def test_sign_check_still_uses_sign_pin(tmp_path):
|
||||
cfg = PolicyConfig(flex_auth_url=SIGN_URL, fetch_flex_auth_url=FETCH_URL)
|
||||
pub = tmp_path / "id.pub"
|
||||
pub.write_text("ssh-ed25519 AAAA test\n")
|
||||
spec = CertSpec(actor_name="agt-example", actor_type=ActorType.AGT,
|
||||
pubkey_path=pub, ttl_hours=1, principals=["agt"])
|
||||
with patch("warden.policy.httpx.post", side_effect=_allow) as post:
|
||||
assert check_sign_policy(cfg, spec) == "decision:fetch"
|
||||
assert [c.args[0] for c in post.call_args_list] == [f"{SIGN_URL}/v1/check"]
|
||||
assert post.call_args.kwargs["json"]["resource"]["system"] == "ops-warden"
|
||||
|
||||
|
||||
def _write_cfg(tmp_path, monkeypatch, policy_block):
|
||||
cfg = tmp_path / "warden.yaml"
|
||||
cfg.write_text(
|
||||
"backend: local\nca_key: /tmp/none\n" + policy_block
|
||||
)
|
||||
monkeypatch.setenv("WARDEN_CONFIG", str(cfg))
|
||||
return cfg
|
||||
|
||||
|
||||
def test_config_loads_fetch_url(tmp_path, monkeypatch):
|
||||
_write_cfg(tmp_path, monkeypatch,
|
||||
f"policy:\n flex_auth_url: {SIGN_URL}\n fetch_flex_auth_url: {FETCH_URL}\n")
|
||||
cfg = load_config()
|
||||
assert cfg.policy.flex_auth_url == SIGN_URL
|
||||
assert cfg.policy.fetch_flex_auth_url == FETCH_URL
|
||||
|
||||
|
||||
def test_gate_status_names_fetch_evaluator(tmp_path, monkeypatch):
|
||||
_write_cfg(tmp_path, monkeypatch,
|
||||
f"policy:\n flex_auth_url: {SIGN_URL}\n fetch_flex_auth_url: {FETCH_URL}\n")
|
||||
status = policy_gate_status()
|
||||
assert "flex-auth-ops-warden-fetch" in status and FETCH_URL in status
|
||||
assert SIGN_URL not in status
|
||||
|
||||
|
||||
def test_gate_status_unset_fetch_url_says_refused(tmp_path, monkeypatch):
|
||||
_write_cfg(tmp_path, monkeypatch, f"policy:\n flex_auth_url: {SIGN_URL}\n")
|
||||
status = policy_gate_status()
|
||||
assert "fetch_flex_auth_url unset" in status and "refused" in status
|
||||
|
||||
|
||||
def test_access_advisory_prints_fetch_evaluator(tmp_path, monkeypatch):
|
||||
from pathlib import Path
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from warden.cli import app
|
||||
|
||||
catalog = Path(__file__).resolve().parents[1] / "registry" / "routing" / "catalog.yaml"
|
||||
monkeypatch.setenv("WARDEN_ROUTING_CATALOG", str(catalog))
|
||||
_write_cfg(tmp_path, monkeypatch,
|
||||
f"policy:\n flex_auth_url: {SIGN_URL}\n fetch_flex_auth_url: {FETCH_URL}\n")
|
||||
result = CliRunner().invoke(app, ["access", "forgejo-admin-api-token"])
|
||||
assert result.exit_code == 0, result.output
|
||||
flat = " ".join(result.output.split())
|
||||
assert "flex-auth-ops-warden-fetch" in flat and FETCH_URL in flat
|
||||
|
|
@ -14,7 +14,7 @@ from warden.policy import check_fetch_policy, check_sign_policy
|
|||
@pytest.mark.parametrize("failure_mode", ["fail_open", "fail_closed"])
|
||||
@pytest.mark.parametrize("operation", ["sign", "fetch"])
|
||||
def test_explicit_refusal_blocks_under_every_outage_mode(tmp_path, status, failure_mode, operation):
|
||||
cfg = PolicyConfig(flex_auth_url="http://pdp.test")
|
||||
cfg = PolicyConfig(flex_auth_url="http://pdp.test", fetch_flex_auth_url="http://pdp.test")
|
||||
cfg.failure_modes["unknown"] = failure_mode
|
||||
public_key = tmp_path / "id.pub"
|
||||
public_key.write_text("ssh-ed25519 AAAA test\n")
|
||||
|
|
@ -37,7 +37,7 @@ def test_explicit_refusal_blocks_under_every_outage_mode(tmp_path, status, failu
|
|||
@pytest.mark.parametrize("status", [500, 503])
|
||||
@pytest.mark.parametrize("failure_mode", ["fail_open", "fail_closed"])
|
||||
def test_actual_evaluator_failure_retains_declared_outage_mode(status, failure_mode):
|
||||
cfg = PolicyConfig(flex_auth_url="http://pdp.test")
|
||||
cfg = PolicyConfig(flex_auth_url="http://sign.test", fetch_flex_auth_url="http://pdp.test")
|
||||
cfg.failure_modes["unknown"] = failure_mode
|
||||
response = httpx.Response(status, request=httpx.Request("POST", "http://pdp.test/v1/check"))
|
||||
with patch("warden.policy.httpx.post", return_value=response):
|
||||
|
|
|
|||
|
|
@ -203,11 +203,18 @@ def _proxy_env(monkeypatch, tmp_path):
|
|||
monkeypatch.setenv("WARDEN_CONFIG", str(_warden_yaml(tmp_path)))
|
||||
|
||||
|
||||
def test_cli_proxy_unknown_zone_fail_open_reaches_transport_guard(monkeypatch, tmp_path):
|
||||
def _allow_fetch_gate(monkeypatch):
|
||||
"""Stand in for an allowing secret-read evaluator (flex-auth-ops-warden-fetch)."""
|
||||
monkeypatch.setattr(
|
||||
"warden.policy.check_fetch_policy", lambda *a, **k: "decision:test-fetch"
|
||||
)
|
||||
|
||||
|
||||
def test_cli_proxy_unset_fetch_evaluator_fails_closed(monkeypatch, tmp_path):
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller")
|
||||
# The unknown-zone profile proceeds when no evaluator is configured, then
|
||||
# the independent safe-transport boundary still refuses captured stdout.
|
||||
# No policy.fetch_flex_auth_url: the fetch is refused before any transport,
|
||||
# whatever the unknown-zone failure mode (CUST-WP-0075).
|
||||
monkeypatch.setattr(
|
||||
"warden.proxy.subprocess.run",
|
||||
lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite gate")),
|
||||
|
|
@ -217,8 +224,9 @@ def test_cli_proxy_unknown_zone_fail_open_reaches_transport_guard(monkeypatch, t
|
|||
["access", "npm", "--domain", "coulomb_social", "--field", "NPM_AUTH_TOKEN",
|
||||
"--path", "platform/x/y/z", "--fetch"],
|
||||
)
|
||||
assert r.exit_code == 6
|
||||
assert "unknown-zone fail_open" in r.output
|
||||
assert r.exit_code == 4
|
||||
assert "fetch_flex_auth_url" in r.output
|
||||
assert "fail_open applied" not in r.output
|
||||
|
||||
|
||||
def test_cli_proxy_requires_caller_auth(monkeypatch, tmp_path):
|
||||
|
|
@ -240,7 +248,10 @@ def test_cli_explicit_policy_refusal_never_fetches_or_starts_child(monkeypatch,
|
|||
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
cfg = tmp_path / "warden.yaml"
|
||||
cfg.write_text(cfg.read_text() + "policy:\n flex_auth_url: http://pdp.test\n")
|
||||
cfg.write_text(
|
||||
cfg.read_text()
|
||||
+ "policy:\n flex_auth_url: http://sign.test\n fetch_flex_auth_url: http://pdp.test\n"
|
||||
)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller-test-value")
|
||||
monkeypatch.setattr(
|
||||
"warden.policy.httpx.post",
|
||||
|
|
@ -634,6 +645,7 @@ def test_build_wrapped_fetch_refuses_placeholder_path():
|
|||
def test_access_fetch_to_nonterminal_stdout_is_refused(tmp_path, monkeypatch):
|
||||
"""The anti-pattern: streaming a value to captured stdout is refused (exit 6)."""
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
_allow_fetch_gate(monkeypatch)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller-token") # G1 caller-auth precheck
|
||||
# The guard trips before the fetch runs; make a real bao call fail loudly if reached.
|
||||
monkeypatch.setattr(
|
||||
|
|
@ -649,6 +661,7 @@ def test_access_fetch_to_nonterminal_stdout_is_refused(tmp_path, monkeypatch):
|
|||
def test_access_fingerprint_masks_and_bypasses_stdout_guard(monkeypatch, tmp_path):
|
||||
"""--fingerprint prints a masked fingerprint (never the value) even to captured stdout."""
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
_allow_fetch_gate(monkeypatch)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller-token")
|
||||
|
||||
class _Fake:
|
||||
|
|
@ -668,6 +681,7 @@ def test_access_fingerprint_masks_and_bypasses_stdout_guard(monkeypatch, tmp_pat
|
|||
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)
|
||||
_allow_fetch_gate(monkeypatch)
|
||||
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.
|
||||
|
|
@ -687,6 +701,7 @@ def test_access_issued_agent_subject_refuses_raw_stream_without_legacy_marker(
|
|||
):
|
||||
"""WP-0034 T02: the issued KeyCape subject is sufficient for exit 7."""
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
_allow_fetch_gate(monkeypatch)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller-token")
|
||||
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
|
||||
monkeypatch.setenv(
|
||||
|
|
@ -706,6 +721,7 @@ def test_access_issued_agent_subject_refuses_raw_stream_without_legacy_marker(
|
|||
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)
|
||||
_allow_fetch_gate(monkeypatch)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller-token")
|
||||
monkeypatch.setenv("WARDEN_AGENT_ID", "grok")
|
||||
|
||||
|
|
|
|||
|
|
@ -128,6 +128,20 @@ value; the owner remains OpenBao, key-cape, flex-auth, or the routed subsystem.
|
|||
|
||||
---
|
||||
|
||||
## Policy gate for proxied credential reads
|
||||
|
||||
`warden access <need>` with `--exec`, `--fetch`, `--to-file`, `--wrap` or
|
||||
`--fingerprint` runs a flex-auth `read` check before any fetch. That check goes
|
||||
to the **fetch evaluator** `flex-auth-ops-warden-fetch`
|
||||
(`policy.fetch_flex_auth_url`, workstation `http://127.0.0.1:19091` through
|
||||
ops-bridge tunnel `flex-auth-ops-warden-fetch-railiance01`). It does not go to
|
||||
the SSH signing pin `flex-auth-ops-warden` (`policy.flex_auth_url`,
|
||||
`127.0.0.1:19090`), which admits only the `ops-warden` system. If the fetch
|
||||
evaluator is unset or unreachable, the fetch is refused (exit 4). It is never
|
||||
sent to the signing pin and never allowed by default. The `gate` line of
|
||||
`warden access <need>` names the evaluator in use. Config:
|
||||
`wiki/OpsWardenConfig.md` → "Two evaluators" (CUST-WP-0075).
|
||||
|
||||
## Examples — do NOT ask ops-warden to own or vend
|
||||
|
||||
| Request | Correct path |
|
||||
|
|
|
|||
|
|
@ -36,10 +36,12 @@ ca_key: ~/.ssh/ops-ca-user
|
|||
inventory_path: ~/.config/warden/inventory.yaml
|
||||
state_dir: ~/.local/state/warden
|
||||
|
||||
# Zone-aware flex-auth gate. With no URL, the explicit unknown/build profile
|
||||
# fails open and records that no evaluator decision was available.
|
||||
# Zone-aware flex-auth gate. With no signing URL, the explicit unknown/build
|
||||
# profile fails open for signing and records that no evaluator decision was
|
||||
# available. Fetch checks need fetch_flex_auth_url and fail closed without it.
|
||||
policy:
|
||||
flex_auth_url: http://127.0.0.1:8080
|
||||
fetch_flex_auth_url: http://127.0.0.1:8081
|
||||
zone_registry_path: /path/to/compiled-flex-auth-registry.json
|
||||
failure_modes:
|
||||
z0-experimental: fail_open
|
||||
|
|
@ -96,6 +98,7 @@ state_dir: ~/.local/state/warden
|
|||
# Configure after flex-auth ssh-certificate policies are deployed:
|
||||
# policy:
|
||||
# flex_auth_url: http://flex-auth-ops-warden.flex-auth.svc.cluster.local:8080
|
||||
# fetch_flex_auth_url: http://flex-auth-ops-warden-fetch.flex-auth.svc.cluster.local.:8080
|
||||
# zone_registry_path: /etc/warden/production_registry_snapshot.json
|
||||
```
|
||||
|
||||
|
|
@ -253,6 +256,7 @@ zone. Signing records `policy_decision_id` when present plus `policy_zone`,
|
|||
```yaml
|
||||
policy:
|
||||
flex_auth_url: http://127.0.0.1:8080
|
||||
fetch_flex_auth_url: http://127.0.0.1:8081
|
||||
zone_registry_path: registry/flex-auth/production_registry_snapshot.json
|
||||
failure_modes:
|
||||
z0-experimental: fail_open
|
||||
|
|
@ -274,6 +278,32 @@ for an unavailable or invalid evaluator.
|
|||
|
||||
Full request shape and rollout notes: `wiki/PolicyGatedSigning.md`.
|
||||
|
||||
### Two evaluators: signing and fetch checks (CUST-WP-0075)
|
||||
|
||||
| Key | Pin | Used by | Admits |
|
||||
|-----|-----|---------|--------|
|
||||
| `policy.flex_auth_url` | `flex-auth-ops-warden` (workstation `127.0.0.1:19090`) | `warden sign`, `warden issue` | `resource.system: ops-warden` only |
|
||||
| `policy.fetch_flex_auth_url` | `flex-auth-ops-warden-fetch` (workstation `127.0.0.1:19091`) | `warden access <need>` fetch transports (`check_fetch_policy`) | `read` for catalog lanes with status `active`, `exec_capable: true` and lane `secret`, for owner systems bound in access-engine `values/ops-warden-fetch.yaml` |
|
||||
|
||||
Both use the same caller identity (`policy.caller_auth`: the ops-warden service
|
||||
account token, audience `flex-auth`). For fetches a missing caller token refuses
|
||||
the fetch instead of sending an unauthenticated request.
|
||||
|
||||
The fetch check never falls back to `flex_auth_url`: the signing pin binds only
|
||||
the `ops-warden` system, so every fetch for another owner would get 403. If
|
||||
`fetch_flex_auth_url` is unset or the evaluator is unreachable, the fetch fails
|
||||
closed (exit 4) with a message naming the missing fetch evaluator, whatever the
|
||||
`unknown` failure mode says. An explicit 401/403 still maps to "flex-auth
|
||||
refused fetch policy check (HTTP 403); caller authentication or system binding
|
||||
was rejected". `warden access <need>` prints which evaluator gates fetches on
|
||||
its `gate` line (or `policy` line).
|
||||
|
||||
In-cluster address: `http://flex-auth-ops-warden-fetch.flex-auth.svc.cluster.local.:8080`
|
||||
(keep the trailing dot). access-engine `tools/gen_ops_warden_fetch.py` generates
|
||||
the pin's readable set from this repo's routing catalog. When you add or retire a
|
||||
lane of that kind, tell access-engine so it can regenerate the set and roll a new
|
||||
image. Deployment notes: access-engine `examples/ops-warden-fetch/README.md`.
|
||||
|
||||
---
|
||||
|
||||
## Environment variables
|
||||
|
|
@ -302,6 +332,29 @@ tunnels:
|
|||
cert_command: "warden sign agt-state-hub-bridge --pubkey ~/.ssh/agt-state-hub-bridge_ed25519.pub"
|
||||
```
|
||||
|
||||
Workstation tunnels to the two flex-auth pins (`remote_host` is each pin
|
||||
Service's ClusterIP, see access-engine `examples/ops-warden-fetch/README.md`):
|
||||
|
||||
```yaml
|
||||
tunnels:
|
||||
flex-auth-ops-warden-railiance01: # signing pin -> policy.flex_auth_url
|
||||
host: 92.205.62.239
|
||||
direction: local
|
||||
remote_host: <flex-auth-ops-warden ClusterIP>
|
||||
remote_port: 8080
|
||||
local_port: 19090
|
||||
health_check:
|
||||
url: http://127.0.0.1:19090/healthz
|
||||
flex-auth-ops-warden-fetch-railiance01: # fetch pin -> policy.fetch_flex_auth_url
|
||||
host: 92.205.62.239
|
||||
direction: local
|
||||
remote_host: <flex-auth-ops-warden-fetch ClusterIP>
|
||||
remote_port: 8080
|
||||
local_port: 19091
|
||||
health_check:
|
||||
url: http://127.0.0.1:19091/healthz
|
||||
```
|
||||
|
||||
`ops-bridge` runs `cert_command` before each SSH launch, captures stdout as the cert,
|
||||
and passes it alongside the private key via `ssh -i <key> -i <cert>`.
|
||||
See `wiki/CertCommandInterface.md` for the full contract and
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ flavor: residual
|
|||
owner: codex
|
||||
topic_slug: custodian
|
||||
created: "2026-09-08"
|
||||
updated: "2026-09-28"
|
||||
updated: "2026-10-03"
|
||||
origin: residual
|
||||
origin_ref: HFACT-WP-0001
|
||||
state_hub_workstream_id: "ae44a935-6fca-514c-a385-4550dd2b1fe8"
|
||||
|
|
@ -64,7 +64,8 @@ status: wait
|
|||
flavor: residual
|
||||
priority: high
|
||||
assignee: the-custodian
|
||||
blocking_reason: "The configured ops-warden caller represents ops-warden; credential requests name their owner as resource.system. Need the flex-auth/credential-owner contract for that exact delegated read, without broadening caller bindings or relabelling resource ownership."
|
||||
needs_human: true
|
||||
blocking_reason: "Code side done 2026-10-03 (policy.fetch_flex_auth_url). Waits on founder go: deploy access-engine pin flex-auth-ops-warden-fetch, add tunnel flex-auth-ops-warden-fetch-railiance01, set the key in live warden.yaml, then record positive and wrong-owner evidence."
|
||||
state_hub_task_id: "8ca28b63-2e04-5333-b635-011b124c1806"
|
||||
```
|
||||
|
||||
|
|
@ -116,3 +117,24 @@ confirms resource.system is runtime policy vocabulary, not a repository rename.
|
|||
Retain the explicit refusal. A native owner route's success does not grant this
|
||||
interim proxy general delegated-read authority; exact positive and caller/owner/
|
||||
tenant negative evidence is still required for the route that replaces it.
|
||||
|
||||
**T03 code side done 2026-10-03 (CUST-WP-0075, founder build-mode directive).**
|
||||
access-engine answered the contract question with a dedicated fetch evaluator:
|
||||
pin `flex-auth-ops-warden-fetch` (access-engine 02b15a7, `values/ops-warden-fetch.yaml`),
|
||||
same caller (ops-warden SA, audience flex-auth), owner systems bound per lane,
|
||||
request shape unchanged. ops-warden now sends `check_fetch_policy` only to the new
|
||||
key `policy.fetch_flex_auth_url`; signing keeps `policy.flex_auth_url`. An unset or
|
||||
unreachable fetch evaluator fails closed with a message naming it, with no fallback
|
||||
to the signing pin and no fail-open. 401/403 keep the existing refusal text.
|
||||
`warden access <need>` names the fetch evaluator on its `gate` (or `policy`) line. Tests:
|
||||
`tests/test_policy_fetch_evaluator.py`, plus updated proxy and refusal tests. Full
|
||||
suite: 494 passed, 4 deselected. Docs: `wiki/OpsWardenConfig.md` ("Two
|
||||
evaluators", tunnel example), `wiki/CredentialRouting.md`, production example.
|
||||
|
||||
Still open (founder go, `needs_human`): deploy the pin (access-engine
|
||||
ADHOC-2026-10-03-T02), add the ops-bridge tunnel (local 19091 → the pin's Service
|
||||
ClusterIP:8080), set `fetch_flex_auth_url: http://127.0.0.1:19091` in the live
|
||||
`~/.config/warden/warden.yaml`, then record exact positive evidence and
|
||||
wrong-owner/caller negative evidence before T03 closes. Until the key is set,
|
||||
every proxied fetch refuses with "secret-read evaluator not configured". Before
|
||||
that change, those fetches got a 403 from the signing pin.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue