Refuse explicit policy authentication and binding denials before side effects
Assistant: codex Assistant-Model: gpt-5.6-luna Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
11e5e8be0f
commit
31d9b6671c
5 changed files with 151 additions and 0 deletions
|
|
@ -131,6 +131,12 @@ def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None:
|
|||
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code in {401, 403}:
|
||||
spec.policy_outcome = "deny"
|
||||
raise CAError(
|
||||
f"flex-auth refused sign policy check (HTTP {e.response.status_code}); "
|
||||
"caller authentication or system binding was rejected"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth rejected sign policy check (HTTP {e.response.status_code}) "
|
||||
f"for security zone {zone!r}",
|
||||
|
|
@ -213,6 +219,11 @@ def check_fetch_policy(
|
|||
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code in {401, 403}:
|
||||
raise CAError(
|
||||
f"flex-auth refused fetch policy check (HTTP {e.response.status_code}); "
|
||||
"caller authentication or system binding was rejected"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth rejected fetch policy check (HTTP {e.response.status_code})",
|
||||
fail_closed=fail_closed,
|
||||
|
|
|
|||
48
tests/test_policy_http_refusal.py
Normal file
48
tests/test_policy_http_refusal.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
"""An explicit caller refusal cannot authorize a CA or credential side effect."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from warden.ca import CAError
|
||||
from warden.config import PolicyConfig
|
||||
from warden.models import ActorType, CertSpec
|
||||
from warden.policy import check_fetch_policy, check_sign_policy
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [401, 403])
|
||||
@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.failure_modes["unknown"] = failure_mode
|
||||
public_key = tmp_path / "id.pub"
|
||||
public_key.write_text("ssh-ed25519 AAAA test\n")
|
||||
spec = CertSpec(actor_name="agt-example", actor_type=ActorType.AGT,
|
||||
pubkey_path=public_key, ttl_hours=1, principals=["agt"])
|
||||
response = httpx.Response(status, request=httpx.Request("POST", "http://pdp.test/v1/check"),
|
||||
text="untrusted response body must not be exposed")
|
||||
with patch("warden.policy.httpx.post", return_value=response):
|
||||
with pytest.raises(CAError, match=f"HTTP {status}") as error:
|
||||
if operation == "sign":
|
||||
check_sign_policy(cfg, spec)
|
||||
else:
|
||||
check_fetch_policy(cfg, need_id="forgejo-admin-api-token",
|
||||
owner_repo="railiance-platform", domain=None)
|
||||
assert "untrusted response" not in str(error.value)
|
||||
if operation == "sign":
|
||||
assert spec.policy_outcome == "deny"
|
||||
|
||||
|
||||
@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.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):
|
||||
if failure_mode == "fail_closed":
|
||||
with pytest.raises(CAError, match=f"HTTP {status}"):
|
||||
check_fetch_policy(cfg, need_id="example", owner_repo="example", domain=None)
|
||||
else:
|
||||
assert check_fetch_policy(cfg, need_id="example", owner_repo="example", domain=None) is None
|
||||
|
|
@ -234,6 +234,28 @@ def test_cli_proxy_requires_caller_auth(monkeypatch, tmp_path):
|
|||
assert r.exit_code == 3
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [401, 403])
|
||||
def test_cli_explicit_policy_refusal_never_fetches_or_starts_child(monkeypatch, tmp_path, status):
|
||||
import httpx
|
||||
|
||||
_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")
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller-test-value")
|
||||
monkeypatch.setattr(
|
||||
"warden.policy.httpx.post",
|
||||
lambda *a, **k: httpx.Response(status, request=httpx.Request("POST", "http://pdp.test/v1/check")),
|
||||
)
|
||||
calls = []
|
||||
for name in ("proxy_exec", "proxy_fetch", "proxy_fetch_to_file", "proxy_fetch_wrapped"):
|
||||
monkeypatch.setattr("warden.proxy." + name, lambda *a, **k: calls.append(True))
|
||||
result = runner.invoke(app, ["access", "forgejo-admin-api-token", "--exec", "--field", "API_TOKEN", "--", "true"])
|
||||
assert result.exit_code == 4
|
||||
assert f"HTTP {status}" in result.output
|
||||
assert "fail_open applied" not in result.output
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_cli_proxy_rejects_retired_no_policy_bypass(monkeypatch, tmp_path):
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller")
|
||||
|
|
|
|||
|
|
@ -134,6 +134,11 @@ truth.
|
|||
|
||||
## Caller identity
|
||||
|
||||
An HTTP 401 or 403 from the policy service refuses the operation under every
|
||||
outage profile. It is an explicit caller-authentication or system-binding refusal,
|
||||
not evaluator unavailability. Resolve the admitted caller and resource contract;
|
||||
do not retry with a different resource owner or disable caller enforcement.
|
||||
|
||||
The production flex-auth pin authenticates ops-warden with Kubernetes
|
||||
TokenReview and binds `resource.system: ops-warden` to
|
||||
`system:serviceaccount:ops-warden:ops-warden`. Supported token sources are:
|
||||
|
|
|
|||
65
workplans/WARDEN-WP-0039-explicit-policy-refusal.md
Normal file
65
workplans/WARDEN-WP-0039-explicit-policy-refusal.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
id: WARDEN-WP-0039
|
||||
type: workplan
|
||||
title: "Preserve explicit policy caller refusals before credential and CA effects"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: active
|
||||
owner: codex
|
||||
topic_slug: custodian
|
||||
created: "2026-09-08"
|
||||
updated: "2026-09-08"
|
||||
origin: residual
|
||||
origin_ref: HFACT-WP-0001
|
||||
---
|
||||
|
||||
The factory critical-path review reproduced a live HTTP 403 from the reachable
|
||||
PDP for the configured ops-warden caller and a railiance-platform secret read.
|
||||
`check_fetch_policy` treated it as evaluator unavailability and used unknown-zone
|
||||
fail-open. Caller-auth rejection is an explicit refusal, not a transport outage.
|
||||
|
||||
## Preserve explicit authentication and binding refusals
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0039-T01
|
||||
status: done
|
||||
priority: high
|
||||
assignee: the-custodian
|
||||
```
|
||||
|
||||
HTTP 401 and 403 stop signing and credential fetch irrespective of the outage
|
||||
profile. Do not expose response bodies or alter system identity to bypass the
|
||||
refusal. Preserve configured behavior for genuine evaluator failures. Prove
|
||||
both permissive and closed outage profiles and refusal before child execution.
|
||||
|
||||
## Verify and publish the correction
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0039-T02
|
||||
status: progress
|
||||
priority: high
|
||||
assignee: the-custodian
|
||||
```
|
||||
|
||||
Run the policy and proxy suites, retain a value-free live refusal receipt,
|
||||
publish the reviewed source, and verify the installed CLI refuses before any
|
||||
credential transport. A refusal is not a successful credential-read admission.
|
||||
|
||||
## Resolve the credential proxy's admitted policy binding
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0039-T03
|
||||
status: wait
|
||||
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."
|
||||
```
|
||||
|
||||
Consume the existing native-lane handoff (WARDEN-WP-0033 / SECRETS-WP-0006)
|
||||
and flex-auth caller contract. Establish whether this interim transport needs an
|
||||
admitted dedicated policy route or must finish its native handoff. Retain the
|
||||
refusal until that contract yields positive and wrong-caller/owner/tenant
|
||||
negative evidence. No credential read, secret generation, or policy grant is
|
||||
authorized by this workplan alone. HFACT-WP-0001-T03 consumes this return.
|
||||
|
||||
Validation: 429 tests passed (4 integration tests deselected by the repository default); Ruff passed for changed Python files. Full tests used the declared phase-memory source and an isolated temporary memory store. The focused policy/proxy suite passed 69 tests. The existing authenticated SSH policy probe still returns HTTP 200/ALLOW, decision:f3f7c88f9585582a; the credential-owner request returns 403. No CA issue or credential read was performed by these probes.
|
||||
Loading…
Add table
Add a link
Reference in a new issue