Send a caller identity to flex-auth so policy.enabled can flip
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

flex-auth's flex-auth-ops-warden pin (FLEX-WP-0016) TokenReviews the caller and
binds resource.system: ops-warden to system:serviceaccount:ops-warden:ops-warden.
policy.py posted /v1/check with no Authorization header, so the pin logs
"caller authentication warning" and can only run callerAuth.mode: warn — which,
under ADHOC-2026-08-17-T01, is exactly what blocks policy.enabled: true.

- policy.caller_auth (none | file | env | command) + src/warden/caller_identity.py:
  token resolved per call, never cached, written, or logged (ADR-0002)
- both check_sign_policy and check_fetch_policy attach the bearer header; an
  unobtainable token fails closed rather than retrying anonymously
- scripts/check_policy_caller_identity.py: read-only gate, prints length and a
  truncated fingerprint only, distinguishes 401 (audience/binding) from 403
- example config: caller_auth block, and flex_auth_url corrected — it pointed at
  flex-auth.flex-auth.svc, a Service that does not exist
- WARDEN-WP-0031, PolicyGatedSigning caller-identity section and flip sequence

Default stays mode: none, so behaviour is unchanged until an operator opts in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-19 15:08:34 +02:00
parent 35aff380a3
commit 0a331413a2
9 changed files with 728 additions and 12 deletions

View file

