FLEX-WP-0006: implement ops-warden signing gate policy
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled

This commit is contained in:
2026-06-23 21:17:42 +02:00
parent 53e0d055c9
commit 0fde95a87c
25 changed files with 1796 additions and 10 deletions

View File

@@ -337,6 +337,16 @@ func runServe(args []string, stdout, stderr io.Writer) int {
return fail(stderr, err)
}
mux := newServeMux(engine)
fmt.Fprintf(stderr, "flex-auth serving on http://%s\n", *addr)
if err := http.ListenAndServe(*addr, mux); err != nil {
return fail(stderr, err)
}
return 0
}
func newServeMux(engine *decisioncore.Engine) *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("content-type", "application/json")
@@ -368,12 +378,7 @@ func runServe(args []string, stdout, stderr io.Writer) int {
decisions, err := engine.BatchCheck(r.Context(), request)
writeHTTP(w, decisions, err)
})
fmt.Fprintf(stderr, "flex-auth serving on http://%s\n", *addr)
if err := http.ListenAndServe(*addr, mux); err != nil {
return fail(stderr, err)
}
return 0
return mux
}
func buildEngine(ctx context.Context, registryPath, policyPath, logPath string) (*decisioncore.Engine, error) {

View File

@@ -2,7 +2,11 @@ package main
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
@@ -74,6 +78,76 @@ func TestRunBatchCheck(t *testing.T) {
}
}
func TestRunCheckOpsWarden(t *testing.T) {
var stdout, stderr bytes.Buffer
code := run([]string{
"check",
"--registry", opsPath("registry_snapshot.json"),
"--policy", opsPath("policy_package.md"),
"--request", opsPath("check_request_allow_adm.json"),
}, &stdout, &stderr)
if code != 0 {
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
}
var decision api.DecisionEnvelope
if err := json.Unmarshal(stdout.Bytes(), &decision); err != nil {
t.Fatalf("unmarshal decision: %v\n%s", err, stdout.String())
}
if decision.Effect != api.DecisionEffectAllow {
t.Fatalf("decision.Effect = %q; want allow", decision.Effect)
}
if decision.ID == "" {
t.Fatal("decision.ID is empty; ops-warden needs a policy_decision_id")
}
}
func TestServeOpsWardenCheckContract(t *testing.T) {
logPath := filepath.Join(t.TempDir(), "decisions.jsonl")
engine, err := buildEngine(context.Background(), opsPath("registry_snapshot.json"), opsPath("policy_package.md"), logPath)
if err != nil {
t.Fatalf("buildEngine: %v", err)
}
server := httptest.NewServer(newServeMux(engine))
defer server.Close()
allow := postCheck(t, server.URL+"/v1/check", opsPath("check_request_allow_adm.json"))
if allow.Effect != api.DecisionEffectAllow || allow.ID == "" {
t.Fatalf("allow decision = %+v; want allow with id", allow)
}
deny := postCheck(t, server.URL+"/v1/check", opsPath("check_request_deny_ttl_above_max.json"))
if deny.Effect != api.DecisionEffectDeny || deny.Reason != "ttl_out_of_bounds" {
t.Fatalf("deny decision = %+v; want ttl_out_of_bounds deny", deny)
}
resp, err := http.Get(server.URL + "/v1/check")
if err != nil {
t.Fatalf("GET /v1/check: %v", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusMethodNotAllowed {
t.Fatalf("GET /v1/check status = %d; want 405", resp.StatusCode)
}
resp, err = http.Post(server.URL+"/v1/check", "application/json", strings.NewReader(`{"subject":`))
if err != nil {
t.Fatalf("POST malformed /v1/check: %v", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("malformed POST status = %d; want 400", resp.StatusCode)
}
logData, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("read decision log: %v", err)
}
if !strings.Contains(string(logData), allow.ID) || !strings.Contains(string(logData), deny.ID) {
t.Fatalf("decision log does not contain both decision ids\nlog: %s\nallow: %s deny: %s", string(logData), allow.ID, deny.ID)
}
}
func TestRunValidateAccessDescriptor(t *testing.T) {
var stdout, stderr bytes.Buffer
code := run([]string{"validate", "--kind", "access-descriptor", "--file", examplePath("access_descriptor.yaml")}, &stdout, &stderr)
@@ -88,3 +162,29 @@ func TestRunValidateAccessDescriptor(t *testing.T) {
func examplePath(name string) string {
return filepath.Join("..", "..", "examples", "caring", name)
}
func opsPath(name string) string {
return filepath.Join("..", "..", "examples", "ops-warden", name)
}
func postCheck(t *testing.T, url, path string) api.DecisionEnvelope {
t.Helper()
body, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
if err != nil {
t.Fatalf("POST %s: %v", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("POST %s status = %d; want 200", path, resp.StatusCode)
}
var decision api.DecisionEnvelope
if err := json.NewDecoder(resp.Body).Decode(&decision); err != nil {
t.Fatalf("decode %s response: %v", path, err)
}
return decision
}

View File

@@ -0,0 +1,82 @@
# Ops-Warden Policy Gate Handoff
Date: 2026-06-23
Workplan: FLEX-WP-0006
Ops-warden unblocker: WARDEN-WP-0009 T01
## Published flex-auth assets
- Policy package: examples/ops-warden/policy_package.md
- Policy fixtures: examples/ops-warden/policy_fixtures.yaml
- Combined registry fixture: examples/ops-warden/registry_snapshot.json
- Protected-system manifest: examples/ops-warden/protected_system_manifest.yaml
- Resource manifest: examples/ops-warden/resource_manifest.yaml
- Subject manifest: examples/ops-warden/subject_manifest.yaml
- Service request fixtures: examples/ops-warden/check_request_*.json
## Local service command
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
Ops-warden can point policy.flex_auth_url at that base URL for local smoke.
Production should keep policy.fail_closed true unless an explicit break-glass
procedure exists.
## Fixture coverage
Allow fixtures:
- fixture:ops-warden-adm-sign-allow
- fixture:ops-warden-agt-sign-allow
- fixture:ops-warden-atm-sign-allow
Deny fixtures:
- 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
## Non-secret smoke evidence
CLI validation on 2026-06-23:
- protected-system manifest: valid
- resource manifest: valid
- subject manifest: valid
- registry snapshot: loaded 1 system, 1 resource manifest, 3 subjects,
3 groups, 3 relationships, and 1 tenant
- policy package: valid with 8 passing fixtures
Local /v1/check service smoke on 2026-06-23:
- allow request: effect allow, reason signing_policy_matched,
decision id decision:706efe49f68d9ef1
- deny request: effect deny, reason ttl_out_of_bounds,
decision id decision:b69bdc25a988f367
- GET /v1/check: HTTP 405
- malformed POST /v1/check: HTTP 400
- decision log contained both decision ids
## Production sequence for ops-warden
1. Deploy the flex-auth registry and policy package above to the selected
flex-auth runtime.
2. Configure ops-warden policy.flex_auth_url to the flex-auth base URL.
3. Set policy.enabled: true.
4. Keep policy.tenant as tenant:platform unless a tenant-specific policy package
is introduced.
5. Run one allow-path sign smoke and confirm signatures.log includes
policy_decision_id.
6. Run one deny-path smoke with fail_closed true and preserve only non-secret
evidence.
## Ownership boundary
flex-auth owns the authorization decision for the signing request. ops-warden
continues to own actor inventory, SSH CA operation, OpenBao SSH engine
integration, host documentation, and signatures.log production evidence.
No SSH private keys, OpenBao tokens, database credentials, or real public-key
material are stored in these fixtures.

View File

@@ -1,6 +1,6 @@
# Flex-Auth Workplan Planning Map
Date: 2026-05-17
Date: 2026-06-23
## Purpose
@@ -21,9 +21,10 @@ This document captures the current sequencing view for flex-auth workplans.
| --- | --- | --- | --- | --- |
| `FLEX-WP-0001` | complete | done | none | Repo intent, boundaries, and authorization landscape research are complete. |
| `FLEX-WP-0005` | complete | done | `FLEX-WP-0001` | Foundations and Topaz alignment are complete: ADR-001/002/003, Go skeleton, `FlexAuthResourceManifest` schema pin, Topaz mapping spike, IAM Profile citation, ops-warden boundary clarification. |
| `FLEX-WP-0002` | P0 | ready | `FLEX-WP-0001`, `FLEX-WP-0005` | Standalone policy-as-code core: schemas, local registry, CARING profile/descriptors, Rego-in-Markdown policy packages, check APIs, explanations, decision log, CLI/service skeleton, tests. |
| `FLEX-WP-0003` | P1 | blocked | `FLEX-WP-0002` | Markitect consumer integration and first CARING benchmark: resource namespace, manifest import, action vocabulary, descriptor fixtures, decision fixtures, integration docs. |
| `FLEX-WP-0004` | P2 | blocked | `FLEX-WP-0002`, `FLEX-WP-0005` | Delegated PDP and directory adapters: Topaz adapter implementation (evaluation already done in `0005`), OpenFGA/SpiceDB, OPA/Cedar, Keycloak Authorization Services, Entra/Graph/SCIM, CARING envelope preservation. |
| `FLEX-WP-0002` | complete | completed | `FLEX-WP-0001`, `FLEX-WP-0005` | Standalone policy-as-code core is complete: schemas, local registry, CARING profile/descriptors, Rego-in-Markdown policy packages, check APIs, explanations, decision log, CLI/service skeleton, tests. |
| `FLEX-WP-0003` | complete | completed | `FLEX-WP-0002` | Markitect consumer integration and first CARING benchmark are complete: resource namespace, manifest import, action vocabulary, descriptor fixtures, decision fixtures, integration docs. |
| `FLEX-WP-0004` | complete | completed | `FLEX-WP-0002`, `FLEX-WP-0005` | Delegated PDP and directory adapter boundary work is complete: Topaz adapter shape, OpenFGA/SpiceDB, OPA/Cedar, Keycloak Authorization Services, Entra/Graph/SCIM, CARING envelope preservation. |
| `FLEX-WP-0006` | complete | finished | `FLEX-WP-0002`, `FLEX-WP-0005` | Ops-warden unblocker is complete: flex-auth publishes `ssh-certificate` / `sign` policies, fixtures, and `/v1/check` smoke evidence for the opt-in pre-sign gate shipped in ops-warden `WARDEN-WP-0007` and tracked for production in `WARDEN-WP-0009`. |
## Dependency Notes
@@ -58,6 +59,14 @@ now implements the Topaz adapter against the spike's output.
Delegated adapters must preserve flex-auth's CARING descriptor and
conformance fields even when backend-native role semantics differ.
`FLEX-WP-0006` was the cross-repo integration unblocker for
ops-warden. ops-warden already implements the opt-in policy call
(`policy.enabled: true`) and production OpenBao signing works without the
gate. flex-auth now publishes the protected-system manifest,
`ssh-certificate` / `sign` policy package, allow/deny fixtures, and
`POST /v1/check` evidence that ops-warden can use before enabling
`policy.enabled` in production.
## State Hub Mirror
Native State Hub dependency edges:
@@ -68,3 +77,7 @@ Native State Hub dependency edges:
- `FLEX-WP-0003 -> FLEX-WP-0002`
- `FLEX-WP-0004 -> FLEX-WP-0002`
- `FLEX-WP-0004 -> FLEX-WP-0005` (Topaz adapter consumes the spike)
- `FLEX-WP-0006 -> FLEX-WP-0002`
- `FLEX-WP-0006 -> FLEX-WP-0005`
- ops-warden: `WARDEN-WP-0009` waits for `FLEX-WP-0006` output before
production enablement of `policy.enabled`.

View File

@@ -13,6 +13,8 @@ examples/
# decision, registry, and audit fixtures (P2.1)
markitect/ # FlexAuthResourceManifest fixtures, decision
# fixtures, and Rego-in-Markdown policy packages
ops-warden/ # SSH certificate signing policy-gate fixtures
# for ops-warden policy.enabled smoke checks
topaz/ # docker-compose + sample directory and policy
# for the Topaz alignment spike (P5.4)
policies/ # generic Rego-in-Markdown packages used by

View File

@@ -0,0 +1,34 @@
# Ops-Warden SSH Signing Policy Gate
This example is the flex-auth side of ops-warden's opt-in pre-sign gate.
When `policy.enabled: true`, ops-warden calls `POST /v1/check` before signing
or issuing an SSH certificate.
Files:
- `protected_system_manifest.yaml` declares the `ops-warden` protected system,
`ssh-certificate` resource type, and `sign` action.
- `resource_manifest.yaml` declares fixture SSH certificate actor resources and
non-secret policy attributes such as allowed principals and TTL maxima.
- `subject_manifest.yaml` declares non-secret fixture actors for `adm`, `agt`,
and `atm` signing paths.
- `registry_snapshot.json` is the combined local registry used by the CLI and
service examples.
- `policy_package.md` is the Rego-in-Markdown policy package.
- `policy_fixtures.yaml` contains allow and deny expectations for package
validation.
- `check_request_*.json` files are ops-warden-shaped `/v1/check` requests.
Run locally:
```bash
flex-auth validate --kind protected-system --file examples/ops-warden/protected_system_manifest.yaml
flex-auth validate --kind resource-manifest --file examples/ops-warden/resource_manifest.yaml
flex-auth validate --kind subject-manifest --file examples/ops-warden/subject_manifest.yaml
flex-auth load-registry --file examples/ops-warden/registry_snapshot.json
flex-auth test-policy --file examples/ops-warden/policy_package.md
flex-auth check --registry examples/ops-warden/registry_snapshot.json --policy examples/ops-warden/policy_package.md --request examples/ops-warden/check_request_allow_adm.json
```
The fixture public-key fingerprints are examples only. Do not put real keys,
OpenBao tokens, or private signing material in these files.

View File

@@ -0,0 +1,23 @@
{
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {
"id": "platform-steward",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden"
},
"context": {
"principals": [
"platform",
"root"
],
"actor_type": "adm",
"ttl_hours": 4,
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
}
}

View File

@@ -0,0 +1,22 @@
{
"id": "check:ops-warden-ci-deploy-agent-agt",
"tenant": "tenant:platform",
"subject": {
"id": "ci-deploy-agent",
"type": "agt"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/ci-deploy-agent",
"type": "ssh-certificate",
"system": "ops-warden"
},
"context": {
"principals": [
"deploy"
],
"actor_type": "agt",
"ttl_hours": 1,
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
}
}

View File

@@ -0,0 +1,22 @@
{
"id": "check:ops-warden-backup-automation-atm",
"tenant": "tenant:platform",
"subject": {
"id": "backup-automation",
"type": "atm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/backup-automation",
"type": "ssh-certificate",
"system": "ops-warden"
},
"context": {
"principals": [
"backup"
],
"actor_type": "atm",
"ttl_hours": 1,
"pubkey_fingerprint": "SHA256:example-atm-fingerprint"
}
}

View File

@@ -0,0 +1,22 @@
{
"id": "check:ops-warden-ci-deploy-agent-agt",
"tenant": "tenant:platform",
"subject": {
"id": "ci-deploy-agent",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/ci-deploy-agent",
"type": "ssh-certificate",
"system": "ops-warden"
},
"context": {
"principals": [
"deploy"
],
"actor_type": "agt",
"ttl_hours": 1,
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
}
}

View File

@@ -0,0 +1,22 @@
{
"id": "check:ops-warden-ci-deploy-agent-agt",
"tenant": "tenant:platform",
"subject": {
"id": "ci-deploy-agent",
"type": "agt"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/ci-deploy-agent",
"type": "ssh-certificate",
"system": "ops-warden"
},
"context": {
"principals": [
"root"
],
"actor_type": "agt",
"ttl_hours": 1,
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
}
}

View File

@@ -0,0 +1,21 @@
{
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {
"id": "platform-steward",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden"
},
"context": {
"principals": [
"platform"
],
"actor_type": "adm",
"ttl_hours": 4
}
}

View File

@@ -0,0 +1,22 @@
{
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {
"id": "platform-steward",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden"
},
"context": {
"principals": [
"platform"
],
"actor_type": "adm",
"ttl_hours": 12,
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
}
}

View File

@@ -0,0 +1,22 @@
{
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {
"id": "unknown-actor",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden"
},
"context": {
"principals": [
"platform"
],
"actor_type": "adm",
"ttl_hours": 4,
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
}
}

View File

@@ -0,0 +1,337 @@
[
{
"id": "fixture:ops-warden-adm-sign-allow",
"request": {
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {
"id": "platform-steward",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "platform-steward",
"actor_type": "adm",
"allowed_subjects": [
"platform-steward",
"iam:platform-steward"
],
"allowed_principals": [
"platform",
"root"
],
"max_ttl_hours": 8
}
},
"context": {
"principals": [
"platform",
"root"
],
"actor_type": "adm",
"ttl_hours": 4,
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
}
},
"expect": {
"effect": "allow",
"reason": "signing_policy_matched"
}
},
{
"id": "fixture:ops-warden-agt-sign-allow",
"request": {
"id": "check:ops-warden-ci-deploy-agent-agt",
"tenant": "tenant:platform",
"subject": {
"id": "ci-deploy-agent",
"type": "agt"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/ci-deploy-agent",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "ci-deploy-agent",
"actor_type": "agt",
"allowed_subjects": [
"ci-deploy-agent",
"iam:ci-deploy-agent"
],
"allowed_principals": [
"deploy",
"git"
],
"max_ttl_hours": 2
}
},
"context": {
"principals": [
"deploy"
],
"actor_type": "agt",
"ttl_hours": 1,
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
}
},
"expect": {
"effect": "allow",
"reason": "signing_policy_matched"
}
},
{
"id": "fixture:ops-warden-atm-sign-allow",
"request": {
"id": "check:ops-warden-backup-automation-atm",
"tenant": "tenant:platform",
"subject": {
"id": "backup-automation",
"type": "atm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/backup-automation",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "backup-automation",
"actor_type": "atm",
"allowed_subjects": [
"backup-automation",
"iam:backup-automation"
],
"allowed_principals": [
"backup"
],
"max_ttl_hours": 1
}
},
"context": {
"principals": [
"backup"
],
"actor_type": "atm",
"ttl_hours": 1,
"pubkey_fingerprint": "SHA256:example-atm-fingerprint"
}
},
"expect": {
"effect": "allow",
"reason": "signing_policy_matched"
}
},
{
"id": "fixture:ops-warden-unknown-subject-deny",
"request": {
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {
"id": "unknown-actor",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "platform-steward",
"actor_type": "adm",
"allowed_subjects": [
"platform-steward",
"iam:platform-steward"
],
"allowed_principals": [
"platform",
"root"
],
"max_ttl_hours": 8
}
},
"context": {
"principals": [
"platform"
],
"actor_type": "adm",
"ttl_hours": 4,
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
}
},
"expect": {
"effect": "deny",
"reason": "unknown_subject"
}
},
{
"id": "fixture:ops-warden-actor-type-mismatch-deny",
"request": {
"id": "check:ops-warden-ci-deploy-agent-agt",
"tenant": "tenant:platform",
"subject": {
"id": "ci-deploy-agent",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/ci-deploy-agent",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "ci-deploy-agent",
"actor_type": "agt",
"allowed_subjects": [
"ci-deploy-agent",
"iam:ci-deploy-agent"
],
"allowed_principals": [
"deploy",
"git"
],
"max_ttl_hours": 2
}
},
"context": {
"principals": [
"deploy"
],
"actor_type": "agt",
"ttl_hours": 1,
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
}
},
"expect": {
"effect": "deny",
"reason": "actor_type_mismatch"
}
},
{
"id": "fixture:ops-warden-ttl-above-max-deny",
"request": {
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {
"id": "platform-steward",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "platform-steward",
"actor_type": "adm",
"allowed_subjects": [
"platform-steward",
"iam:platform-steward"
],
"allowed_principals": [
"platform",
"root"
],
"max_ttl_hours": 8
}
},
"context": {
"principals": [
"platform"
],
"actor_type": "adm",
"ttl_hours": 12,
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
}
},
"expect": {
"effect": "deny",
"reason": "ttl_out_of_bounds"
}
},
{
"id": "fixture:ops-warden-disallowed-principal-deny",
"request": {
"id": "check:ops-warden-ci-deploy-agent-agt",
"tenant": "tenant:platform",
"subject": {
"id": "ci-deploy-agent",
"type": "agt"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/ci-deploy-agent",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "ci-deploy-agent",
"actor_type": "agt",
"allowed_subjects": [
"ci-deploy-agent",
"iam:ci-deploy-agent"
],
"allowed_principals": [
"deploy",
"git"
],
"max_ttl_hours": 2
}
},
"context": {
"principals": [
"root"
],
"actor_type": "agt",
"ttl_hours": 1,
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
}
},
"expect": {
"effect": "deny",
"reason": "disallowed_principal"
}
},
{
"id": "fixture:ops-warden-missing-fingerprint-deny",
"request": {
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {
"id": "platform-steward",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "platform-steward",
"actor_type": "adm",
"allowed_subjects": [
"platform-steward",
"iam:platform-steward"
],
"allowed_principals": [
"platform",
"root"
],
"max_ttl_hours": 8
}
},
"context": {
"principals": [
"platform"
],
"actor_type": "adm",
"ttl_hours": 4
}
},
"expect": {
"effect": "deny",
"reason": "missing_pubkey_fingerprint"
}
}
]

View File

@@ -0,0 +1,257 @@
---
id: ops-warden.ssh-certificate.sign
name: Ops-Warden SSH certificate signing
namespace: ops-warden:ssh-certificate
version: v1
status: ready
package: flexauth.ops_warden.ssh_signing
actions:
- sign
owner: team:platform-security
fixtures:
- policy_fixtures.yaml
caring:
profile: caring-0.4.0-rc2
enforce: false
canonical_roles:
- Operator
organization_relations:
- ServiceProvider
scopes:
- level: Platform
id: platform:ssh-signing
tenant: tenant:platform
planes:
- Identity
- Secret
- Audit
capabilities:
- Use
- Operate
- Audit
exposure_modes:
- Metadata
conditions:
- TimeLimited
- Logged
restrictions:
- PrivilegeEscalationBlocked
- SecretAccessBlocked
activation:
mode: local
metadata:
source: examples/ops-warden/policy_package.md
ops_warden_policy_gate: v2
---
# Ops-Warden SSH Certificate Signing
This package authorizes ops-warden's opt-in pre-sign policy gate. The caller
keeps SSH CA custody, actor inventory, and OpenBao signing; flex-auth decides
whether a specific `sign` request is allowed now.
## Rules
```rego
import future.keywords.contains
import future.keywords.if
import future.keywords.in
actor_types := {"adm", "agt", "atm"}
decision := {"effect": "allow", "reason": "signing_policy_matched"} if {
allowed
} else := {"effect": "deny", "reason": first_denial} if {
true
}
allowed if {
input.action == "sign"
input.resource.system == "ops-warden"
input.resource.type == "ssh-certificate"
effective_tenant == "tenant:platform"
valid_actor_type
subject_type_matches_context
actor_type_matches_resource
resource_id_matches_actor
subject_id_allowed
valid_ttl
has_pubkey_fingerprint
principals_allowed
}
default effective_tenant := ""
effective_tenant := input.tenant if {
is_string(input.tenant)
input.tenant != ""
} else := input.resource.tenant if {
is_string(input.resource.tenant)
input.resource.tenant != ""
} else := input.subject.tenant if {
is_string(input.subject.tenant)
input.subject.tenant != ""
}
default first_denial := "no_matching_rule"
first_denial := "wrong_action" if {
input.action != "sign"
} else := "wrong_system" if {
input.resource.system != "ops-warden"
} else := "wrong_resource_type" if {
input.resource.type != "ssh-certificate"
} else := "wrong_tenant" if {
effective_tenant != "tenant:platform"
} else := "unknown_actor_resource" if {
not has_actor_resource
} else := "unknown_subject" if {
not subject_id_allowed
} else := "actor_type_mismatch" if {
not valid_actor_type
} else := "actor_type_mismatch" if {
not subject_type_matches_context
} else := "actor_type_mismatch" if {
not actor_type_matches_resource
} else := "actor_resource_mismatch" if {
not resource_id_matches_actor
} else := "ttl_out_of_bounds" if {
not valid_ttl
} else := "missing_pubkey_fingerprint" if {
not has_pubkey_fingerprint
} else := "missing_principal" if {
not has_principals
} else := "disallowed_principal" if {
count(disallowed_principals) > 0
}
has_actor_resource if {
is_string(input.resource.attributes.actor_id)
input.resource.attributes.actor_id != ""
}
valid_actor_type if {
is_string(input.context.actor_type)
input.context.actor_type in actor_types
}
subject_type_matches_context if {
input.subject.type == input.context.actor_type
}
subject_type_matches_context if {
input.subject.attributes.actor_type == input.context.actor_type
}
actor_type_matches_resource if {
input.context.actor_type == input.resource.attributes.actor_type
}
resource_id_matches_actor if {
input.resource.id == sprintf("ssh-cert:actor/%s", [input.resource.attributes.actor_id])
}
subject_id_allowed if {
input.subject.id in input.resource.attributes.allowed_subjects
}
has_ttl if {
is_number(input.context.ttl_hours)
}
valid_ttl if {
has_ttl
input.context.ttl_hours > 0
input.context.ttl_hours <= input.resource.attributes.max_ttl_hours
}
has_pubkey_fingerprint if {
is_string(input.context.pubkey_fingerprint)
input.context.pubkey_fingerprint != ""
}
has_principals if {
count(input.context.principals) > 0
}
principals_allowed if {
has_principals
count(disallowed_principals) == 0
}
allowed_principal(principal) if {
principal in input.resource.attributes.allowed_principals
}
disallowed_principals contains principal if {
principal := input.context.principals[_]
not allowed_principal(principal)
}
```
## Tests
```rego test
package flexauth.ops_warden.ssh_signing_test
import future.keywords.if
import data.flexauth.ops_warden.ssh_signing
adm_request := {
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {"id": "platform-steward", "type": "adm"},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "platform-steward",
"actor_type": "adm",
"allowed_subjects": ["platform-steward", "iam:platform-steward"],
"allowed_principals": ["platform", "root"],
"max_ttl_hours": 8
}
},
"context": {
"actor_type": "adm",
"principals": ["platform"],
"pubkey_fingerprint": "SHA256:example-adm-fingerprint",
"ttl_hours": 4
}
}
test_adm_sign_allowed if {
ssh_signing.decision.effect == "allow" with input as adm_request
}
test_high_ttl_denied if {
ssh_signing.decision.reason == "ttl_out_of_bounds" with input as {
"tenant": "tenant:platform",
"subject": {"id": "platform-steward", "type": "adm"},
"action": "sign",
"resource": adm_request.resource,
"context": {
"actor_type": "adm",
"principals": ["platform"],
"pubkey_fingerprint": "SHA256:example-adm-fingerprint",
"ttl_hours": 12
}
}
}
test_missing_fingerprint_denied if {
ssh_signing.decision.reason == "missing_pubkey_fingerprint" with input as {
"tenant": "tenant:platform",
"subject": {"id": "platform-steward", "type": "adm"},
"action": "sign",
"resource": adm_request.resource,
"context": {
"actor_type": "adm",
"principals": ["platform"],
"ttl_hours": 4
}
}
}
```

View File

@@ -0,0 +1,36 @@
id: ops-warden
name: Ops Warden
resource_types:
- name: ssh-certificate
scope_level: Resource
planes:
- Identity
- Secret
- Audit
metadata:
description: Short-lived SSH certificate signing request.
actions:
- name: sign
capabilities:
- Use
- Operate
- Audit
planes:
- Identity
- Secret
- Audit
exposure_modes:
- Metadata
metadata:
required_context:
- principals
- actor_type
- pubkey_fingerprint
- ttl_hours
caring_profiles:
- caring-0.4.0-rc2
metadata:
flex_auth_contract: protected-system-v0
ops_warden_policy_gate: v2
policy_enabled_config: policy.enabled
tenant: tenant:platform

View File

@@ -0,0 +1,366 @@
{
"systems": [
{
"id": "ops-warden",
"name": "Ops Warden",
"resource_types": [
{
"name": "ssh-certificate",
"scope_level": "Resource",
"planes": [
"Identity",
"Secret",
"Audit"
],
"metadata": {
"description": "Short-lived SSH certificate signing request."
}
}
],
"actions": [
{
"name": "sign",
"capabilities": [
"Use",
"Operate",
"Audit"
],
"planes": [
"Identity",
"Secret",
"Audit"
],
"exposure_modes": [
"Metadata"
],
"metadata": {
"required_context": [
"principals",
"actor_type",
"pubkey_fingerprint",
"ttl_hours"
]
}
}
],
"caring_profiles": [
"caring-0.4.0-rc2"
],
"metadata": {
"flex_auth_contract": "protected-system-v0",
"ops_warden_policy_gate": "v2",
"policy_enabled_config": "policy.enabled",
"tenant": "tenant:platform"
}
}
],
"resource_manifests": [
{
"id": "ops-warden-ssh-certificates",
"system": "ops-warden",
"resources": [
{
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"labels": [
"ssh-signing",
"adm"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "platform-steward",
"actor_type": "adm",
"allowed_subjects": [
"platform-steward",
"iam:platform-steward"
],
"allowed_principals": [
"platform",
"root"
],
"max_ttl_hours": 8
}
},
{
"id": "ssh-cert:actor/ci-deploy-agent",
"type": "ssh-certificate",
"labels": [
"ssh-signing",
"agt"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "ci-deploy-agent",
"actor_type": "agt",
"allowed_subjects": [
"ci-deploy-agent",
"iam:ci-deploy-agent"
],
"allowed_principals": [
"deploy",
"git"
],
"max_ttl_hours": 2
}
},
{
"id": "ssh-cert:actor/backup-automation",
"type": "ssh-certificate",
"labels": [
"ssh-signing",
"atm"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "backup-automation",
"actor_type": "atm",
"allowed_subjects": [
"backup-automation",
"iam:backup-automation"
],
"allowed_principals": [
"backup"
],
"max_ttl_hours": 1
}
}
],
"actions": [
"sign"
],
"caring_profile": "caring-0.4.0-rc2",
"metadata": {
"flex_auth_contract": "resource-registration-v0",
"tenant": "tenant:platform"
}
}
],
"tenants": [
{
"id": "tenant:platform",
"name": "Platform Tenant"
}
],
"subjects": [
{
"id": "platform-steward",
"type": "Agent",
"display_name": "Platform Steward",
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
],
"groups": [
"group:ops-warden-admins"
],
"tenant": "tenant:platform",
"metadata": {
"actor_type": "adm"
}
},
{
"id": "ci-deploy-agent",
"type": "Agent",
"display_name": "CI Deploy Agent",
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
],
"groups": [
"group:ops-warden-agents"
],
"tenant": "tenant:platform",
"metadata": {
"actor_type": "agt"
}
},
{
"id": "backup-automation",
"type": "Automation",
"display_name": "Backup Automation",
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
],
"groups": [
"group:ops-warden-automations"
],
"tenant": "tenant:platform",
"metadata": {
"actor_type": "atm"
}
}
],
"groups": [
{
"id": "group:ops-warden-admins",
"display_name": "Ops Warden Admin Actors",
"members": [
"platform-steward"
],
"tenant": "tenant:platform"
},
{
"id": "group:ops-warden-agents",
"display_name": "Ops Warden Agent Actors",
"members": [
"ci-deploy-agent"
],
"tenant": "tenant:platform"
},
{
"id": "group:ops-warden-automations",
"display_name": "Ops Warden Automation Actors",
"members": [
"backup-automation"
],
"tenant": "tenant:platform"
}
],
"relationships": [
{
"id": "rel:platform-steward-sign-platform-steward",
"system": "ops-warden",
"subject": "group:ops-warden-admins",
"relation": "signer",
"object": "ssh-cert:actor/platform-steward",
"tenant": "tenant:platform",
"conditions": [
"TimeLimited",
"Logged"
],
"caring": {
"id": "descriptor:ops-warden-adm-signer",
"profile": "caring-0.4.0-rc2",
"subject_type": "Group",
"organization_relation": "ServiceProvider",
"canonical_role": "Operator",
"scope": {
"level": "Resource",
"id": "ssh-cert:actor/platform-steward",
"tenant": "tenant:platform",
"resource": "ssh-cert:actor/platform-steward"
},
"planes": [
"Identity",
"Secret",
"Audit"
],
"capabilities": [
"Use",
"Operate",
"Audit"
],
"exposure_modes": [
"Metadata"
],
"conditions": [
"TimeLimited",
"Logged"
],
"restrictions": [
"PrivilegeEscalationBlocked",
"SecretAccessBlocked"
],
"access_path": "mediated"
}
},
{
"id": "rel:ci-deploy-agent-sign-ci-deploy-agent",
"system": "ops-warden",
"subject": "group:ops-warden-agents",
"relation": "signer",
"object": "ssh-cert:actor/ci-deploy-agent",
"tenant": "tenant:platform",
"conditions": [
"TimeLimited",
"Logged"
],
"caring": {
"id": "descriptor:ops-warden-agt-signer",
"profile": "caring-0.4.0-rc2",
"subject_type": "Group",
"organization_relation": "ServiceProvider",
"canonical_role": "Operator",
"scope": {
"level": "Resource",
"id": "ssh-cert:actor/ci-deploy-agent",
"tenant": "tenant:platform",
"resource": "ssh-cert:actor/ci-deploy-agent"
},
"planes": [
"Identity",
"Secret",
"Audit"
],
"capabilities": [
"Use",
"Operate",
"Audit"
],
"exposure_modes": [
"Metadata"
],
"conditions": [
"TimeLimited",
"Logged"
],
"restrictions": [
"PrivilegeEscalationBlocked",
"SecretAccessBlocked"
],
"access_path": "mediated"
}
},
{
"id": "rel:backup-automation-sign-backup-automation",
"system": "ops-warden",
"subject": "group:ops-warden-automations",
"relation": "signer",
"object": "ssh-cert:actor/backup-automation",
"tenant": "tenant:platform",
"conditions": [
"TimeLimited",
"Logged"
],
"caring": {
"id": "descriptor:ops-warden-atm-signer",
"profile": "caring-0.4.0-rc2",
"subject_type": "Group",
"organization_relation": "ServiceProvider",
"canonical_role": "Operator",
"scope": {
"level": "Resource",
"id": "ssh-cert:actor/backup-automation",
"tenant": "tenant:platform",
"resource": "ssh-cert:actor/backup-automation"
},
"planes": [
"Identity",
"Secret",
"Audit"
],
"capabilities": [
"Use",
"Operate",
"Audit"
],
"exposure_modes": [
"Metadata"
],
"conditions": [
"TimeLimited",
"Logged"
],
"restrictions": [
"PrivilegeEscalationBlocked",
"SecretAccessBlocked"
],
"access_path": "mediated"
}
}
]
}

View File

@@ -0,0 +1,59 @@
id: ops-warden-ssh-certificates
system: ops-warden
resources:
- id: ssh-cert:actor/platform-steward
type: ssh-certificate
labels:
- ssh-signing
- adm
trust_zone: platform
owner: team:platform-security
attributes:
actor_id: platform-steward
actor_type: adm
allowed_subjects:
- platform-steward
- iam:platform-steward
allowed_principals:
- platform
- root
max_ttl_hours: 8
- id: ssh-cert:actor/ci-deploy-agent
type: ssh-certificate
labels:
- ssh-signing
- agt
trust_zone: platform
owner: team:platform-security
attributes:
actor_id: ci-deploy-agent
actor_type: agt
allowed_subjects:
- ci-deploy-agent
- iam:ci-deploy-agent
allowed_principals:
- deploy
- git
max_ttl_hours: 2
- id: ssh-cert:actor/backup-automation
type: ssh-certificate
labels:
- ssh-signing
- atm
trust_zone: platform
owner: team:platform-security
attributes:
actor_id: backup-automation
actor_type: atm
allowed_subjects:
- backup-automation
- iam:backup-automation
allowed_principals:
- backup
max_ttl_hours: 1
actions:
- sign
caring_profile: caring-0.4.0-rc2
metadata:
flex_auth_contract: resource-registration-v0
tenant: tenant:platform

