feat: adopt security zones and explicit workload refs
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
This commit is contained in:
tegwick 2026-08-22 15:36:37 +02:00
parent 12c637cbf2
commit 7ce58ae638
52 changed files with 1547 additions and 658 deletions

View file

@ -135,11 +135,12 @@ schedule rather than in lockstep. The binding condition is sequencing, not a dat
flex-auth warn-only -> ops-warden pre-sign gate presents its SA token
-> logs clean of unauthenticated callers
-> flex-auth fail-closed
-> policy.enabled: true (ops-warden side, FLEX-WP-0007)
-> zone-specific enforce stance (flex-auth policy package)
```
`policy.enabled` must not flip anywhere while `/v1/check` still answers
unauthenticated callers.
An enforce stance must not be assigned while `/v1/check` still answers
unauthenticated callers. ops-warden has no global enable switch or gate bypass;
it applies the compiled zone stance and its local per-zone failure mode.
**Division of the call:** the mechanism above is an architecture recommendation and
ops-warden's to make. Accepting the pod-spec change and the rollout timing are the
@ -179,4 +180,4 @@ and automation work — not platform-admin equivalents on hosts.
- `wiki/AccessRouting.md` — issue-vs-route role and boundary
- `wiki/CredentialRouting.md`
- `wiki/PolicyGatedSigning.md` (future flex-auth hook)
- `net-kingdom/docs/platform-identity-security-architecture.md`
- `net-kingdom/docs/platform-identity-security-architecture.md`

View file

@ -76,7 +76,7 @@ prevent, and duplicates OpenBao.
| --- | --- | --- |
| **G1** | **Caller identity, never warden's** | The proxy runs the owner's tool with the caller's own environment; ops-warden injects no token of its own. Secret lanes require the caller to already hold a credential (`caller_auth_present`), else they fail with the auth pointer. |
| **G2** | **Transit only — no persistence/logging of values** | Sanctioned transports keep the value off stdout: `--out` writes it to a mode-0600 file, `--exec` injects it into a child process env, `--wrap` returns a single-use OpenBao wrapping token (not the value). Raw `--fetch` to stdout is refused for captured/piped output (`--unsafe-stdout` overrides for a human terminal). warden never writes the value to disk or log; the audit record is **metadata only**. (WP-0026 T02) |
| **G3** | **Policy gate before fetch** | `check_fetch_policy` (flex-auth) runs before any secret-lane fetch. With `policy.enabled: false` the proxy refuses unless `--no-policy` is given to acknowledge proxying ungated. |
| **G3** | **Policy gate before fetch** | `check_fetch_policy` (flex-auth) runs before every secret-lane fetch. Zone membership selects stance and local failure mode; an unresolved workload uses the explicit `unknown` profile. The retired `--no-policy` flag is rejected, so this gate has no CLI bypass. |
The catalog side enforces a fourth, upstream guard: **handoff fields are templates,
never values.** `_assert_no_secret_material` rejects any known token prefix or

View file

@ -36,11 +36,19 @@ ca_key: ~/.ssh/ops-ca-user
inventory_path: ~/.config/warden/inventory.yaml
state_dir: ~/.local/state/warden
# Optional flex-auth gate (default off — see wiki/PolicyGatedSigning.md)
# Zone-aware flex-auth gate. With no URL, the explicit unknown/build profile
# fails open and records that no evaluator decision was available.
policy:
enabled: false
flex_auth_url: http://127.0.0.1:8080
fail_closed: true
zone_registry_path: /path/to/compiled-flex-auth-registry.json
failure_modes:
z0-experimental: fail_open
z1-operational: fail_open
z2-protected: fail_open
z2-continuity: fail_open
z3-critical: fail_closed
unknown: fail_open
not-applicable: fail_closed
```
### Bootstrapping the local CA key
@ -85,11 +93,10 @@ vault:
inventory_path: ~/.config/warden/inventory.yaml
state_dir: ~/.local/state/warden
# Enable after flex-auth ssh-certificate policies are deployed:
# Configure after flex-auth ssh-certificate policies are deployed:
# policy:
# enabled: true
# flex_auth_url: http://flex-auth.flex-auth.svc.cluster.local:8080
# fail_closed: true
# flex_auth_url: http://flex-auth-ops-warden.flex-auth.svc.cluster.local:8080
# zone_registry_path: /etc/warden/production_registry_snapshot.json
```
### Example — in-cluster caller (pod or trusted host)
@ -235,22 +242,36 @@ hosts:
---
## Policy gate (flex-auth, opt-in)
## Policy gate (flex-auth, zone-aware)
When `policy.enabled: true`, `warden sign` and `warden issue` call flex-auth
`POST /v1/check` before signing. Deny or unreachable (with `fail_closed: true`)
blocks issuance. Allowed decisions store `policy_decision_id` in `signatures.log`.
`warden sign` and `warden issue` evaluate flex-auth whenever a URL is configured.
The target resource's compiled `security_zone` selects the local PEP failure
mode. A rendered deny always blocks; evaluator failure blocks or proceeds per
zone. Signing records `policy_decision_id` when present plus `policy_zone`,
`policy_failure_mode`, and `policy_outcome`.
```yaml
policy:
enabled: false # default — no behavior change
flex_auth_url: http://127.0.0.1:8080
fail_closed: true # deny when flex-auth unreachable
zone_registry_path: registry/flex-auth/production_registry_snapshot.json
failure_modes:
z0-experimental: fail_open
z1-operational: fail_open
z2-protected: fail_open
z2-continuity: fail_open
z3-critical: fail_closed
unknown: fail_open
not-applicable: fail_closed
tenant: tenant:platform
subject_env: WARDEN_POLICY_SUBJECT
system: ops-warden
```
`policy.enabled` and the global `policy.fail_closed` are retired; configuration
loading rejects them with a migration error. Stance is owned by flex-auth's
versioned policy package, not this block. The failure-mode map is PEP behavior
for an unavailable or invalid evaluator.
Full request shape and rollout notes: `wiki/PolicyGatedSigning.md`.
---
@ -261,7 +282,7 @@ Full request shape and rollout notes: `wiki/PolicyGatedSigning.md`.
|----------|---------|-------------|
| `WARDEN_CONFIG` | `~/.config/warden/warden.yaml` | Config file path |
| `VAULT_TOKEN` | — | API token for `backend: vault` (OpenBao or Vault; name configurable via `vault.token_env`) |
| `WARDEN_POLICY_SUBJECT` | — | IAM subject id for flex-auth checks (when `policy.enabled`) |
| `WARDEN_POLICY_SUBJECT` | — | IAM subject id for flex-auth checks |
---
@ -284,4 +305,4 @@ tunnels:
`ops-bridge` runs `cert_command` before each SSH launch, captures stdout as the cert,
and passes it alongside the private key via `ssh -i <key> -i <cert>`.
See `wiki/CertCommandInterface.md` for the full contract and
`wiki/playbooks/ops-bridge-tunnel-cert.md` for static-key → cert_command migration.
`wiki/playbooks/ops-bridge-tunnel-cert.md` for static-key → cert_command migration.

View file

@ -1,347 +1,196 @@
# Policy-Gated SSH Signing
# Zone-aware policy-gated signing
Date: 2026-06-23
Status: **implemented (opt-in)** — WARDEN-WP-0007; policy package confirmed FLEX-WP-0006
Ops-warden asks flex-auth for a decision before SSH certificate issuance. The
gate is resource-scoped through security-zone membership; there is no repo-wide
enable switch.
By default `warden sign` authorizes via **inventory allow-list** and TTL policy
only. When `policy.enabled: true` in `warden.yaml`, ops-warden calls flex-auth
before signing and records the decision id in `signatures.log`.
Authority stays split:
---
- flex-auth owns the versioned pre-sign stance (`enforced`, `advisory`, or
`exempt`) and returns the decision;
- ops-warden owns what the PEP does when flex-auth is unavailable or invalid;
- the workload owner declares identity and zone membership;
- zone-engine owns `security-zones_v0.1` admission and resolution semantics.
## Flow
Binding decisions: `ADR-0009` (current) and `ADR-0006` (superseded rationale).
## Request path
```text
warden sign <actor> --pubkey <path>
|
v
Load actor from inventory (type, principals, ttl)
|
v
policy.enabled?
no -> skip
yes -> flex-auth POST /v1/check
|
+-- DENY / unreachable (fail_closed) -> CAError
|
v ALLOW
CABackend.sign() (local or OpenBao SSH engine)
|
v
Append signatures.log (+ policy_decision_id when set)
warden sign <actor>
-> inventory, principal, actor-type, and TTL checks
-> resource id ssh-cert:actor/<actor>
-> read compiled security_zone for that resource
-> POST flex-auth /v1/check with authenticated caller identity
allow -> sign; record decision and zone evidence
audit_only -> sign; record advisory decision and zone evidence
deny -> refuse before the CA backend
unavailable/invalid
-> apply that zone's PEP failure mode
-> record fail_open when issuance proceeds
```
The same gate runs for `warden issue` (local backend only).
The request contains actor id/type, requested principals, TTL, and a SHA-256
fingerprint of the public key. It never contains a private key or secret value.
---
## Compiled membership
## flex-auth request shape
| Field | Source |
| --- | --- |
| `subject.id` | `WARDEN_POLICY_SUBJECT` env var, or actor name |
| `subject.type` | Actor type (`adm` / `agt` / `atm`) |
| `tenant` | `policy.tenant` (default `tenant:platform`) |
| `resource.id` | `ssh-cert:actor/<actor-name>` |
| `resource.type` | `ssh-certificate` |
| `action` | `sign` |
| `context.principals` | From inventory |
| `context.actor_type` | adm \| agt \| atm |
| `context.pubkey_fingerprint` | SHA256 of pubkey text |
| `context.ttl_hours` | Requested TTL |
flex-auth must return `effect: allow` and an `id` (or `request_id`) on allow.
Deny responses include a `reason` surfaced in the CLI error.
---
## Configuration
`scripts/build_flex_auth_registry.py` compiles inventory actor resources. Each
actor carries an explicit `zone_subject`:
```yaml
# warden.yaml — policy gate (opt-in, default off)
policy:
enabled: false
flex_auth_url: http://127.0.0.1:8080
fail_closed: true
tenant: tenant:platform
subject_env: WARDEN_POLICY_SUBJECT
system: ops-warden
caller_auth: # how ops-warden identifies itself — see "Caller identity"
mode: none
actors:
agt-state-hub-bridge:
type: agt
principals: [agt-task-bridge]
ttl_hours: 24
zone_subject:
applicability: applicable
workload_id: ops-bridge-tunnel
```
| Key | Default | Description |
| --- | --- | --- |
| `enabled` | `false` | When `true`, call flex-auth before every sign/issue |
| `flex_auth_url` | `http://127.0.0.1:8080` | flex-auth base URL |
| `fail_closed` | `true` | Deny sign when flex-auth is unreachable or returns HTTP error |
| `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)) |
The compiler consumes zone-engine's resolved view when available and emits
resource attributes:
Set `WARDEN_POLICY_SUBJECT` to the caller's IAM profile `sub` when available.
If unset, the actor name is used as subject id.
---
## Versioning
| Version | Gate | Status |
| --- | --- | --- |
| **v1** | Inventory + TTL max | Shipped |
| **v2** | flex-auth opt-in via `policy.enabled` | Shipped (WP-0007) |
| **v2.1** | Identity claims required for `adm` signs | Planned |
| **v3** | Tenant-scoped policies per `tenant:*` | Planned |
---
## What stays in inventory
- Actor registration (name, type, default principals, default TTL)
- Host reference documentation
- Scorecard local checks
flex-auth decides **whether this sign request is allowed now**; inventory
defines **what the actor is allowed to request**.
---
## flex-auth policy package (FLEX-WP-0006)
flex-auth owns the `ssh-certificate` / `sign` policy package. ops-warden consumes
it via `POST /v1/check` when `policy.enabled: true`.
**Handoff (canonical):** `~/flex-auth/docs/ops-warden-policy-gate-handoff.md`
| Asset | flex-auth path |
| --- | --- |
| Policy package | `examples/ops-warden/policy_package.md` |
| Allow/deny fixtures | `examples/ops-warden/policy_fixtures.yaml` |
| Registry snapshot | `examples/ops-warden/registry_snapshot.json` |
| Subject manifest | `examples/ops-warden/subject_manifest.yaml` |
| Resource manifest | `examples/ops-warden/resource_manifest.yaml` |
### Tenant and subject bindings
| Field | Value |
| --- | --- |
| Tenant | `tenant:platform` (`policy.tenant`) |
| Resource system | `ops-warden` (`policy.system`) |
| Resource type | `ssh-certificate` |
| Action | `sign` |
| Resource id | `ssh-cert:actor/<actor-name>` |
| Actor type | Example flex-auth subject | ops-warden inventory name pattern |
| --- | --- | --- |
| `adm` | `platform-steward` | `adm-*` |
| `agt` | `ci-deploy-agent` | `agt-*` |
| `atm` | `backup-automation` | `atm-*` |
**Subject id sent to flex-auth:** `WARDEN_POLICY_SUBJECT` when set, otherwise the
inventory actor name. flex-auth may also allow `iam:<actor-name>` when listed in
`allowed_subjects` on the resource.
**Principals and TTL:** Taken from the sign request (inventory defaults). flex-auth
denies when principals are empty/disallowed or TTL exceeds `max_ttl_hours` on the
registered resource.
### Fixture coverage (flex-auth)
Allow: `fixture:ops-warden-adm-sign-allow`, `fixture:ops-warden-agt-sign-allow`,
`fixture:ops-warden-atm-sign-allow`.
Deny: `fixture:ops-warden-unknown-subject-deny`,
`fixture:ops-warden-actor-type-mismatch-deny`, `fixture:ops-warden-ttl-above-max-deny`,
`fixture:ops-warden-disallowed-principal-deny`,
`fixture:ops-warden-missing-fingerprint-deny`.
### Local smoke
```bash
# flex-auth (from ~/flex-auth)
flex-auth serve --addr 127.0.0.1:8080 \
--registry examples/ops-warden/registry_snapshot.json \
--policy examples/ops-warden/policy_package.md \
--log /tmp/flex-auth-ops-warden-decisions.jsonl
# warden.yaml — policy.enabled: true, flex_auth_url pointing at flex-auth
# Use an actor registered in the flex-auth registry (example fixtures use
# template names; production needs a registry slice for real inventory actors).
```json
{
"workload_id": "ops-bridge-tunnel",
"security_zone": "z2-continuity",
"security_zone_admission": "satisfied",
"security_zone_revision": "sha256:..."
}
```
Local end-to-end evidence: `history/2026-06-23-flex-auth-policy-gate-local-smoke.md`.
If the workload reference or resolved membership is absent, the resource says
`security_zone: unknown` with a reason. A native non-workload actor/action says
`security_zone_admission: not-applicable`. The compiler never parses a path or
repository name to repair missing identity.
### Production registry from inventory
`trust_zone: platform` was a dormant, unrelated field and is retired. It must
not coexist with `security_zone` as a competing membership source.
Build a flex-auth registry snapshot that mirrors `inventory.yaml` actors:
Build the snapshot:
```bash
python scripts/build_flex_auth_registry.py ~/.config/warden/inventory.yaml \
python3 scripts/build_flex_auth_registry.py \
~/.config/warden/inventory.yaml \
--zone-resolutions /path/to/zone-resolved-view.json \
-o registry/flex-auth/production_registry_snapshot.json
flex-auth load-registry --file registry/flex-auth/production_registry_snapshot.json
```
Re-run after adding or changing actors. Deploy the snapshot to the production
flex-auth runtime together with `~/flex-auth/examples/ops-warden/policy_package.md`.
Omitting `--zone-resolutions` is safe: applicable actors resolve `unknown`, not
to a guessed zone.
Smoke (non-secret):
## PEP failure modes
```bash
./scripts/policy_gate_production_smoke.sh
# OpenBao-backed — preferred: credential broker (no manual VAULT_TOKEN):
cd ~/railiance-platform && make credential-exec-ops-warden-smoke
# Manual fallback when broker unavailable:
SMOKE_VAULT=1 ./scripts/policy_gate_production_smoke.sh
```
The initial build profile accepted by ops-warden is:
Evidence: `history/2026-06-23-flex-auth-policy-gate-production-smoke.md`.
| Zone/result | Dependency failure |
| --- | --- |
| `z0-experimental` | `fail_open` |
| `z1-operational` | `fail_open` |
| `z2-protected` | `fail_open` |
| `z2-continuity` | `fail_open` |
| `z3-critical` | `fail_closed` |
| `unknown` | `fail_open` under the versioned build profile |
| `not-applicable` | `fail_closed` for this pre-sign PEP |
---
These are dependency failure modes, not policy stance. A rendered deny always
blocks. The `unknown` row does not grant membership or an exception; it is the
explicit build-stage treatment until authoritative declarations land.
## 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
Configuration:
```yaml
policy:
flex_auth_url: http://127.0.0.1:19090
zone_registry_path: registry/flex-auth/production_registry_snapshot.json
failure_modes:
z0-experimental: fail_open
z1-operational: fail_open
z2-protected: fail_open
z2-continuity: fail_open
z3-critical: fail_closed
unknown: fail_open
not-applicable: fail_closed
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: command
command:
- kubectl
- create
- token
- ops-warden
- -n
- ops-warden
- --audience
- flex-auth
- --duration
- 10m
```
| 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 |
`policy.enabled` and the global `policy.fail_closed` are retired. The loader
rejects either key so old and new controls cannot coexist as two sources of
truth.
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.
## Caller identity
### Readiness gate
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:
- `file` — projected ServiceAccount token for an in-cluster PEP;
- `command` — short-lived `kubectl create token` on a workstation;
- `env` — attended fallback;
- `none` — no identity header; only useful for an intentionally unauthenticated
development evaluator.
Tokens are resolved per call, never cached, logged, or echoed. Under a
fail-closed zone, an unavailable caller token blocks. Under a fail-open zone it
becomes a recorded evaluator failure; ops-warden never retries anonymously with
a secret copied into its own state.
Re-establish the value-safe caller proof:
```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
python3 scripts/check_policy_caller_identity.py \
--url http://127.0.0.1:19090
```
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`.
Expected evidence is HTTP 200 with a decision id and anonymous HTTP 401 on the
enforcing pin. The script reports only token length and a truncated fingerprint.
### Enablement is deferred by decision (ADR-0006)
## Audit evidence
`policy.enabled` is **false, and that is a decision rather than a blocker.** The
gate is ready: flex-auth's pin runs `callerAuth.mode: enforce`, the readiness
gate exits 0 against it, and an anonymous `/v1/check` returns 401.
Successful signing records:
It is not enabled because `policy.enabled` is one boolean over the whole repo,
and with `fail_closed: true` it makes flex-auth a hard dependency of every
`warden sign` — including the certs the ops-bridge tunnels depend on, one of
which carries the policy call. Uniform enforcement across an estate under active
refactor hardens the access needed to perform the refactor.
- `policy_decision_id` when flex-auth returned one;
- `policy_zone`;
- `policy_failure_mode`;
- `policy_outcome` (`allow`, `audit_only`, or `fail_open`).
`ADR-0006` scopes enforcement to security zones; `WARDEN-WP-0032` defines them.
Do not set `policy.enabled: true` outside that model — a zone-blind enablement
is out of order under the ADR, not merely inadvisable.
A fail-open result must therefore be visible rather than indistinguishable from
an unevaluated request. Denies do not reach the CA backend and produce no
certificate.
The sequence below is retained as the *mechanics*, for whoever enables the gate
for a zone once zones exist.
## Rollout and rollback
### Flip sequence
1. Validate `tenancy.yaml` and the workload declarations referenced by actor
`zone_subject` entries.
2. Compile the registry and inspect unknown/not-applicable results.
3. Run `scripts/check_policy_caller_identity.py` against the enforcing pin.
4. Deploy the same compiled registry revision and matching flex-auth policy
package.
5. Smoke an allow/advisory path, an enforced deny, and evaluator loss for one
fail-open and one fail-closed zone.
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`
with `fail_closed: true`, unreachable flex-auth blocks all signs.
### Operator checklist
| Step | Owner | Action |
| --- | --- | --- |
| 1 | flex-auth | Deploy runtime; confirm `curl <flex_auth_url>/healthz` → 200 (**FLEX-WP-0007**) |
| 2 | flex-auth | Load production registry + policy package (`~/flex-auth/examples/ops-warden/`) |
| 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` — 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 |
Cross-repo references:
- `~/flex-auth/workplans/FLEX-WP-0007-ops-warden-policy-gate-production-deployment.md`
- `history/2026-06-23-flex-auth-production-pickup-suggestion.md`
- `history/2026-06-23-flex-auth-policy-gate-production-smoke.md`
### Summary
1. Deploy the flex-auth registry and policy package to the production flex-auth
runtime — **not** only the example fixtures.
2. Set `policy.flex_auth_url` to the production flex-auth base URL.
3. Enable `policy.enabled: true` only after steps 15 pass.
4. Keep `fail_closed: true` unless an explicit break-glass procedure exists.
5. Smoke allow and deny paths; preserve non-secret evidence only.
### Rollback
If signs are blocked after enabling the gate:
1. Set `policy.enabled: false` in `warden.yaml` (inventory + TTL gate only).
2. Confirm `warden sign` succeeds without flex-auth.
3. File a State Hub note to `flex-auth` with non-secret symptoms (HTTP status,
`fail_closed` behaviour, actor name).
4. Re-enable only after flex-auth runtime and registry are verified.
Evidence fields for the flip: flex-auth health URL, smoke script exit codes,
`warden activity --kind sign --json` showing `policy_decision_id` on allow path.
---
Rollback is a versioned profile or registry rollback. Do not reintroduce
`policy.enabled: false`: that would erase per-zone evidence and recreate the
global control ADR-0009 supersedes.
## See also
- `wiki/OpsWardenConfig.md` — full config reference
- `wiki/CredentialRouting.md`
- `~/flex-auth/docs/ops-warden-policy-gate-handoff.md` — flex-auth handoff
- `flex-auth/INTENT.md`
- `net-kingdom/docs/platform-identity-security-architecture.md`
- `tenancy.yaml`
- `docs/evidence/security-zone-admission-2026-08-22.md`
- `wiki/OpsWardenConfig.md`
- `wiki/WorkloadSecurityPosture.md`
- `history/2026-08-19-flex-auth-caller-identity-evidence.md`

View file

@ -111,6 +111,28 @@ This is the practical bridge from WARDEN-WP-0014 (`warden access`) to WP-0015: a
assist can remove manual secret handling friction, while posture/maturity decides
whether the secret may flow at all.
## Security-zone consumer contract
Security zones are now the prescriptive sibling of this descriptive posture
model. `zone-engine` owns `security-zones_v0.1`; ops-warden follows it as a
consumer under `ADR-0009`.
- Workload maturity and data classification are admission evidence, not zone
names and not control stance.
- The workload owner declares authoritative `workload_identity` and `zones:` in
the same `tenancy.yaml` service entry.
- Credential lanes and actor resources reference that workload explicitly.
Managed deployables use Repo Manager's exact `(rapp_id, name, deployable?)`
tuple; independent operational workloads reference their owner declaration.
- Missing identity, membership, or evidence resolves `unknown`. No compiler may
infer it from a path, repository owner, actor class, or environment.
- flex-auth owns pre-sign stance; ops-warden owns PEP dependency failure behavior,
the agent read boundary, and `warden plan` escalation behavior.
Ops-warden declares `z1-operational` with M1/internal/medium evidence. That is an
accuracy statement, not a target: M2 remains unavailable until SLO history,
on-call, and incident/recovery evidence exist.
---
## Canon layering (where each part lands)

View file

@ -62,9 +62,9 @@ When `WARDEN_AGENT_ID` is set and the lane is `risk: high`, `warden access --fet
```bash
export WARDEN_AGENT_ID=grok # or claude, codex
warden access railiance-backup-offsite-lane --no-policy --out /tmp/nc.token
warden access railiance-backup-offsite-lane --no-policy --wrap
warden access railiance-backup-offsite-lane --no-policy --fingerprint
warden access railiance-backup-offsite-lane --out /tmp/nc.token
warden access railiance-backup-offsite-lane --wrap
warden access railiance-backup-offsite-lane --fingerprint
```
`warden route show <id> --json` includes `"risk"` and `"high_risk"`.

