flex-auth/SCOPE.md
tegwick 0bc624ba62
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 3s
Build and Publish Container Image / build-and-push (push) Successful in 57s
fix(decision): registry facts win over caller-supplied attributes
secrets-engine's first live request rejected our allow: binding.
request_digest is computed over material they never sent, because we
enrich subject and resource from the registry before hashing. Answering
that meant reading the enrichment path, which had a worse defect in it.

Enrichment was additive-if-absent — addAttribute wrote a registry value
only where the request had no value for that key. So where a caller
supplied a key, the caller's value won and the registry's never applied.
Every registry ceiling and allowlist was advisory. Verified against the
shipped ops-warden package, each one added key on an otherwise-denied
request:

  max_ttl_hours: 99      registry says 8    -> allowed a 12h certificate
  allowed_principals     registry allowlist -> disallowed_principal bypassed
  allowed_subjects       registry allowlist -> unknown_subject bypassed

The third is the one to read twice: a subject the registry does not know
authorized itself by naming itself in the allowlist it was being checked
against.

Not remotely reachable today — the PEP builds the CheckRequest,
ops-warden sends no resource.attributes, and enforce admits one identity.
It is a defence-in-depth failure: any path that lets attacker-influenced
data into a CheckRequest field became a full policy bypass rather than a
bounded input problem. Callers sending resource.attributes is not
hypothetical; secrets-engine does it on every request.

Registry facts now win, and diagnostics.registry_overrode names every
displaced key, because a registry that silently discards a contradicting
claim hides that a caller asserted authority it did not have.

subject.type is carved out, and the reason is a finding of its own.
Making the registry win there denied every secrets-engine allow: the
registry's type is CARING vocabulary (Human, Agent, Automation, Service)
and the request's is the protected system's actor vocabulary (service,
adm, agt, atm). Two fields sharing a name; substituting one for the other
is translation rather than identity, which GH-DEC-2026-008 ruled against.
Note what surfaced it — the registry's type had been dead data since the
field existed, because the caller's value always won.

Also publishes binding.submitted_request_digest, over the request exactly
as sent. request_digest was published as the consumer replay test and
cannot be one. Nothing is lost hashing the pre-enrichment form:
enrichment is a function of the request and the snapshot, and
registry_snapshot_digest already pins the snapshot.

Existing pins do not move. All three replay fixtures' request_digest and
approval_binding_digest values are byte-identical — those requests
contradict no registry fact. A field to add, not a value to correct.

Regression tests verified failing against the old behaviour before being
kept. FLEX-DEC-2026-012; FLEX-WP-0025 carries the residual, that a policy
still cannot tell a fact from an assertion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aQMM1dPXaPiXVn6DwwtLd

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 715613@bnt-lap001
Assistant-Session: fabd95c1-4c9e-4080-8849-8707ae025f80
2026-09-07 13:43:33 +02:00

15 KiB

flex-auth Scope

One-liner

flex-auth is a policy-as-code authorization registry and decision engine for NetKingdom-aligned systems. Under the NetKingdom Security Layer Model (security-layer-model_v0.7, accepted 2026-08-29) it is Engine / PDP — the estate's only policy decision point.

Core Idea

flex-auth turns verified identity claims, resource manifests, relationship facts, policy packages, and request context into inspectable authorization decisions. It is intentionally separate from identity providers and from the applications that enforce decisions.

The product should start as a useful standalone local authorization system, then grow into delegated mode where Topaz, OpenFGA, SpiceDB, OPA, Cedar, Keycloak Authorization Services, Entra, Graph, SCIM, LDAP, and similar systems can be coordinated behind a stable flex-auth API.

In Scope

  • Protected-system registration.
  • Resource namespaces, resource hierarchy, and action vocabularies.
  • Subject, group, team, tenant, role, and service-account records needed for authorization.
  • Relationship facts and inherited access.
  • Versioned policy packages with metadata, fixtures, validation, tests, and activation state.
  • Deterministic check, batch-check, list-allowed, explain, and decision-record capabilities.
  • Local decision logs with policy version and provenance.
  • CLI and service entry points for local development and integration tests.
  • Markitect as the first concrete protected-system consumer.
  • Delegated PDP and directory adapter boundaries after the standalone core is stable.
  • The decision-record schema, published as flex-auth's own contract (security-layer-model_v0.7 §17).
  • The canonical request digest as the published replay test for consumers (§6.4.2) — binding.submitted_request_digest, over the request as sent. request_digest is over the enriched request and is flex-auth's own audit-replay identity, not a consumer check (FLEX-DEC-2026-012).
  • Decision lifetimes on every allow, and revocation visibility deadlines stated per input class (§9.7).
  • Registry-snapshot provenance, so a decision that turned on registry content is replayable from its own record (§6.1, §9.7.2).

Out of Scope

  • Acting as an identity provider.
  • Replacing key-cape, NetKingdom SSO, Keycloak, Entra, or other identity systems.
  • Owning login, MFA, PKCE, token issuance, token lifecycle, or canonical identity profile behavior.
  • Embedding protected-system enforcement logic inside flex-auth.
  • Making Markitect-specific assumptions part of the generic authorization model.
  • Forcing a single PDP backend or directory provider.
  • Hiding policy behavior behind opaque administrative toggles.
  • Expressing fail-open. When flex-auth is unreachable there is no evaluator in the path; the residue is the consumer's declared stance (§9.3).
  • Owning enforcement. A PEP's obligations under §6.4 are the consumer's, including its published stance map.
  • Owning the approval object, its storage, or its lifecycle — that is approval-engine's (§9.4). flex-auth validates approvals as input claims and never mutates them.
  • Owning evidence of authentication or assurance. flex-auth consumes assurance claims and never redefines them; it owns evidence of the decision (FLEX-DEC-2026-002, recorded as declined in §13).
  • Authoring security doctrine. gate-house owns invariants, authority ceilings, operating modes, and the authority context flex-auth consumes as claims.

Relevant When

  • A protected system needs resource-specific authorization decisions.
  • A team needs policy-as-code packages that can be reviewed, tested, versioned, and explained.
  • Authorization must combine identity claims, groups, roles, tenants, relationships, context, and resource metadata.
  • A local development workflow needs deterministic authorization behavior before enterprise PDP integrations exist.
  • Markitect resources need central authorization decisions and durable audit records.
  • A delegated backend needs to be introduced without changing the protected-system-facing API.

Not Relevant When

  • The task is only about authentication, login, token issuance, or MFA.
  • The desired outcome can be handled by coarse identity-provider roles alone.
  • The protected system only needs local development labels and no central policy administration.
  • A team is choosing identity-provider configuration rather than resource-specific authorization.

Current State

The standalone core is implemented and all seven baseline workplans (FLEX-WP-0001 through FLEX-WP-0007) are complete. The repository carries the intent baseline, authorization landscape research, ADR set, and a working Go service (cmd/flex-auth) with validate, load-registry, serve, and POST /v1/check plus registry, policy, decision, audit, Markitect, and delegated-adapter internals. The standalone policy-as-code core (FLEX-WP-0002), Markitect consumer integration (FLEX-WP-0003, manifest ingest, decisions, and fixtures), and the delegated PDP/directory adapter shapes (FLEX-WP-0004, Topaz/OpenFGA/OPA/Cedar/Keycloak/Entra tradeoffs documented with at least one controlled adapter shape) all landed in May 2026.

Standalone decisions include a structured binding containing the normalized subject, action, resource, context, and a full request digest. This makes the execute-time decision contract inspectable without parsing reason or diagnostic prose. Durable human approval lifecycle, supersession, and authenticated multi-approver evidence are approval-engine responsibilities; the proposed cross-system object is documented in schemas/action_authorization.schema.json and is not yet a deployed approval-engine endpoint.

secrets-engine is a shipped consumer as of 2026-09-06 (FLEX-WP-0021): secrets-engine.catalog-lane.lifecycle v2 over resource.type: secret-catalog-lane, twelve actions delivered by secrets-engine rather than inferred, destroy gated on a dual-control approval claim, and a dedicated flex-auth-secrets-engine pin at http://flex-auth-secrets-engine.flex-auth.svc.cluster.local.:8080 — trailing dot required, in-cluster only — with callerAuth.mode: warn. Adoption is not complete: secrets-engine is a CLI rather than a workload, and the pin's default-deny ingress admits a pod, so an operator-run access path is still undecided.

v1 of that package had no tenant rule and allowed a foreign tenant; v2 supersedes rather than amends it, because a fail-open correction must be visible to a consumer as a version change (FLEX-DEC-2026-008). The sweep that finding prompted shows tenant-engine unscoped on tenant as well, carried by FLEX-WP-0022.

The first shipped protected-system consumer is ops-warden: its opt-in pre-sign gate calls POST /v1/check for resource.type: ssh-certificate, action: sign decisions (examples/ops-warden/, policy package, allow/deny fixtures, and tests). FLEX-WP-0006 published that gate and FLEX-WP-0007 deployed flex-auth as a reachable production runtime for it. The joint OpenBao-backed smoke is verified (2026-06-29: vault-backed allow recorded decision:032b096c433ad80c; TTL-over-max denied ttl_out_of_bounds by flex-auth before OpenBao). FLEX-WP-0016 then shipped an independently rollable in-cluster ops-warden pin and moved it to callerAuth.mode: enforce once ops-warden adopted a calling identity (WARDEN-WP-0031): anonymous /v1/check is 401, a token bound to another protected system is 403.

Production policy.enabled is deliberately off, and is not coming back as a boolean. ops-warden's ADR-0009 (2026-08-22, superseding ADR-0006) settles it: policy.enabled is one repo-wide flag, and with fail_closed: true it makes flex-auth a hard dependency of every warden sign — including the SSH certificates the ops-bridge tunnels depend on, one of which carries the policy call itself. ADR-0009 retires policy.enabled and the global policy.fail_closed outright and replaces them with a total per-zone stance map in the consumer PEP (ops-warden/pep-stance.yaml): open for z0-z2 and unknown, closed for z3-critical. That map is the reference shape for security-layer-model_v0.7 §6.4 obligation 3. It is no longer the only published one: secrets-engine published pep-stance.yaml (total over catalog stage plus unknown, runtime-read and test-pinned), so §13.1's register now has two rows rather than the one the standard recorded as itself the finding. flex-auth reviewed the pair in docs/stance-register-review.md — the first exercise of the aggregate-divergence capability it claimed on 2026-08-29 — and found that the two maps take opposite stances on unknown (fail_open vs fail_closed) and scope on different axes (security-zone vs catalog-stage), which makes the rows conformant but not comparable.

flex-auth reviewed the zone model as the PDP on 2026-08-19 and holds one boundary: zone membership compiles into the registry snapshot flex-auth already consumes (no schema change needed), while per-zone stance belongs in the versioned policy package, because registry content is absent from decision provenance. Fail-open is not expressible by a PDP at all and stays a consumer-side setting. The enforcing pins stay enforcing regardless — an enforcing pin with no enforcing consumer still turns the anonymous path into a 401 rather than a decision.

Layer position (2026-08-29). The NetKingdom Security Layer Model was accepted at v0.7. flex-auth is Engine / PDP, declared machine-readably in INTENT.md frontmatter per §11. Four of flex-auth's review findings were adopted into the standard: §9.3's two-owner split for degraded mode, §6.4.2 scoped to the decision's own binding with the canonical request digest as its mechanical test and negative caching ruled explicitly, §9.7.2 split by role, and §17 moving the decision-record schema to flex-auth. Conformance state is conforming with one declared gap — the registry-snapshot digest in decision provenance, which §9.7.2 promotes to a conformance prerequisite. FLEX-WP-0019 carries the work.

State Hub integration is present through:

  • .custodian-brief.md
  • state_hub_workstream_id fields in workplans
  • state_hub_task_id fields in task blocks
  • active State Hub dispatch for the flex-auth repo

How It Fits

flex-auth sits between identity and protected systems:

key-cape / NetKingdom SSO
  -> verified identity claims
  -> flex-auth authorization registry and policy decisions
  -> protected systems such as Markitect

