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
and smoke evidence (WP-0009 archived). flex-auth published the `ssh-certificate`
policy package (FLEX-WP-0006). `policy.enabled` remains **false** in production
until flex-auth is deployed to a reachable URL (flex-auth FLEX-WP-0007).
policy package (FLEX-WP-0006) and now runs the in-cluster pin
`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
readiness gate (`scripts/check_tunnel_cert_readiness.py`) plus an opt-in offline
@ -273,6 +277,7 @@ for the rest.
| WP | Status | Focus |
| --- | --- | --- |
| 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 |
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 |
| --- | --- | --- |
| 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 |
| 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 |
@ -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`)
- **Access routing:** WP-0010 + WP-0011 shipped (`warden route`, pointer catalog)
- **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`,
conformance checker, dev doubles); canon landing owner-driven
- **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).
# See wiki/PolicyGatedSigning.md (operator checklist) and wiki/playbooks/operator-openbao-token-hygiene.md
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
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
tenant: tenant:platform
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
import os
import shlex
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, Optional
from typing import Dict, List, Optional
import yaml
@ -13,6 +14,35 @@ class ConfigError(Exception):
"""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
class PolicyConfig:
enabled: bool = False
@ -21,6 +51,7 @@ class PolicyConfig:
tenant: str = "tenant:platform"
subject_env: str = "WARDEN_POLICY_SUBJECT"
system: str = "ops-warden"
caller_auth: "CallerAuthConfig" = field(default_factory=lambda: CallerAuthConfig())
@dataclass
@ -117,6 +148,33 @@ def load_config(path: Optional[Path] = None) -> WardenConfig:
)
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(
enabled=bool(policy_raw.get("enabled", False)),
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")),
subject_env=str(policy_raw.get("subject_env", "WARDEN_POLICY_SUBJECT")),
system=str(policy_raw.get("system", "ops-warden")),
caller_auth=caller_cfg,
)
return WardenConfig(

View file

@ -8,6 +8,7 @@ from pathlib import Path
import httpx
from warden.ca import CAError
from warden.caller_identity import CallerIdentityError, caller_auth_headers
from warden.config import PolicyConfig
from warden.models import CertSpec
@ -19,6 +20,21 @@ def pubkey_fingerprint(pubkey_path: Path) -> str:
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:
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"
headers = _caller_headers(cfg)
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()
except httpx.HTTPStatusError as e:
if cfg.fail_closed:
@ -120,8 +137,9 @@ def check_fetch_policy(
}
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
headers = _caller_headers(cfg)
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()
except httpx.HTTPStatusError as e:
if cfg.fail_closed:

View file

@ -1,4 +1,5 @@
"""Tests for warden.policy — flex-auth gate."""
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch
@ -6,7 +7,12 @@ import httpx
import pytest
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.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))
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
subject_env: WARDEN_POLICY_SUBJECT
system: ops-warden
caller_auth: # how ops-warden identifies itself — see "Caller identity"
mode: none
```
| Key | Default | Description |
@ -76,6 +78,7 @@ policy:
| `tenant` | `tenant:platform` | Tenant sent in subject and resource |
| `subject_env` | `WARDEN_POLICY_SUBJECT` | Env var for IAM subject id override |
| `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.
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
**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` |
| 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) |
| 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` |
| 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 |

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`.