feat: adopt security zones and explicit workload refs
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
This commit is contained in:
parent
12c637cbf2
commit
7ce58ae638
52 changed files with 1547 additions and 658 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 1–5 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`
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
:'
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue