feat: adopt security zones and explicit workload refs
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
This commit is contained in:
parent
12c637cbf2
commit
7ce58ae638
52 changed files with 1547 additions and 658 deletions
|
|
@ -1,7 +1,8 @@
|
|||
"""flex-auth policy gate for SSH signing (opt-in via warden.yaml)."""
|
||||
"""Zone-aware flex-auth policy gates for OpsWarden."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -20,21 +21,60 @@ def pubkey_fingerprint(pubkey_path: Path) -> str:
|
|||
return f"sha256:{digest}"
|
||||
|
||||
|
||||
def _caller_headers(cfg: PolicyConfig) -> dict[str, str]:
|
||||
def _caller_headers(cfg: PolicyConfig, *, fail_closed: bool) -> dict[str, str]:
|
||||
"""Bearer header identifying ops-warden itself to flex-auth (FLEX-WP-0016).
|
||||
|
||||
When the token cannot be obtained we refuse the call under ``fail_closed``
|
||||
When the token cannot be obtained we refuse the call under the selected
|
||||
zone's ``fail_closed`` behavior
|
||||
rather than silently falling back to an unauthenticated request — an
|
||||
unauthenticated call is exactly what keeps the flex-auth pin in ``warn``.
|
||||
"""
|
||||
try:
|
||||
return caller_auth_headers(cfg.caller_auth)
|
||||
except CallerIdentityError as e:
|
||||
if cfg.fail_closed:
|
||||
if fail_closed:
|
||||
raise CAError(f"flex-auth caller identity unavailable: {e}") from e
|
||||
return {}
|
||||
|
||||
|
||||
def _resource_zone(cfg: PolicyConfig, resource_id: str) -> str:
|
||||
"""Read a compiled resource zone; absence or ambiguity is always unknown."""
|
||||
if cfg.zone_registry_path is None:
|
||||
return "unknown"
|
||||
try:
|
||||
registry = json.loads(cfg.zone_registry_path.read_text())
|
||||
resources = registry["resource_manifests"][0]["resources"]
|
||||
resource = next(item for item in resources if item.get("id") == resource_id)
|
||||
attributes = resource.get("attributes") or {}
|
||||
if attributes.get("security_zone_admission") == "not-applicable":
|
||||
return "not-applicable"
|
||||
zone = str(attributes.get("security_zone") or "unknown")
|
||||
return zone if zone in cfg.failure_modes else "unknown"
|
||||
except (OSError, ValueError, KeyError, StopIteration, TypeError):
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _is_fail_closed(cfg: PolicyConfig, zone: str) -> bool:
|
||||
return cfg.failure_modes.get(zone, cfg.failure_modes["unknown"]) == "fail_closed"
|
||||
|
||||
|
||||
def _evaluator_failure(
|
||||
message: str,
|
||||
*,
|
||||
fail_closed: bool,
|
||||
cause: Exception | None = None,
|
||||
spec: CertSpec | None = None,
|
||||
) -> None:
|
||||
if fail_closed:
|
||||
if spec is not None:
|
||||
spec.policy_outcome = "fail_closed"
|
||||
if cause is None:
|
||||
raise CAError(message)
|
||||
raise CAError(message) from cause
|
||||
if spec is not None:
|
||||
spec.policy_outcome = "fail_open"
|
||||
|
||||
|
||||
def _subject_id(cfg: PolicyConfig, spec: CertSpec) -> str:
|
||||
return os.environ.get(cfg.subject_env, "").strip() or spec.actor_name
|
||||
|
||||
|
|
@ -42,11 +82,21 @@ def _subject_id(cfg: PolicyConfig, spec: CertSpec) -> str:
|
|||
def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None:
|
||||
"""Call flex-auth /v1/check before signing.
|
||||
|
||||
Returns decision id when policy is enabled and effect is allow.
|
||||
Returns None when policy is disabled.
|
||||
Raises CAError on deny or when fail_closed and flex-auth is unreachable.
|
||||
Returns a decision id on ``allow`` or ``audit_only``. A deny always blocks.
|
||||
Evaluator failures use the PEP-owned failure mode for the target workload's
|
||||
compiled zone; absent resolution is the explicit ``unknown`` profile.
|
||||
"""
|
||||
if not cfg.enabled:
|
||||
resource_id = f"ssh-cert:actor/{spec.actor_name}"
|
||||
zone = _resource_zone(cfg, resource_id)
|
||||
fail_closed = _is_fail_closed(cfg, zone)
|
||||
spec.policy_zone = zone
|
||||
spec.policy_failure_mode = "fail_closed" if fail_closed else "fail_open"
|
||||
if cfg.flex_auth_url is None:
|
||||
_evaluator_failure(
|
||||
f"flex-auth URL is not configured for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
|
||||
pubkey_path = Path(os.path.expanduser(str(spec.pubkey_path)))
|
||||
|
|
@ -76,37 +126,54 @@ def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None:
|
|||
}
|
||||
|
||||
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
|
||||
headers = _caller_headers(cfg)
|
||||
headers = _caller_headers(cfg, fail_closed=fail_closed)
|
||||
try:
|
||||
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPStatusError as e:
|
||||
if cfg.fail_closed:
|
||||
raise CAError(
|
||||
f"flex-auth denied or rejected sign policy check (HTTP {e.response.status_code})"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth rejected sign policy check (HTTP {e.response.status_code}) "
|
||||
f"for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
except httpx.RequestError as e:
|
||||
if cfg.fail_closed:
|
||||
raise CAError(
|
||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} "
|
||||
f"(fail_closed=true): {e}"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
decision = response.json()
|
||||
except ValueError as e:
|
||||
raise CAError("flex-auth returned non-JSON decision") from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth returned a non-JSON decision for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
|
||||
effect = str(decision.get("effect", "")).lower()
|
||||
decision_id = decision.get("id") or decision.get("request_id")
|
||||
if effect != "allow":
|
||||
if effect not in {"allow", "audit_only"}:
|
||||
spec.policy_outcome = "deny"
|
||||
reason = decision.get("reason") or "no reason provided"
|
||||
raise CAError(f"flex-auth denied SSH sign for {spec.actor_name!r}: {reason}")
|
||||
|
||||
if not decision_id:
|
||||
raise CAError("flex-auth allow decision missing id")
|
||||
_evaluator_failure(
|
||||
f"flex-auth {effect} decision missing id for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
spec.policy_outcome = effect
|
||||
return str(decision_id)
|
||||
|
||||
|
||||
|
|
@ -116,13 +183,17 @@ def check_fetch_policy(
|
|||
"""Call flex-auth /v1/check before proxying a non-SSH credential fetch (WP-0014).
|
||||
|
||||
The action is ``read`` on a ``secret`` resource owned by another subsystem —
|
||||
ops-warden is the conduit, not the owner. Returns the decision id on allow,
|
||||
None when policy is disabled, and raises CAError on deny (or on an unreachable
|
||||
flex-auth when fail_closed). No secret value is ever part of this request.
|
||||
ops-warden is the conduit, not the owner. Unresolved target workload identity
|
||||
selects the explicit ``unknown`` profile; no secret value enters the request.
|
||||
"""
|
||||
if not cfg.enabled:
|
||||
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,
|
||||
)
|
||||
return None
|
||||
|
||||
subject_id = os.environ.get(cfg.subject_env, "").strip() or "operator"
|
||||
request = {
|
||||
"subject": {"id": subject_id, "type": "operator", "tenant": cfg.tenant},
|
||||
|
|
@ -137,33 +208,44 @@ def check_fetch_policy(
|
|||
}
|
||||
|
||||
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
|
||||
headers = _caller_headers(cfg)
|
||||
headers = _caller_headers(cfg, fail_closed=fail_closed)
|
||||
try:
|
||||
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPStatusError as e:
|
||||
if cfg.fail_closed:
|
||||
raise CAError(
|
||||
f"flex-auth denied or rejected fetch policy check (HTTP {e.response.status_code})"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth rejected fetch policy check (HTTP {e.response.status_code})",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
)
|
||||
return None
|
||||
except httpx.RequestError as e:
|
||||
if cfg.fail_closed:
|
||||
raise CAError(
|
||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} (fail_closed=true): {e}"
|
||||
) from 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
|
||||
|
||||
try:
|
||||
decision = response.json()
|
||||
except ValueError as e:
|
||||
raise CAError("flex-auth returned non-JSON decision") from e
|
||||
_evaluator_failure(
|
||||
"flex-auth returned a non-JSON decision for security zone 'unknown'",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
)
|
||||
return None
|
||||
|
||||
effect = str(decision.get("effect", "")).lower()
|
||||
decision_id = decision.get("id") or decision.get("request_id")
|
||||
if effect != "allow":
|
||||
if effect not in {"allow", "audit_only"}:
|
||||
reason = decision.get("reason") or "no reason provided"
|
||||
raise CAError(f"flex-auth denied secret read for {need_id!r}: {reason}")
|
||||
if not decision_id:
|
||||
raise CAError("flex-auth allow decision missing id")
|
||||
return str(decision_id)
|
||||
_evaluator_failure(
|
||||
f"flex-auth {effect} decision missing id for security zone 'unknown'",
|
||||
fail_closed=fail_closed,
|
||||
)
|
||||
return None
|
||||
return str(decision_id)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue