diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 8066959..b10d646 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -46,6 +46,7 @@ | workplan | FLEX-WP-0034 | finished | — | workplans/FLEX-WP-0034-informed-decision-review-discovery.md | | workplan | FLEX-WP-0035 | finished | — | workplans/FLEX-WP-0035-per-identity-application-readback.md | | workplan | FLEX-WP-0036 | active | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md | +| workplan | FLEX-WP-0037 | active | — | workplans/FLEX-WP-0037-ops-warden-agt-custodian-actor.md | | task | FLEX-WP-ADHOC-2026-10-03-T01 | done | — | workplans/ADHOC-2026-10-03.md | | task | FLEX-WP-ADHOC-2026-10-03-T02 | wait | — | workplans/ADHOC-2026-10-03.md | | task | FLEX-WP-ADHOC-2026-10-11-T01 | done | — | workplans/ADHOC-2026-10-11.md | @@ -224,6 +225,9 @@ | task | FLEX-WP-0036-T06 | wait | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md | | task | FLEX-WP-0036-T07 | todo | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md | | task | FLEX-WP-0036-T08 | todo | — | workplans/FLEX-WP-0036-runtime-rename-access-engine.md | +| task | FLEX-WP-0037-T01 | done | — | workplans/FLEX-WP-0037-ops-warden-agt-custodian-actor.md | +| task | FLEX-WP-0037-T02 | progress | — | workplans/FLEX-WP-0037-ops-warden-agt-custodian-actor.md | +| task | FLEX-WP-0037-T03 | wait | — | workplans/FLEX-WP-0037-ops-warden-agt-custodian-actor.md | | intake | FLEX-IN-0001 | closed | — | intakes/intakes.md | | intake | FLEX-IN-0002 | closed | — | intakes/intakes.md | | intake | FLEX-IN-0003 | open | — | intakes/intakes.md | diff --git a/cmd/access-engine/main_test.go b/cmd/access-engine/main_test.go index f2db0ef..af4b0c2 100644 --- a/cmd/access-engine/main_test.go +++ b/cmd/access-engine/main_test.go @@ -285,7 +285,7 @@ func TestRunLoadRegistryOpsWardenProduction(t *testing.T) { if err := json.Unmarshal(stdout.Bytes(), &result); err != nil { t.Fatalf("unmarshal load-registry output: %v; stdout = %s", err, stdout.String()) } - if result["subjects"] != float64(4) || result["relationships"] != float64(4) || result["resource_manifests"] != float64(1) { + if result["subjects"] != float64(5) || result["relationships"] != float64(5) || result["resource_manifests"] != float64(1) { t.Fatalf("load-registry result = %+v; want production actor registry counts", result) } } @@ -336,6 +336,36 @@ func TestOpsWardenProductionRegistryActors(t *testing.T) { wantEffect: api.DecisionEffectAuditOnly, wantReason: "advisory_would_signing_policy_matched", }, + { + name: "custodian agent proceeds with its own principal while zone is unknown", + subjectID: "agt-custodian", + actor: "agt-custodian", + actorType: "agt", + principal: "agt-custodian", + ttlHours: 12, + wantEffect: api.DecisionEffectAuditOnly, + wantReason: "advisory_would_signing_policy_matched", + }, + { + name: "custodian agent IAM subject proceeds while zone is unknown", + subjectID: "iam:agt-custodian", + actor: "agt-custodian", + actorType: "agt", + principal: "agt-custodian", + ttlHours: 1, + wantEffect: api.DecisionEffectAuditOnly, + wantReason: "advisory_would_signing_policy_matched", + }, + { + name: "custodian agent unknown principal is not matched", + subjectID: "agt-custodian", + actor: "agt-custodian", + actorType: "agt", + principal: "adm-full", + ttlHours: 1, + wantEffect: api.DecisionEffectAuditOnly, + wantReason: "advisory_would_disallowed_principal", + }, { name: "admin actor allow", subjectID: "adm-example", diff --git a/docs/ops-warden-registry-sync.md b/docs/ops-warden-registry-sync.md index 5404f56..ea484d3 100644 --- a/docs/ops-warden-registry-sync.md +++ b/docs/ops-warden-registry-sync.md @@ -91,6 +91,7 @@ It registers: | --- | --- | --- | --- | --- | | adm-example | adm | adm-full | 48 | adm-example, iam:adm-example | | agt-codex-interhub-bootstrap | agt | agt-interhub-bootstrap | 2 | agt-codex-interhub-bootstrap, iam:agt-codex-interhub-bootstrap | +| agt-custodian | agt | agt-custodian | 12 | agt-custodian, iam:agt-custodian (added 2026-10-20, FLEX-WP-0037) | | agt-state-hub-bridge | agt | agt-task-bridge | 24 | agt-state-hub-bridge, iam:agt-state-hub-bridge | | atm-backup-daily | atm | atm-backup-daily | 8 | atm-backup-daily, iam:atm-backup-daily | diff --git a/examples/ops-warden/production_registry_snapshot.json b/examples/ops-warden/production_registry_snapshot.json index cba4291..09ce5b3 100644 --- a/examples/ops-warden/production_registry_snapshot.json +++ b/examples/ops-warden/production_registry_snapshot.json @@ -108,6 +108,31 @@ "security_zone_reason": "workload_resolution_absent" } }, + { + "id": "ssh-cert:actor/agt-custodian", + "type": "ssh-certificate", + "labels": [ + "ssh-signing", + "agt" + ], + "owner": "team:platform-security", + "attributes": { + "actor_id": "agt-custodian", + "actor_type": "agt", + "allowed_subjects": [ + "agt-custodian", + "iam:agt-custodian" + ], + "allowed_principals": [ + "agt-custodian" + ], + "max_ttl_hours": 12, + "workload_id": "custodian-agents", + "security_zone": "unknown", + "security_zone_admission": "unknown", + "security_zone_reason": "workload_resolution_absent" + } + }, { "id": "ssh-cert:actor/agt-state-hub-bridge", "type": "ssh-certificate", @@ -208,6 +233,22 @@ "actor_type": "agt" } }, + { + "id": "agt-custodian", + "type": "Agent", + "display_name": "Supervised interactive custodian agent (custodian-agt on Railiance01)", + "organization_relation": "ServiceProvider", + "roles": [ + "Operator" + ], + "groups": [ + "group:ops-warden-agents" + ], + "tenant": "tenant:platform", + "metadata": { + "actor_type": "agt" + } + }, { "id": "agt-state-hub-bridge", "type": "Agent", @@ -255,6 +296,7 @@ "display_name": "Ops Warden Agents", "members": [ "agt-codex-interhub-bootstrap", + "agt-custodian", "agt-state-hub-bridge" ], "tenant": "tenant:platform" @@ -363,6 +405,53 @@ "access_path": "mediated" } }, + { + "id": "rel:agt-custodian-sign-agt-custodian", + "system": "ops-warden", + "subject": "group:ops-warden-agents", + "relation": "signer", + "object": "ssh-cert:actor/agt-custodian", + "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/agt-custodian", + "tenant": "tenant:platform", + "resource": "ssh-cert:actor/agt-custodian" + }, + "planes": [ + "Identity", + "Secret", + "Audit" + ], + "capabilities": [ + "Use", + "Operate", + "Audit" + ], + "exposure_modes": [ + "Metadata" + ], + "conditions": [ + "TimeLimited", + "Logged" + ], + "restrictions": [ + "PrivilegeEscalationBlocked", + "SecretAccessBlocked" + ], + "access_path": "mediated" + } + }, { "id": "rel:agt-state-hub-bridge-sign-agt-state-hub-bridge", "system": "ops-warden", diff --git a/workplans/FLEX-WP-0037-ops-warden-agt-custodian-actor.md b/workplans/FLEX-WP-0037-ops-warden-agt-custodian-actor.md new file mode 100644 index 0000000..a2a93a9 --- /dev/null +++ b/workplans/FLEX-WP-0037-ops-warden-agt-custodian-actor.md @@ -0,0 +1,108 @@ +--- +id: FLEX-WP-0037 +type: workplan +title: "Register ops-warden actor agt-custodian in the signing-policy registry and roll the flex-auth-ops-warden pin" +domain: infotech +repo: access-engine +status: active +owner: claude +topic_slug: netkingdom +created: "2026-10-20" +updated: "2026-10-20" +origin: demand +origin_ref: CUST-WP-0073-T02 (warden sign agt-custodian refused with unknown_actor_resource) +related: [CUST-WP-0073, WARDEN-WP-0045, FLEX-WP-0016, FLEX-WP-0007] +--- + +ops-warden `WARDEN-WP-0045` (commits `e62763c`, `a07c6c7`) added the actor +`agt-custodian` (type `agt`, principal `agt-custodian`, default and max TTL +12h, extension `permit-pty`, zone subject workload `custodian-agents`) for the +supervised interactive custodian agent of `CUST-WP-0073-T02`. The in-cluster +deployment `flex-auth/flex-auth-ops-warden` serves the registry snapshot baked +into its image (`/opt/flex-auth/examples/ops-warden/production_registry_snapshot.json`), +which does not know the actor, so `warden sign agt-custodian` is refused with +`unknown_actor_resource`. + +## Add agt-custodian to the production fixture + +```task +id: FLEX-WP-0037-T01 +status: done +priority: high +``` + +Regenerated the snapshot with ops-warden `scripts/build_flex_auth_registry.py` +from the live inventory and took **only** the `agt-custodian` elements +(resource `ssh-cert:actor/agt-custodian`, subject `agt-custodian`, membership +in `group:ops-warden-agents`, relationship +`rel:agt-custodian-sign-agt-custodian`). The diff to +`examples/ops-warden/production_registry_snapshot.json` is additions only. + +Drift deliberately not taken (needs its own owner decision): + +- live inventory also carries `agt-rehearsal` (principal + `rehearsal-inert-nohost`, 2h), which is not in ops-warden's committed + snapshot either; +- the generator now emits `security_zone_admission: unknown` / + `catalog_applicability_absent` for `adm-example` (fixture: `not-applicable`, + human operator) and drops `workload_id: codex-interhub-bootstrap` from + `agt-codex-interhub-bootstrap`. + +Validation: `access-engine load-registry` loads 5 subjects / 5 relationships; +`go test ./...` passes. `TestOpsWardenProductionRegistryActors` gains +agt-custodian cases: own principal (actor and `iam:` subject) proceeds with +`advisory_would_signing_policy_matched`; a foreign principal yields +`advisory_would_disallowed_principal`. Both are `audit_only` because the +actor's security zone resolves `unknown` (same advisory phase as every other +agt actor in this fixture). + +## Build the image and pin flex-auth-ops-warden + +```task +id: FLEX-WP-0037-T02 +status: progress +priority: high +``` + +Pushing `examples/**` to main triggers `.forgejo/workflows/image.yaml`, which +publishes `forgejo.coulomb.social/coulomb/flex-auth:main-`. Take the +digest from the "Report immutable digest" step and set it in +`values/ops-warden.yaml` only (the fetch pin, `values/ops-warden-fetch.yaml`, +serves a different registry and stays where it is). Rollback target is the +current pin `sha256:138aa3471c46bca6e814691fa1e6520aedda3dffd743e6b09141ab433afdb64b`. +flex-auth has no Argo Application; the pin is applied by an attended Helm +upgrade, so a pushed pin does not deploy by itself. + +## Attended rollout and smoke + +```task +id: FLEX-WP-0037-T03 +status: wait +priority: high +needs_human: true +blocking_reason: "Attended helm upgrade of flex-auth-ops-warden on railiance01 plus an allow smoke; agents may not change the live cluster." +``` + +```bash +helm upgrade --install flex-auth-ops-warden charts/flex-auth \ + --namespace flex-auth -f values/ops-warden.yaml --wait --timeout 2m +kubectl -n flex-auth get deploy flex-auth-ops-warden \ + -o jsonpath='{.spec.template.spec.containers[0].image}' +``` + +Smoke through the `127.0.0.1:19090` tunnel (caller auth is `enforce`, so the +request needs the ops-warden ServiceAccount bearer; the simplest real smoke is +`warden sign agt-custodian` itself): + +```bash +curl -s -X POST http://127.0.0.1:19090/v1/check \ + -H 'Content-Type: application/json' -H "Authorization: Bearer $OPS_WARDEN_SA_TOKEN" \ + -d '{"id":"check:smoke-agt-custodian","tenant":"tenant:platform", + "subject":{"id":"agt-custodian","type":"agt"},"action":"sign", + "resource":{"id":"ssh-cert:actor/agt-custodian","type":"ssh-certificate","system":"ops-warden"}, + "context":{"principals":["agt-custodian"],"actor_type":"agt","ttl_hours":1, + "pubkey_fingerprint":"SHA256:smoke"}}' +``` + +Expected: `effect` `audit_only` (or `allow` once the zone resolves), reason +`advisory_would_signing_policy_matched` (not `unknown_actor_resource`).