@ -50,8 +50,12 @@ net-kingdom / info-tech-canon is owner-driven (tracked via coordination messages
**Policy gate** is shipped on the caller side (WP-0007) with production registry **Policy gate** is shipped on the caller side (WP-0007) with production registry
and smoke evidence (WP-0009 archived). flex-auth published the `ssh-certificate` and smoke evidence (WP-0009 archived). flex-auth published the `ssh-certificate`
policy package (FLEX-WP-0006). `policy.enabled` remains **false** in production policy package (FLEX-WP-0006) and now runs the in-cluster pin
until flex-auth is deployed to a reachable URL (flex-auth FLEX-WP-0007). `flex-auth-ops-warden` (FLEX-WP-0007 finished; FLEX-WP-0016). `policy.enabled`
remains **false**: that pin is in `callerAuth.mode: warn` because ops-warden sent
no caller identity. WP-0031 ships the calling side (`policy.caller_auth` +
`scripts/check_policy_caller_identity.py`); the flip waits on an operator picking
a token source and flex-auth then enforcing.
**ops-bridge cert_command pilot** is shipped to pilot-ready (WP-0016): a read-only **ops-bridge cert_command pilot** is shipped to pilot-ready (WP-0016): a read-only
readiness gate (`scripts/check_tunnel_cert_readiness.py`) plus an opt-in offline readiness gate (`scripts/check_tunnel_cert_readiness.py`) plus an opt-in offline
@ -273,6 +277,7 @@ for the rest.
| WP | Status | Focus | | WP | Status | Focus |
| --- | --- | --- | | --- | --- | --- |
| WP-0027 | `backlog` | Tamper-resistant credential governance + mass rotation/lockdown (Strand B follow-on to WP-0026) | | WP-0027 | `backlog` | Tamper-resistant credential governance + mass rotation/lockdown (Strand B follow-on to WP-0026) |
| WP-0031 | `active` | Calling-side identity for flex-auth — code + gate done; flip sequence waits on operator + flex-auth enforce |
| WP-0030 | `proposed` | Delegation register — record intended owner + blocker on every interim lane, `warden route gaps`, promotion gate | | WP-0030 | `proposed` | Delegation register — record intended owner + blocker on every interim lane, `warden route gaps`, promotion gate |
Remaining production distance is also in other repos' lanes (see Known gaps). Remaining production distance is also in other repos' lanes (see Known gaps).
@ -281,7 +286,7 @@ Remaining production distance is also in other repos' lanes (see Known gaps).
| Gap | Owner | Notes | | Gap | Owner | Notes |
| --- | --- | --- | | --- | --- | --- |
| flex-auth production runtime + registry deploy | flex-auth | **FLEX-WP-0007** — unblocks `policy.enabled: true` | | `callerAuth.mode: enforce` on the ops-warden pin | flex-auth | **FLEX-WP-0016 T03** — waits on WARDEN-WP-0031 T04, then unblocks `policy.enabled: true` |
| ops-bridge `cert_command` on live tunnels | ops-bridge | Playbook + readiness gate shipped (WP-0016); pilot cutover handed off, awaiting ops-bridge | | ops-bridge `cert_command` on live tunnels | ops-bridge | Playbook + readiness gate shipped (WP-0016); pilot cutover handed off, awaiting ops-bridge |
| Principals sync warden ↔ railiance-infra | ops-warden + infra | `scripts/check_principals_drift.py` — operator runs periodically | | Principals sync warden ↔ railiance-infra | ops-warden + infra | `scripts/check_principals_drift.py` — operator runs periodically |
| NK-WP-0009 joint SSH tutorial | net-kingdom | Parallel coordination track | | NK-WP-0009 joint SSH tutorial | net-kingdom | Parallel coordination track |
@ -347,7 +352,8 @@ Remaining production distance is also in other repos' lanes (see Known gaps).
- **Production sign:** verified 2026-06-18 (`history/2026-06-17-openbao-production-verify.md`) - **Production sign:** verified 2026-06-18 (`history/2026-06-17-openbao-production-verify.md`)
- **Access routing:** WP-0010 + WP-0011 shipped (`warden route`, pointer catalog) - **Access routing:** WP-0010 + WP-0011 shipped (`warden route`, pointer catalog)
- **Policy gate:** caller shipped (WP-0007); registry + smoke complete (WP-0009 archived). - **Policy gate:** caller shipped (WP-0007); registry + smoke complete (WP-0009 archived).
`policy.enabled: false` until flex-auth reachable (`FLEX-WP-0007`) `policy.enabled: false` — the `flex-auth-ops-warden` pin is in `warn` until
ops-warden sends a caller identity (WP-0031)
- **Workload posture:** WP-0015 shipped (standard, descriptors, `warden policy`, - **Workload posture:** WP-0015 shipped (standard, descriptors, `warden policy`,
conformance checker, dev doubles); canon landing owner-driven conformance checker, dev doubles); canon landing owner-driven
- **ops-bridge cert_command:** WP-0016 shipped to pilot-ready (readiness gate + - **ops-bridge cert_command:** WP-0016 shipped to pilot-ready (readiness gate +

View file

@ -19,9 +19,29 @@ state_dir: ~/.local/state/warden
# Registry: registry/flex-auth/production_registry_snapshot.json (build from inventory). # Registry: registry/flex-auth/production_registry_snapshot.json (build from inventory).
# See wiki/PolicyGatedSigning.md (operator checklist) and wiki/playbooks/operator-openbao-token-hygiene.md # See wiki/PolicyGatedSigning.md (operator checklist) and wiki/playbooks/operator-openbao-token-hygiene.md
policy: policy:
# Keep false until the flex-auth ops-warden pin runs callerAuth.mode: enforce.
# Gate: python scripts/check_policy_caller_identity.py (WARDEN-WP-0031)
enabled: false enabled: false
flex_auth_url: http://flex-auth.flex-auth.svc.cluster.local:8080 # The in-cluster pin for ops-warden's signing policy (FLEX-WP-0016). A bare
# 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
fail_closed: true fail_closed: true
tenant: tenant:platform tenant: tenant:platform
subject_env: WARDEN_POLICY_SUBJECT subject_env: WARDEN_POLICY_SUBJECT
system: ops-warden system: ops-warden
# How ops-warden proves it is ops-warden. flex-auth TokenReviews this bearer
# token and requires the principal system:serviceaccount:ops-warden:ops-warden
# for resource.system: ops-warden. Mode none sends no header, which is what
# holds the pin in warn.
caller_auth:
mode: none # none | file | env | command
# In-cluster PEP — projected ServiceAccount token, audience-bound:
# mode: file
# token_path: /var/run/secrets/flex-auth/token
# Workstation — mint a short-lived bound token per call:
# mode: command
# command: kubectl create token ops-warden -n ops-warden
# --audience flex-auth --duration 10m
token_env: WARDEN_POLICY_CALLER_TOKEN
audience: flex-auth

View file

@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""Readiness gate for the flex-auth ``policy.enabled`` flip (WARDEN-WP-0031).
flex-auth deployed ``flex-auth-ops-warden`` (FLEX-WP-0016) in ``callerAuth.mode:
warn``: it authenticates the caller with a Kubernetes TokenReview and binds
``resource.system: ops-warden`` to ``system:serviceaccount:ops-warden:ops-warden``,
but a caller that sends no ``Authorization`` header only produces a
``caller authentication warning`` and is still served. That pin cannot move to
``enforce`` — and therefore ``policy.enabled: true`` cannot be set — until
ops-warden's calling side actually presents a token.
This script asserts the calling side *without* flipping anything:
* warden.yaml loads and ``policy.caller_auth.mode`` is not ``none``,
* a caller token can actually be obtained (file / env / command),
* (optional, ``--url``) a live ``/v1/check`` against the warn pin returns a
decision **and** the response is reached with the header attached.
Exit 0 = ready to ask flex-auth to enforce, 1 = not ready, 2 = bad input.
The token is never printed, logged, or written anywhere — only its length and a
truncated SHA-256 fingerprint, which are safe to paste into a handoff message.
Usage:
python scripts/check_policy_caller_identity.py [--config ~/.config/warden/warden.yaml]
python scripts/check_policy_caller_identity.py --url http://127.0.0.1:19090
"""
from __future__ import annotations
import argparse
import hashlib
import sys
from pathlib import Path
from typing import List, Optional, Tuple
_SRC = Path(__file__).resolve().parent.parent / "src"
if _SRC.is_dir() and str(_SRC) not in sys.path:
sys.path.insert(0, str(_SRC))
from warden.caller_identity import ( # noqa: E402
CallerIdentityError,
resolve_caller_token,
)
from warden.config import ConfigError, load_config # noqa: E402
Check = Tuple[str, str, str]
def _fingerprint(token: str) -> str:
return "sha256:" + hashlib.sha256(token.encode()).hexdigest()[:12]
def run_checks(config_path: Optional[Path], url: Optional[str]) -> List[Check]:
checks: List[Check] = []
try:
cfg = load_config(config_path)
except ConfigError as e:
return [("fail", "warden.yaml", str(e))]
policy = cfg.policy
checks.append(
("ok", "warden.yaml", f"loaded; policy.enabled={str(policy.enabled).lower()}")
)
mode = policy.caller_auth.mode
if mode == "none":
checks.append(
(
"fail",
"caller_auth.mode",
"none — no Authorization header is sent; the flex-auth pin stays in warn",
)
)
return checks
checks.append(("ok", "caller_auth.mode", mode))
try:
token = resolve_caller_token(policy.caller_auth)
except CallerIdentityError as e:
checks.append(("fail", "caller token", str(e)))
return checks
assert token is not None
checks.append(
("ok", "caller token", f"obtained, {len(token)} chars, {_fingerprint(token)}")
)
target = url or policy.flex_auth_url
if url is None and not policy.enabled:
checks.append(
(
"skip",
"live /v1/check",
f"policy.enabled=false; pass --url to smoke {target} anyway",
)
)
return checks
import httpx # local import: the offline checks above must not need it
probe = {
"subject": {"id": "readiness", "type": "agt", "tenant": policy.tenant},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/agt-state-hub-bridge",
"type": "ssh-certificate",
"system": policy.system,
"tenant": policy.tenant,
},
"context": {"readiness_probe": True},
}
try:
response = httpx.post(
target.rstrip("/") + "/v1/check",
json=probe,
headers={"Authorization": f"Bearer {token}"},
timeout=10.0,
)
except httpx.RequestError as e:
checks.append(("fail", "live /v1/check", f"unreachable at {target}: {e}"))
return checks
if response.status_code == 401:
checks.append(
(
"fail",
"live /v1/check",
"401 — the token was sent but flex-auth did not accept it "
"(check the TokenReview audience and the ServiceAccount binding)",
)
)
elif response.status_code == 403:
checks.append(
(
"fail",
"live /v1/check",
f"403 — authenticated, but the principal may not represent "
f"system {policy.system!r}",
)
)
elif response.status_code >= 400:
checks.append(
("fail", "live /v1/check", f"HTTP {response.status_code} from {target}")
)
else:
try:
decision = response.json()
except ValueError:
checks.append(("fail", "live /v1/check", "non-JSON decision"))
return checks
effect = str(decision.get("effect", "?"))
decision_id = decision.get("id") or decision.get("request_id") or "?"
checks.append(
("ok", "live /v1/check", f"HTTP 200, effect={effect}, decision={decision_id}")
)
return checks
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--config", type=Path, default=None, help="path to warden.yaml")
parser.add_argument(
"--url",
default=None,
help="flex-auth base URL to smoke (e.g. a port-forward of the warn pin)",
)
args = parser.parse_args()
checks = run_checks(args.config, args.url)
glyph = {"ok": "✓", "fail": "✗", "skip": "·"}
print("flex-auth caller-identity readiness\n")
for status, label, detail in checks:
print(f" {glyph[status]} {label}: {detail}")
failed = [c for c in checks if c[0] == "fail"]
if failed:
print(
f"\nNOT READY — {len(failed)} check(s) failed. "
"Do not ask flex-auth to enforce, and do not set policy.enabled: true."
)
return 1
print(
"\nREADY — the calling side presents an identity. Next: tell flex-auth to set "
"callerAuth.mode: enforce on flex-auth-ops-warden, re-run this check, then set "
"policy.enabled: true with fail_closed: true."
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,92 @@
"""Caller identity for ops-warden's outbound flex-auth policy calls.
flex-auth's `flex-auth-ops-warden` pin (FLEX-WP-0016) authenticates the *caller*
before it evaluates the request: `Authorization: Bearer <token>` is passed to a
Kubernetes TokenReview, and `resource.system: ops-warden` is bound to the
principal `system:serviceaccount:ops-warden:ops-warden`. Until ops-warden sends
that header, the pin logs `caller authentication warning` and can only run in
`warn` mode — which is why `policy.enabled` cannot flip.
This module resolves the token at call time and hands it straight to the request.
Nothing is cached to disk, logged, or echoed: ops-warden carries the value, it
does not hold it (ADR-0002).
"""
from __future__ import annotations
import os
import subprocess
from warden.config import CallerAuthConfig
class CallerIdentityError(Exception):
"""Raised when a caller token was configured but could not be obtained."""
def resolve_caller_token(cfg: CallerAuthConfig) -> str | None:
"""Return the bearer token for flex-auth, or None when mode is ``none``.
Raises CallerIdentityError when a token was configured but is unavailable.
The token itself never appears in an exception message.
"""
mode = cfg.mode
if mode == "none":
return None
if mode == "file":
if cfg.token_path is None:
raise CallerIdentityError("caller_auth mode 'file' has no token_path")
try:
token = cfg.token_path.read_text()
except OSError as e:
raise CallerIdentityError(
f"caller token file unreadable: {cfg.token_path} ({e.strerror})"
) from e
elif mode == "env":
token = os.environ.get(cfg.token_env, "")
if not token.strip():
raise CallerIdentityError(
f"caller token env {cfg.token_env} is unset or empty"
)
elif mode == "command":
if not cfg.command:
raise CallerIdentityError("caller_auth mode 'command' has no command")
try:
result = subprocess.run(
cfg.command,
capture_output=True,
text=True,
timeout=30,
check=False,
)
except FileNotFoundError as e:
raise CallerIdentityError(
f"caller token command not found: {cfg.command[0]}"
) from e
except subprocess.TimeoutExpired as e:
raise CallerIdentityError("caller token command timed out") from e
if result.returncode != 0:
stderr = (result.stderr or "").strip().splitlines()
detail = stderr[-1] if stderr else f"exit {result.returncode}"
raise CallerIdentityError(f"caller token command failed: {detail}")
token = result.stdout
else:
raise CallerIdentityError(f"unsupported caller_auth mode {mode!r}")
token = token.strip()
if not token:
raise CallerIdentityError(f"caller_auth mode {mode!r} produced an empty token")
if any(ch.isspace() for ch in token):
# flex-auth rejects a bearer token containing whitespace outright.
raise CallerIdentityError(
f"caller_auth mode {mode!r} produced a token containing whitespace"
)
return token
def caller_auth_headers(cfg: CallerAuthConfig) -> dict[str, str]:
"""Headers to attach to a flex-auth /v1/check call ({} when unauthenticated)."""
token = resolve_caller_token(cfg)
if token is None:
return {}
return {"Authorization": f"Bearer {token}"}

View file

@ -2,9 +2,10 @@
from __future__ import annotations from __future__ import annotations
import os import os
import shlex
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Dict, Optional from typing import Dict, List, Optional
import yaml import yaml
@ -13,6 +14,35 @@ class ConfigError(Exception):
"""Raised when config is invalid or missing.""" """Raised when config is invalid or missing."""
@dataclass
class CallerAuthConfig:
"""How ops-warden proves *its own* identity to flex-auth (FLEX-WP-0016).
flex-auth's ops-warden pin authenticates the caller with a Kubernetes
TokenReview and binds ``resource.system: ops-warden`` to the principal
``system:serviceaccount:ops-warden:ops-warden``. A workstation ``warden
sign`` is not a ServiceAccount, so the token has to come from somewhere:
``none`` send no ``Authorization`` header (pre-FLEX-WP-0016 behaviour;
accepted only while that pin runs ``callerAuth.mode: warn``)
``file`` read a projected ServiceAccount token from ``token_path``
(in-cluster PEP, audience-bound by the projection)
``env`` read the token from ``token_env``
``command`` run ``command`` and use its stdout, e.g.
``kubectl create token ops-warden -n ops-warden
--audience flex-auth --duration 10m``
ops-warden never stores the token: it is read, sent, and dropped
(ADR-0002 — transparent conduit, not a broker).
"""
mode: str = "none"
token_path: Optional[Path] = None
token_env: str = "WARDEN_POLICY_CALLER_TOKEN"
command: Optional[List[str]] = None
audience: str = "flex-auth"
@dataclass @dataclass
class PolicyConfig: class PolicyConfig:
enabled: bool = False enabled: bool = False
@ -21,6 +51,7 @@ class PolicyConfig:
tenant: str = "tenant:platform" tenant: str = "tenant:platform"
subject_env: str = "WARDEN_POLICY_SUBJECT" subject_env: str = "WARDEN_POLICY_SUBJECT"
system: str = "ops-warden" system: str = "ops-warden"
caller_auth: "CallerAuthConfig" = field(default_factory=lambda: CallerAuthConfig())
@dataclass @dataclass
@ -117,6 +148,33 @@ def load_config(path: Optional[Path] = None) -> WardenConfig:
) )
policy_raw = raw.get("policy") or {} policy_raw = raw.get("policy") or {}
caller_raw = policy_raw.get("caller_auth") or {}
caller_command = caller_raw.get("command")
if isinstance(caller_command, str):
caller_command = shlex.split(caller_command)
elif caller_command is not None:
caller_command = [str(part) for part in caller_command]
caller_token_path = caller_raw.get("token_path")
caller_cfg = CallerAuthConfig(
mode=str(caller_raw.get("mode", "none")).strip().lower(),
token_path=(
Path(os.path.expanduser(str(caller_token_path)))
if caller_token_path
else None
),
token_env=str(caller_raw.get("token_env", "WARDEN_POLICY_CALLER_TOKEN")),
command=caller_command,
audience=str(caller_raw.get("audience", "flex-auth")),
)
if caller_cfg.mode not in {"none", "file", "env", "command"}:
raise ConfigError(
f"policy.caller_auth.mode must be none|file|env|command, "
f"got {caller_cfg.mode!r}"
)
if caller_cfg.mode == "file" and caller_cfg.token_path is None:
raise ConfigError("policy.caller_auth.token_path is required for mode: file")
if caller_cfg.mode == "command" and not caller_cfg.command:
raise ConfigError("policy.caller_auth.command is required for mode: command")
policy_cfg = PolicyConfig( policy_cfg = PolicyConfig(
enabled=bool(policy_raw.get("enabled", False)), enabled=bool(policy_raw.get("enabled", False)),
flex_auth_url=str(policy_raw.get("flex_auth_url", "http://127.0.0.1:8080")), flex_auth_url=str(policy_raw.get("flex_auth_url", "http://127.0.0.1:8080")),
@ -124,6 +182,7 @@ def load_config(path: Optional[Path] = None) -> WardenConfig:
tenant=str(policy_raw.get("tenant", "tenant:platform")), tenant=str(policy_raw.get("tenant", "tenant:platform")),
subject_env=str(policy_raw.get("subject_env", "WARDEN_POLICY_SUBJECT")), subject_env=str(policy_raw.get("subject_env", "WARDEN_POLICY_SUBJECT")),
system=str(policy_raw.get("system", "ops-warden")), system=str(policy_raw.get("system", "ops-warden")),
caller_auth=caller_cfg,
) )
return WardenConfig( return WardenConfig(

View file

@ -8,6 +8,7 @@ from pathlib import Path
import httpx import httpx
from warden.ca import CAError from warden.ca import CAError
from warden.caller_identity import CallerIdentityError, caller_auth_headers
from warden.config import PolicyConfig from warden.config import PolicyConfig
from warden.models import CertSpec from warden.models import CertSpec
@ -19,6 +20,21 @@ def pubkey_fingerprint(pubkey_path: Path) -> str:
return f"sha256:{digest}" return f"sha256:{digest}"
def _caller_headers(cfg: PolicyConfig) -> 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``
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:
raise CAError(f"flex-auth caller identity unavailable: {e}") from e
return {}
def _subject_id(cfg: PolicyConfig, spec: CertSpec) -> str: def _subject_id(cfg: PolicyConfig, spec: CertSpec) -> str:
return os.environ.get(cfg.subject_env, "").strip() or spec.actor_name return os.environ.get(cfg.subject_env, "").strip() or spec.actor_name
@ -60,8 +76,9 @@ def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None:
} }
url = cfg.flex_auth_url.rstrip("/") + "/v1/check" url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
headers = _caller_headers(cfg)
try: try:
response = httpx.post(url, json=request, timeout=10.0) response = httpx.post(url, json=request, headers=headers, timeout=10.0)
response.raise_for_status() response.raise_for_status()
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
if cfg.fail_closed: if cfg.fail_closed:
@ -120,8 +137,9 @@ def check_fetch_policy(
} }
url = cfg.flex_auth_url.rstrip("/") + "/v1/check" url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
headers = _caller_headers(cfg)
try: try:
response = httpx.post(url, json=request, timeout=10.0) response = httpx.post(url, json=request, headers=headers, timeout=10.0)
response.raise_for_status() response.raise_for_status()
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
if cfg.fail_closed: if cfg.fail_closed:

View file

@ -1,4 +1,5 @@
"""Tests for warden.policy — flex-auth gate.""" """Tests for warden.policy — flex-auth gate."""
import subprocess
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -6,7 +7,12 @@ import httpx
import pytest import pytest
from warden.ca import CAError from warden.ca import CAError
from warden.config import PolicyConfig from warden.caller_identity import (
CallerIdentityError,
caller_auth_headers,
resolve_caller_token,
)
from warden.config import CallerAuthConfig, PolicyConfig
from warden.models import ActorType, CertSpec from warden.models import ActorType, CertSpec
from warden.policy import check_sign_policy, pubkey_fingerprint from warden.policy import check_sign_policy, pubkey_fingerprint
@ -137,4 +143,123 @@ def test_subject_from_env(tmp_path, monkeypatch):
check_sign_policy(cfg, _spec(pubkey)) check_sign_policy(cfg, _spec(pubkey))
body = post.call_args[1]["json"] body = post.call_args[1]["json"]
assert body["subject"]["id"] == "iam:bernd" assert body["subject"]["id"] == "iam:bernd"
# --- caller identity (FLEX-WP-0016 / WARDEN-WP-0031) -----------------------
def test_caller_auth_none_sends_no_header():
assert caller_auth_headers(CallerAuthConfig()) == {}
def test_caller_auth_file_reads_projected_token(tmp_path):
token_file = tmp_path / "token"
token_file.write_text("sa-token-value\n")
cfg = CallerAuthConfig(mode="file", token_path=token_file)
assert caller_auth_headers(cfg) == {"Authorization": "Bearer sa-token-value"}
def test_caller_auth_file_missing_raises(tmp_path):
cfg = CallerAuthConfig(mode="file", token_path=tmp_path / "absent")
with pytest.raises(CallerIdentityError, match="unreadable"):
resolve_caller_token(cfg)
def test_caller_auth_env_mode(monkeypatch):
monkeypatch.setenv("WARDEN_POLICY_CALLER_TOKEN", " env-token ")
assert resolve_caller_token(CallerAuthConfig(mode="env")) == "env-token"
monkeypatch.setenv("WARDEN_POLICY_CALLER_TOKEN", "")
with pytest.raises(CallerIdentityError, match="unset or empty"):
resolve_caller_token(CallerAuthConfig(mode="env"))
def test_caller_auth_command_mode_uses_stdout(monkeypatch):
cfg = CallerAuthConfig(mode="command", command=["kubectl", "create", "token"])
def fake_run(cmd, **kwargs):
assert cmd == cfg.command
return subprocess.CompletedProcess(cmd, 0, stdout="minted-token\n", stderr="")
monkeypatch.setattr(subprocess, "run", fake_run)
assert resolve_caller_token(cfg) == "minted-token"
def test_caller_auth_command_failure_message_excludes_token(monkeypatch):
cfg = CallerAuthConfig(mode="command", command=["kubectl", "create", "token"])
def fake_run(cmd, **kwargs):
return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="error: forbidden\n")
monkeypatch.setattr(subprocess, "run", fake_run)
with pytest.raises(CallerIdentityError, match="error: forbidden"):
resolve_caller_token(cfg)
def test_caller_auth_rejects_whitespace_token(tmp_path):
token_file = tmp_path / "token"
token_file.write_text("two words")
cfg = CallerAuthConfig(mode="file", token_path=token_file)
with pytest.raises(CallerIdentityError, match="whitespace"):
resolve_caller_token(cfg)
def test_sign_policy_sends_authorization_header(tmp_path, monkeypatch):
"""The header flex-auth's ops-warden pin needs to leave warn mode."""
from warden import policy as policy_mod
token_file = tmp_path / "token"
token_file.write_text("sa-token-value")
pubkey = tmp_path / "id.pub"
pubkey.write_text("ssh-ed25519 AAAA test\n")
cfg = PolicyConfig(
enabled=True,
caller_auth=CallerAuthConfig(mode="file", token_path=token_file),
)
spec = CertSpec(
actor_name="agt-state-hub-bridge",
actor_type=ActorType.AGT,
principals=["agt"],
ttl_hours=24,
pubkey_path=pubkey,
)
seen = {}
class _Response:
status_code = 200
def raise_for_status(self):
return None
def json(self):
return {"effect": "allow", "id": "decision:49350f1064f674d7"}
def fake_post(url, json=None, headers=None, timeout=None):
seen["headers"] = headers
return _Response()
monkeypatch.setattr(policy_mod.httpx, "post", fake_post)
assert policy_mod.check_sign_policy(cfg, spec) == "decision:49350f1064f674d7"
assert seen["headers"] == {"Authorization": "Bearer sa-token-value"}
def test_sign_policy_fail_closed_when_caller_token_unavailable(tmp_path):
from warden.ca import CAError
from warden import policy as policy_mod
pubkey = tmp_path / "id.pub"
pubkey.write_text("ssh-ed25519 AAAA test\n")
cfg = PolicyConfig(
enabled=True,
fail_closed=True,
caller_auth=CallerAuthConfig(mode="file", token_path=tmp_path / "absent"),
)
spec = CertSpec(
actor_name="agt-state-hub-bridge",
actor_type=ActorType.AGT,
principals=["agt"],
ttl_hours=24,
pubkey_path=pubkey,
)
with pytest.raises(CAError, match="caller identity unavailable"):
policy_mod.check_sign_policy(cfg, spec)

View file

@ -66,6 +66,8 @@ policy:
tenant: tenant:platform tenant: tenant:platform
subject_env: WARDEN_POLICY_SUBJECT subject_env: WARDEN_POLICY_SUBJECT
system: ops-warden system: ops-warden
caller_auth: # how ops-warden identifies itself — see "Caller identity"
mode: none
``` ```
| Key | Default | Description | | Key | Default | Description |
@ -76,6 +78,7 @@ policy:
| `tenant` | `tenant:platform` | Tenant sent in subject and resource | | `tenant` | `tenant:platform` | Tenant sent in subject and resource |
| `subject_env` | `WARDEN_POLICY_SUBJECT` | Env var for IAM subject id override | | `subject_env` | `WARDEN_POLICY_SUBJECT` | Env var for IAM subject id override |
| `system` | `ops-warden` | Resource system identifier | | `system` | `ops-warden` | Resource system identifier |
| `caller_auth.mode` | `none` | `none` \| `file` \| `env` \| `command` — source of the `Authorization` bearer token flex-auth TokenReviews ([Caller identity](#caller-identity-warden-wp-0031)) |
Set `WARDEN_POLICY_SUBJECT` to the caller's IAM profile `sub` when available. Set `WARDEN_POLICY_SUBJECT` to the caller's IAM profile `sub` when available.
If unset, the actor name is used as subject id. If unset, the actor name is used as subject id.
@ -196,6 +199,75 @@ Evidence: `history/2026-06-23-flex-auth-policy-gate-production-smoke.md`.
--- ---
## Caller identity (WARDEN-WP-0031)
flex-auth authenticates the **caller** before it evaluates the request. The
in-cluster pin `flex-auth-ops-warden` (FLEX-WP-0016) passes the bearer token to a
Kubernetes TokenReview and requires the principal
`system:serviceaccount:ops-warden:ops-warden` for any request carrying
`resource.system: ops-warden`.
Until ops-warden sends that header the pin logs
```
caller authentication warning: caller is not authenticated
```
and can only run `callerAuth.mode: warn`. Per ADHOC-2026-08-17-T01,
`policy.enabled` must not flip while `/v1/check` still answers unauthenticated
callers — so **the missing header is what blocks the flip**, and it is
ops-warden's to fix, not flex-auth's.
### Configure a token source
```yaml
policy:
caller_auth:
mode: none # none | file | env | command
token_path: /var/run/secrets/flex-auth/token # mode: file
token_env: WARDEN_POLICY_CALLER_TOKEN # mode: env
command: kubectl create token ops-warden -n ops-warden --audience flex-auth --duration 10m
audience: flex-auth
```
| Mode | Use it when |
| --- | --- |
| `none` | Default. No header — pre-FLEX-WP-0016 behaviour, only viable while the pin is in `warn` |
| `file` | In-cluster PEP with a projected, audience-bound ServiceAccount token |
| `env` | The token is already in the environment (CI, a wrapper) |
| `command` | Workstation `warden sign` — mint a short-lived bound token per call |
A workstation is not a ServiceAccount, which is why `command` exists. The token is
read, sent, and dropped: never cached, written, or logged (ADR-0002). If a token
is configured but cannot be obtained, the sign is **refused** under
`fail_closed` — falling back to an anonymous call would defeat the gate.
### Readiness gate
```bash
python scripts/check_policy_caller_identity.py # offline
python scripts/check_policy_caller_identity.py --url http://127.0.0.1:19090 # port-forward of the warn pin
```
Exit 0 ready / 1 not ready / 2 bad input. It prints the token's length and a
truncated fingerprint, never the value, so its output is safe to paste into a
handoff message. A live `401` means the token was sent but rejected (audience or
binding); `403` means it authenticated but may not represent `system: ops-warden`.
### Flip sequence
1. Configure `caller_auth`; `check_policy_caller_identity.py` exits 0 offline.
2. Smoke against a port-forward of the warn pin. The evidence is the **absence**
of `caller authentication warning` in its log, not the `allow` — warn serves
unauthenticated callers too, so an allow alone proves nothing.
3. Ask flex-auth to set `callerAuth.mode: enforce` (their FLEX-WP-0016 T03).
4. Re-run the gate against the enforcing pin.
5. Only then: `policy.enabled: true`, `fail_closed: true`.
Reversing 3 and 5 401s every `warden sign`.
---
## Production rollout ## Production rollout
**Keep `policy.enabled: false` until flex-auth is reachable** at `policy.flex_auth_url` **Keep `policy.enabled: false` until flex-auth is reachable** at `policy.flex_auth_url`
@ -210,7 +282,9 @@ with `fail_closed: true`, unreachable flex-auth blocks all signs.
| 3 | ops-warden | Regenerate registry from inventory: `scripts/build_flex_auth_registry.py` | | 3 | ops-warden | Regenerate registry from inventory: `scripts/build_flex_auth_registry.py` |
| 4 | ops-warden | Local smoke: `./scripts/policy_gate_production_smoke.sh` | | 4 | ops-warden | Local smoke: `./scripts/policy_gate_production_smoke.sh` |
| 5 | operator | Vault smoke: `make credential-exec-ops-warden-smoke` in `railiance-platform` (or manual `SMOKE_VAULT=1` fallback) | | 5 | operator | Vault smoke: `make credential-exec-ops-warden-smoke` in `railiance-platform` (or manual `SMOKE_VAULT=1` fallback) |
| 6 | operator | Set `policy.flex_auth_url` in `~/.config/warden/warden.yaml` | | 6 | operator | Set `policy.flex_auth_url` in `~/.config/warden/warden.yaml` — the pin is `flex-auth-ops-warden.flex-auth.svc.cluster.local:8080`, reached from a workstation via port-forward or tunnel |
| 6a | ops-warden | Configure `policy.caller_auth`; `scripts/check_policy_caller_identity.py` exits 0 (see **Caller identity**) |
| 6b | flex-auth | Set `callerAuth.mode: enforce` on the ops-warden pin (FLEX-WP-0016 T03) |
| 7 | operator | Set `policy.enabled: true`; keep `fail_closed: true` | | 7 | operator | Set `policy.enabled: true`; keep `fail_closed: true` |
| 8 | operator | Allow smoke: `warden sign <actor>` — `signatures.log` has `policy_decision_id` | | 8 | operator | Allow smoke: `warden sign <actor>` — `signatures.log` has `policy_decision_id` |
| 9 | operator | Deny smoke: e.g. `--ttl` above max — CLI shows flex-auth `reason`, no cert | | 9 | operator | Deny smoke: e.g. `--ttl` above max — CLI shows flex-auth `reason`, no cert |

View file

@ -0,0 +1,133 @@
---
id: WARDEN-WP-0031
type: workplan
title: "Calling-side identity for flex-auth, so policy.enabled can flip"
domain: infotech
repo: ops-warden
status: active
owner: ops-warden
topic_slug: netkingdom
planning_priority: P1
depends_on_workplans:
- WARDEN-WP-0007
related_workplans:
- WARDEN-WP-0009
created: "2026-08-19"
updated: "2026-08-19"
---
# WARDEN-WP-0031 — Calling-side identity for flex-auth
flex-auth shipped `flex-auth-ops-warden` (FLEX-WP-0016 T01/T02): an
independently rollable in-cluster pin carrying ops-warden's production registry
and policy package, on digest `sha256:138aa347…`, at
`flex-auth-ops-warden.flex-auth.svc.cluster.local:8080`.
It runs `callerAuth.mode: warn`, and it says why in its own logs:
```
caller authentication warning: caller is not authenticated
```
`src/warden/policy.py` posted `/v1/check` with **no `Authorization` header**.
flex-auth authenticates the caller with a Kubernetes TokenReview and binds
`resource.system: ops-warden` to `system:serviceaccount:ops-warden:ops-warden`;
an unauthenticated caller can only be served in `warn`. So the pin cannot
enforce, and per ADHOC-2026-08-17-T01 — `policy.enabled` must not flip anywhere
while `/v1/check` still answers unauthenticated callers — `policy.enabled` stays
false. The gap is ours, not flex-auth's, and this workplan closes it.
Warn is also not A2 evidence: a request that succeeds because failures are
downgraded proves nothing about the enforcing path.
## Ownership
| Concern | Owner |
| --- | --- |
| The pin, its digest, `callerAuth.mode` | flex-auth |
| Sending a caller identity on `/v1/check` | **ops-warden** (this workplan) |
| Choosing the token source on a given host | ops-warden operator |
| `policy.enabled: true` in `warden.yaml` | ops-warden operator |
## Design note — fail closed on identity too
When a caller token is configured but cannot be obtained, `check_sign_policy`
raises under `fail_closed` rather than retrying unauthenticated. Falling back to
an anonymous call is precisely the behaviour that keeps the pin in `warn`; a
gate that silently degrades to the ungated path is not a gate (ADR-0004's choke
point argument, applied to ops-warden as a caller).
## Tasks
```task
id: WARDEN-WP-0031-T01
status: done
priority: high
```
**Caller identity on the outbound policy call.** `policy.caller_auth` in
`warden.yaml` (`mode: none | file | env | command`, `token_path`, `token_env`,
`command`, `audience`); `src/warden/caller_identity.py` resolves the token at
call time and never caches, logs, or echoes it (ADR-0002); both
`check_sign_policy` and `check_fetch_policy` attach `Authorization: Bearer …`.
Whitespace-bearing and empty tokens are rejected before the call, because
flex-auth rejects them outright.
Done 2026-08-19. `mode: none` remains the default, so behaviour is unchanged
until an operator opts in. Tests in `tests/test_policy.py`.
```task
id: WARDEN-WP-0031-T02
status: done
priority: high
```
**Readiness gate.** `scripts/check_policy_caller_identity.py` — read-only:
config loads, mode is not `none`, a token is actually obtainable, and with
`--url` a live `/v1/check` against a port-forward of the warn pin. Prints the
token's length and a truncated SHA-256 fingerprint only, so its output is safe
to paste into a handoff. Exit 0 ready / 1 not ready / 2 bad input. Distinguishes
401 (token not accepted — audience or binding) from 403 (authenticated but not
allowed to represent `system: ops-warden`).
Done 2026-08-19.
```task
id: WARDEN-WP-0031-T03
status: done
priority: medium
```
**Docs.** `examples/warden.production.example.yaml` gains the `caller_auth`
block with both realistic sources, and its `flex_auth_url` is corrected — the
example pointed at `flex-auth.flex-auth.svc.cluster.local`, a Service that does
not exist. `wiki/PolicyGatedSigning.md` gains the caller-identity section and
the flip sequence.
Done 2026-08-19.
```task
id: WARDEN-WP-0031-T04
status: wait
priority: high
```
**Pick the token source and prove it against the warn pin.** Operator work on
the real host: `kubectl create token` (workstation) or a projected token
(in-cluster PEP), then
`python scripts/check_policy_caller_identity.py --url http://127.0.0.1:19090`
against a port-forward. Expect `effect=allow` for `agt-state-hub-bridge` while
the warn log stops printing `caller authentication warning` — the absence of
that line, not the allow, is the evidence.
```task
id: WARDEN-WP-0031-T05
status: wait
priority: high
```
**Sequence the flip.** Only after T04: tell flex-auth to set
`callerAuth.mode: enforce` on `flex-auth-ops-warden` (their FLEX-WP-0016 T03),
re-run the gate against the enforcing pin, and only then set
`policy.enabled: true` with `fail_closed: true`. Flipping before enforce buys
nothing; flipping before T04 401s every `warden sign`.