Identity systems establish who the caller is and provide coarse claims. flex-auth decides what that caller can do to a specific resource under a specific context. Protected systems enforce the decision and keep their own local diagnostics.

Terminology

  • Subject: Human, service account, group, team, tenant, role, or emergency principal.
  • Resource: Protected object registered by a system.
  • Namespace: Resource type and ownership boundary.
  • Action: Operation requested against a resource.
  • Context: Request, environment, assurance, workflow, or runtime attributes.
  • Policy package: Versioned policy-as-code bundle with metadata and tests.
  • Relationship fact: Subject-resource or resource-resource relation.
  • Decision: Allow, deny, redact, audit-only, or not-applicable outcome with reason and provenance.
  • PDP: Policy decision point, either flex-auth standalone logic or a delegated backend.
  • PEP: Policy enforcement point in the protected system.
  • key-cape / NetKingdom SSO: identity source and coarse claims provider; flex-auth consumes the NetKingdom IAM Profile (~/net-kingdom/canon/standards/iam-profile_v0.2.md).
  • ops-warden: first shipped protected-system consumer. Its opt-in pre-sign gate calls flex-auth for ssh-certificate / sign decisions before issuing a short-lived SSH certificate (FLEX-WP-0006, FLEX-WP-0007). ops-warden owns the SSH CA, OpenBao signing, and actor inventory; flex-auth owns the policy decision. ops-warden's routing charter names flex-auth as the owner of every "may I perform action X?" question.
  • Markitect: first knowledge-pipeline consumer. Integration is complete on the flex-auth side (FLEX-WP-0003 — resource-manifest ingest, Markitect- compatible decisions, and fixtures); a live Markitect runtime calling the gate in production is the next consumer milestone after ops-warden.
  • Topaz: aligned evaluator. Per ADR-003 the standalone core is shaped to match Topaz's Rego + directory model from day one; the Topaz adapter in FLEX-WP-0004 is therefore a small step rather than a conversion.
  • OpenFGA and SpiceDB: candidate relationship authorization backends.
  • OPA and Cedar: candidate rule and typed-policy engines.
  • Keycloak Authorization Services: adapter path for Keycloak-centric deployments. Default architecture is "Keycloak as SSO only, flex-auth owns authorization"; Keycloak AuthZ is one optional delegated PDP.
  • Entra, Graph, SCIM, LDAP, and Keycloak APIs: directory and group resolver sources.

Disjoint From

  • ops-warden is a flex-auth consumer, not an overlap (see Related / Overlapping). The two remain disjoint on identity surface: ops-warden issues SSH certificates for ops actors (adm/agt/atm) and is not a resource-policy engine; flex-auth decides whether a given sign request is allowed and never issues certificates. The once-hypothetical flow of surfacing an agt actor as a flex-auth subject is now realized through the signing policy gate.
  • ops-bridge owns SSH reverse-tunnel connectivity and explicitly disclaims being a credential authority or policy engine. No overlap.

Provided Capabilities

type: orientation
title: Agent orientation
status: current
description: Repo intent, boundaries, research, workplan map, and State Hub agent brief are available for new coding agents.
type: workplan
title: Standalone policy core plan
status: current
description: Workplan for schemas, local registry, policy packages, check APIs, list/explain, decision log, CLI/service skeleton, tests, and examples.
type: workplan
title: Markitect consumer integration plan
status: current
description: Workplan for Markitect resource namespace, manifest import, action vocabulary, check fixtures, contract tests, and integration flow.
type: workplan
title: Delegated adapter plan
status: current
description: Workplan for Topaz evaluation, relationship PDP adapters, rule PDP adapters, Keycloak AuthZ path, directory resolvers, and operations docs.
type: contract
title: Decision-record contract
status: current
description: Published decision-record schema, the submitted-request digest as the replay test for consumers, request-enrichment precedence, and decision provenance — flex-auth's own contract under security-layer-model v0.7 section 17.
type: orientation
title: Layer declaration
status: current
description: Machine-readable Engine/PDP declaration in INTENT.md frontmatter with conformance state and the one declared gap, per security-layer-model v0.7 section 11.