View file

@ -47,10 +47,10 @@ Config (host/port/env names only):
2. Fetch via sanctioned transport (never paste into chat):
```bash
warden access binky-company-email-imap --all --no-policy --out /tmp/imap.user
warden access binky-company-email-imap --all --out /tmp/imap.user
# primary field is IMAP_USERNAME; for password use --field after template support
# or:
warden access binky-company-email-imap --all --no-policy --exec -- \
warden access binky-company-email-imap --all --exec -- \
env IMAP_USERNAME=… # prefer secrets-engine / dual-field exec when wired
```

View file

@ -53,7 +53,7 @@ Design: `binky-control/integrations/qonto-mcp.md`
```bash
# lengths / presence only when debugging
warden access binky-qonto-api --all --no-policy --exec -- \
warden access binky-qonto-api --all --exec -- \
sh -c 'export QONTO_API_KEY="$API_KEY" QONTO_ORGANIZATION_ID="$API_USER";
# then: qonto-mcp-server or curl thirdparty with Authorization login:key
:'

View file

@ -122,10 +122,11 @@ bridge up state-hub-railiance01
- [ ] Tunnel establishes without static cert file on disk
- [ ] Re-run `bridge up` after cert TTL expires — `cert_command` re-issues automatically
### 5. Policy gate (optional, after FLEX-WP-0007)
### 5. Zone-aware policy evidence
When `policy.enabled: true`, confirm `signatures.log` includes `policy_decision_id`
on tunnel-driven signs. See `wiki/PolicyGatedSigning.md`.
Confirm `signatures.log` records `policy_zone`, `policy_failure_mode`,
`policy_outcome`, and `policy_decision_id` when flex-auth returns a decision on
tunnel-driven signs. See `wiki/PolicyGatedSigning.md`.
---
@ -177,4 +178,4 @@ starting cutover (WARDEN-WP-0023).
- `wiki/OpsWardenConfig.md` — cert_command example
- `wiki/playbooks/operator-openbao-token-hygiene.md`
- `wiki/AuditTrail.md` — query recent signs via `warden activity`
- `warden route show ops-bridge-tunnel --json`
- `warden route show ops-bridge-tunnel --json`

View file

@ -35,9 +35,9 @@ Used by `railiance-backup` (workstation) and `forgejo-backup` (platform).
```bash
# Preferred: file or exec injection via warden
warden access railiance-backup-offsite-lane --no-policy --out /tmp/nc.token
warden access railiance-backup-offsite-lane --out /tmp/nc.token
# or:
warden access railiance-backup-offsite-lane --no-policy --exec -- env | grep -v .
warden access railiance-backup-offsite-lane --exec -- env | grep -v .
```
If you must use raw bao in an interactive human shell (not an agent session):
@ -54,7 +54,7 @@ Used by `railiance-backup` (workstation) and `forgejo-backup` (platform).
3. **Proxy via warden access** (catalog active + resolvable):
```bash
warden access railiance-backup-offsite-lane --no-policy --fetch --out /tmp/nc.token
warden access railiance-backup-offsite-lane --fetch --out /tmp/nc.token
# Primary field is NC_WEBDAV_TOKEN. AGE_PRIVATE_KEY is recovery escrow only.
```

View file

@ -61,7 +61,7 @@ Never paste values into chat, State Hub, workplans, or Git.
3. **Or proxy via warden access** (same `bao kv get`, audited metadata only):
```bash
warden route show reuse-surface-hub-write-token --json
warden access reuse-surface-hub-write-token --no-policy --fetch
warden access reuse-surface-hub-write-token --fetch
```
4. **Webhook HMAC** (same path, second field — must match Forgejo org webhook):
@ -87,4 +87,4 @@ Never paste values into chat, State Hub, workplans, or Git.
Rotation: `railiance-platform/docs/reuse-surface-runtime-secrets-rotation-runbook.md`
(OpenBao patch → ESO `force-sync` → hub rollout → `make reuse-forgejo-webhook` when
the webhook HMAC changes → `make reuse-webhook-smoke`). Lifecycle:
`railiance-platform/docs/credential-lane-lifecycle-runbook.md` (CCR-2026-0005).
`railiance-platform/docs/credential-lane-lifecycle-runbook.md` (CCR-2026-0005).

View file

@ -69,7 +69,7 @@ First worked lane: `tenants/binky/company-email/imap` (CCR-2026-0007).
```bash
bao login -method=oidc -path=netkingdom role=<lane>-workload-kv-read
warden access <catalog-id> --no-policy --out /tmp/secret.file # mode 0600
warden access <catalog-id> --out /tmp/secret.file # mode 0600
# agents: never raw --fetch stream; WARDEN_AGENT_ID + risk=high → exit 7
```

View file

@ -48,11 +48,10 @@ this token** — it is the access front door: `warden access` proxies the read f
**ops-warden transparent fallback** — same lane via the `warden access` proxy (fetches as
you, holds nothing). Field-verified flags (whynot-design, @whynot/design@0.4.0):
```bash
# --exec needs the env-var name; --no-policy is required while the gate is advisory
# (policy.enabled=false), else the call exits 4.
warden access whynot-design-npm-publish --no-policy --field NPM_AUTH_TOKEN \
# --exec needs the env-var name. The zone-aware policy gate always runs first.
warden access whynot-design-npm-publish --field NPM_AUTH_TOKEN \
--exec -- npm publish
warden access whynot-design-npm-publish --no-policy --field NPM_AUTH_TOKEN --fetch
warden access whynot-design-npm-publish --field NPM_AUTH_TOKEN --fetch
```
On either path the value transits to you (or the child env) and never enters
ops-warden's memory, disk, or audit log.