View File

@@ -0,0 +1,54 @@
id: subjects:ops-warden-platform
tenants:
- id: tenant:platform
name: Platform Tenant
subjects:
- id: platform-steward
type: Agent
display_name: Platform Steward
organization_relation: ServiceProvider
roles:
- Operator
groups:
- group:ops-warden-admins
tenant: tenant:platform
metadata:
actor_type: adm
- id: ci-deploy-agent
type: Agent
display_name: CI Deploy Agent
organization_relation: ServiceProvider
roles:
- Operator
groups:
- group:ops-warden-agents
tenant: tenant:platform
metadata:
actor_type: agt
- id: backup-automation
type: Automation
display_name: Backup Automation
organization_relation: ServiceProvider
roles:
- Operator
groups:
- group:ops-warden-automations
tenant: tenant:platform
metadata:
actor_type: atm
groups:
- id: group:ops-warden-admins
display_name: Ops Warden Admin Actors
members:
- platform-steward
tenant: tenant:platform
- id: group:ops-warden-agents
display_name: Ops Warden Agent Actors
members:
- ci-deploy-agent
tenant: tenant:platform
- id: group:ops-warden-automations
display_name: Ops Warden Automation Actors
members:
- backup-automation
tenant: tenant:platform

View File

@@ -105,6 +105,7 @@ func (e *Engine) BatchCheck(ctx context.Context, request api.BatchCheckRequest)
for _, resource := range request.Resources {
decision, err := e.Check(ctx, api.CheckRequest{
ID: request.ID,
Tenant: request.Tenant,
Subject: request.Subject,
Action: request.Action,
Resource: resource,
@@ -188,6 +189,15 @@ func (e *Engine) normalizeRequest(request api.CheckRequest) (api.CheckRequest, r
normalized := request
facts := registryFacts{}
if normalized.Tenant != "" {
if normalized.Subject.Tenant == "" {
normalized.Subject.Tenant = normalized.Tenant
}
if normalized.Resource.Tenant == "" {
normalized.Resource.Tenant = normalized.Tenant
}
}
if subject, ok := e.store.Subject(request.Subject.ID); ok {
facts.subjectFound = true
facts.subject = subject

View File

@@ -74,6 +74,28 @@ func TestRedactPolicyPackageMarkdownValidates(t *testing.T) {
}
}
func TestOpsWardenPolicyPackageMarkdownValidates(t *testing.T) {
pkg, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "ops-warden", "policy_package.md"))
if err != nil {
t.Fatalf("LoadAndValidateFile: %v", err)
}
if !pkg.Valid {
t.Fatalf("pkg.Valid = false\n%s", formatValidation(pkg.Validation))
}
if pkg.Metadata.Namespace != "ops-warden:ssh-certificate" {
t.Fatalf("metadata.Namespace = %q; want ops-warden:ssh-certificate", pkg.Metadata.Namespace)
}
if len(pkg.Validation.Fixtures) != 8 {
t.Fatalf("Validation.Fixtures len = %d; want 8", len(pkg.Validation.Fixtures))
}
for _, fixture := range pkg.Validation.Fixtures {
if !fixture.Passed {
t.Fatalf("fixture %s failed: %s\nactual: %+v", fixture.ID, fixture.Error, fixture.Actual)
}
}
}
func TestCaringFindingsAreAdvisoryUntilEnforced(t *testing.T) {
doc := inlinePolicy(false, "allow")
pkg, err := policy.Load([]byte(doc), "inline-policy.md")

View File

@@ -148,6 +148,7 @@ type DecisionExpectation struct {
// CheckRequest is the stable protected-system-facing decision request.
type CheckRequest struct {
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Subject SubjectRef `json:"subject" yaml:"subject"`
Action string `json:"action" yaml:"action"`
Resource ResourceRef `json:"resource" yaml:"resource"`
@@ -159,6 +160,7 @@ type CheckRequest struct {
// BatchCheckRequest evaluates one subject/action against multiple resources.
type BatchCheckRequest struct {
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Subject SubjectRef `json:"subject" yaml:"subject"`
Action string `json:"action" yaml:"action"`
Resources []ResourceRef `json:"resources" yaml:"resources"`

View File

@@ -7,6 +7,7 @@
"required": ["subject", "action", "resource"],
"properties": {
"id": {"type": "string", "minLength": 1},
"tenant": {"type": "string", "minLength": 1},
"subject": {"$ref": "#/$defs/subject_ref"},
"action": {"type": "string", "minLength": 1},
"resource": {"$ref": "#/$defs/resource_ref"},

View File

@@ -0,0 +1,230 @@
---
id: FLEX-WP-0006
type: workplan
title: "Ops-Warden SSH Signing Policy Gate"
domain: infotech
repo: flex-auth
status: finished
owner: codex
topic_slug: flex-auth
planning_priority: P0
planning_order: 60
depends_on_workplans:
- FLEX-WP-0002
- FLEX-WP-0005
related_workplans:
- WARDEN-WP-0009
created: "2026-06-23"
updated: "2026-06-23"
state_hub_workstream_id: "bbea4049-8acc-4d7c-8cf5-3106c6b93f7f"
---
# FLEX-WP-0006: Ops-Warden SSH Signing Policy Gate
## Purpose
Publish the flex-auth policy package, registry fixtures, and service contract
evidence needed for ops-warden's opt-in pre-sign gate.
Ops-warden already shipped the caller side in `WARDEN-WP-0007`:
`policy.enabled: true` makes `warden sign` and local-backend `warden issue`
call `POST /v1/check` before signing. Production OpenBao-backed signing was
verified in `WARDEN-WP-0008`. The remaining blocked work is flex-auth-owned:
policies for `resource.type: ssh-certificate` and `action: sign`.
This workplan unblocks `ops-warden/workplans/WARDEN-WP-0009-flex-auth-policy-gate-production.md`.
## Gate Contract
The shipped ops-warden gate sends this flex-auth decision request:
| Field | Meaning |
| --- | --- |
| `subject.id` | `WARDEN_POLICY_SUBJECT` when set, otherwise actor name |
| `subject.type` | Actor type: `adm`, `agt`, or `atm` |
| `tenant` | `policy.tenant`, default `tenant:platform` |
| `resource.id` | `ssh-cert:actor/<actor-name>` |
| `resource.type` | `ssh-certificate` |
| `resource.system` | `ops-warden` |
| `action` | `sign` |
| `context.principals` | Requested SSH certificate principals from inventory |
| `context.actor_type` | Actor type: `adm`, `agt`, or `atm` |
| `context.pubkey_fingerprint` | SHA256 fingerprint of the submitted public key text |
| `context.ttl_hours` | Requested certificate TTL |
Allow responses must return `effect: allow` plus a decision `id` or
`request_id`; ops-warden records that value as `policy_decision_id` in
`signatures.log`. Deny responses must include a human-readable `reason` that
the ops-warden CLI can surface.
## Policy Boundary
flex-auth decides whether this specific signing request is allowed now.
ops-warden remains responsible for SSH CA operation, OpenBao integration, actor
inventory, host documentation, and local scorecard checks.
The flex-auth policy must not request or store SSH private keys, OpenBao
tokens, database credentials, or other secrets. Acceptance evidence should use
fixtures, non-secret request bodies, decision ids, and sanitized logs only.
## T1 - Pin the ops-warden protected-system contract
```task
id: FLEX-WP-0006-T01
status: done
priority: high
state_hub_task_id: "8831b904-dbef-4d55-8eb5-053c939c86b3"
```
Add an ops-warden protected-system manifest and registry fixture slice that
declares:
- protected system `ops-warden`
- resource type `ssh-certificate`
- action `sign`
- tenant `tenant:platform`
- supported actor subject types `adm`, `agt`, and `atm`
- required context fields: `principals`, `actor_type`,
`pubkey_fingerprint`, and `ttl_hours`
Output should live under `examples/ops-warden/` unless implementation reveals a
more idiomatic local path. The manifest and fixture must validate with the
existing `flex-auth validate` and `flex-auth load-registry` commands.
## T2 - Author the ssh-certificate sign policy package
```task
id: FLEX-WP-0006-T02
status: done
priority: high
state_hub_task_id: "9ea206fa-f93d-46b6-8b8e-e8669dd502d4"
```
Create the Rego-in-Markdown policy package for ops-warden signing decisions.
Minimum allow criteria:
- request action is `sign`
- resource system is `ops-warden`
- resource type is `ssh-certificate`
- tenant is `tenant:platform` unless the fixture explicitly tests another
configured tenant
- subject and `context.actor_type` are aligned with the actor resource
- requested TTL is positive and within the policy's configured maximum for the
actor type
- requested principals are non-empty and allowed for the actor fixture
- `context.pubkey_fingerprint` is present
Minimum deny criteria:
- unknown subject or actor resource
- mismatched `subject.type`, `context.actor_type`, or
`resource.id: ssh-cert:actor/<actor>`
- TTL outside policy bounds
- missing fingerprint
- disallowed or empty principals
- wrong action, system, resource type, or tenant
The package should produce clear deny reasons and preserve the standard
decision envelope so ops-warden can surface `reason` without special casing.
## T3 - Add allow and deny fixtures
```task
id: FLEX-WP-0006-T03
status: done
priority: high
state_hub_task_id: "6bf5cb9b-d46c-49cf-aec0-12f6e864a1f8"
```
Add fixture requests and expected decisions for the gate.
Required fixtures:
- allow: valid `adm` sign request
- allow: valid `agt` sign request, if an agent actor fixture exists
- deny: unknown subject
- deny: actor type mismatch
- deny: TTL above policy max
- deny: missing or disallowed principal
- deny: missing `pubkey_fingerprint`
Wire the fixtures into Go tests or the existing policy fixture runner so
`make test` proves the package behavior. Fixture data must remain non-secret.
## T4 - Verify the `/v1/check` service contract
```task
id: FLEX-WP-0006-T04
status: done
priority: high
state_hub_task_id: "077d29db-30e0-4447-90fd-620c0884306c"
```
Run `flex-auth serve` with the ops-warden registry and policy package, then
exercise `POST /v1/check` with ops-warden-shaped JSON.
Acceptance evidence:
- allow response includes `effect: allow` and a stable decision `id`
- deny response includes `effect: deny` and a useful `reason`
- decision log records the allow and deny decisions without secrets
- method mismatch and malformed JSON fail predictably
- the documented behavior is compatible with ops-warden `fail_closed: true`
If the current service shape needs a small compatibility adjustment, keep it
inside the stable v1 API instead of adding an ops-warden-specific route.
## T5 - Hand off production-readiness evidence to ops-warden
```task
id: FLEX-WP-0006-T05
status: done
priority: medium
state_hub_task_id: "06cac0b1-51c0-4ae0-b605-c940f7821ac7"
```
Publish the handoff notes ops-warden needs to close `WARDEN-WP-0009 T01` and
start its production enablement smoke.
Include:
- policy package path and version
- registry fixture path
- local service command
- allow and deny fixture names
- non-secret decision ids from local smoke
- expected `policy.enabled` production sequence
- reminder that OpenBao SSH signing and SSH CA custody remain ops-warden-owned
Record progress in State Hub and ask the custodian operator to run
`make fix-consistency REPO=flex-auth` from `~/state-hub` after this workplan is
merged or otherwise accepted.
## Implementation Summary
Implemented on 2026-06-23.
- Added examples/ops-warden/ with the protected-system manifest, subject and
resource manifests, combined registry snapshot, policy package, check
requests, and allow/deny fixtures.
- Added top-level tenant support to CheckRequest and BatchCheckRequest so the
shipped ops-warden policy gate request shape is accepted by /v1/check.
- Added CLI and HTTP service tests for the ops-warden allow path, deny path,
malformed JSON, method mismatch, and decision-log recording.
- Added docs/ops-warden-policy-gate-handoff.md with non-secret smoke evidence
and the ops-warden production enablement sequence.
## Exit Criteria
- flex-auth contains a validated ops-warden protected-system manifest and
registry fixture.
- flex-auth contains an `ssh-certificate` / `sign` policy package with allow
and deny fixtures for `adm`, `agt`, and `atm`-style actors where applicable.
- `make test` passes.
- `POST /v1/check` accepts the shipped ops-warden request shape and returns
decision envelopes compatible with `policy.enabled: true`.
- A sanitized handoff note exists for ops-warden `WARDEN-WP-0009`.
- State Hub progress is logged and the operator is told to run
`make fix-consistency REPO=flex-auth`.