Commit graph

10 commits

Author SHA1 Message Date
afd9be5aa9 fix: the address we published was a misdirection, and the channel is unauthenticated
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s
Build and Publish Container Image / build-and-push (push) Successful in 57s
secrets-engine probed the Service DNS name handed over in FLEX-WP-0021-T05
and found it resolves, from the workstation, to an unrelated public host.
Reproduced here: search ad.binect.de answers wildcard, so
flex-auth-secrets-engine.flex-auth.svc.cluster.local and
this-service-does-not-exist.flex-auth.svc.cluster.local both resolve to
80.158.43.29, while the trailing-dot FQDN correctly fails. A bare Service
name in a handover is not merely unreachable from there, it is a live
misdirection, and the handover was ours.

Had a deployment pointed at it, the CheckRequest body would have gone to
that host: subject, tenant, lane and resource ids, stage, field names,
purpose, plus the caller's bearer token.

Trailing-dot FQDN and "in-cluster only" now replace the bare name in the
example README, SCOPE.md, and the T05 note.

Their real question was how the response channel is authenticated, and
they declined to answer it locally because choosing a transport control
for our service is not a consumer's call. Right boundary, so the answer
is recorded here as FLEX-DEC-2026-010: it is not authenticated. Pins
serve plain HTTP, the envelope carries no signature, and a responder that
knows the package id and version can return a well-formed allow that
passes every check a consumer performs.

The part worth stating in the contract is that the digests do not help
and look like they do. Every input to request_digest,
policy_package_digest and registry_snapshot_digest is either sent by the
caller or published in this repo, so a forger reproduces all three
exactly. They establish integrity of the binding, never authenticity of
the source — and publishing more digests makes a forged envelope look
more authenticated, not less.

For secrets-engine specifically: fail-closed protects against a PDP that
is absent, not against one that lies. An unreachable PDP denies; a lying
PDP allows.

Third instance of one seam in three decisions. 008: a tenant carried
into the digest and never compared — visible, not enforced. 009: a caller
authenticated and never recorded — enforced, not visible. 010: a record
verifiable and unauthentic — checkable, but not evidence.

One nuance that changes the operator recommendation: kubectl port-forward
does authenticate the responder, transitively — no DNS name, one named
pod, API-server TLS. That is the exact reverse of the caller direction,
where it bypasses the NetworkPolicy. Independent properties pointing
opposite ways, so neither can be summarised as "the network protects it".

FLEX-WP-0024 carries signing; key custody routes through warden/OpenBao
rather than minting a key here.

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-06 22:44:45 +02:00
a96e970007 Pin deployed secrets-engine tenant policy v2
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
2026-09-06 21:39:10 +02:00
d98323b2bb fix(secrets-engine): v2 adds the tenant rule v1 never had
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 41s
secrets-engine.catalog-lane.lifecycle v1 contained no reference to
input.tenant — not in well_formed, not in the denial ladder, not in a
test. A rotate on lane:glas-primary under tenant:coulomb returned allow
against the deployed package (decision:066e629bbf0c0924).

Found while answering glas-harness's tenant-alignment request, which had
asked for wrong-tenant denial evidence. There was none to return.

Three covers failed the same way: every one of the 29 fixtures carried
tenant:platform, so the suite could not report on the field; T02's own
gate named "wrong-tenant deny" and was recorded done unmet; and the
engine hashes tenant into request_digest but never compares it. Four
other published packages carry the branch — this one was the outlier.

v2 adds wrong_tenant above wrong_system, three Rego tests and three
fixtures (28/28, 32/32). The absent-tenant test caught a second defect
in the first draft: a bare input.tenant != comparison is undefined on a
missing key, so the branch dropped and the ladder reported the wrong
rung. request_tenant := object.get(input, "tenant", "") fixes it.

v2 supersedes rather than amends v1 because the defect failed open: a
consumer pinned to _VERSION=v1 would keep receiving allows with no
signal the rule beneath the version string had changed. The earlier
dual-control correction stayed at v1 because it denied everything.

Replay envelopes regenerated at v2; both request_digest values are
byte-identical, so secrets-engine's digest join needs no re-pinning.

The sweep this prompted found tenant-engine unscoped on tenant as well —
deployed, and verified allowing tenant:coulomb. Not the same fix: its
request tenant names the target rather than the caller, so a constant
would break it. Recorded and carried by FLEX-WP-0022 rather than patched
unilaterally.

FLEX-WP-0021 closes at T05; the pin still serves v1 until a redeploy.

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-06 20:38:45 +02:00
0b4e5c28bc deploy: add verified secrets-engine production policy pin
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
2026-09-06 19:45:40 +02:00
dd3ce4c109 Publish approval_binding_digest: a claim cannot name the request carrying it
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run
Build and Publish Container Image / build-and-push (push) Successful in 52s
secrets-engine confirmed T03, and re-verifying against the regenerated
destroy fixture found something neither repository can fix alone: a
pdp_digest recorded at issue time can never equal the request_digest of a
request that carries the claim in its context, because the claim is part
of the context that is hashed. Embedding the claim changes the very
digest the claim would need to name.

Not fixture staleness. It holds for every dual-control request whose
claim travels in context -- the shape GH-DEC-2026-008 had just ruled
mandatory. Left unresolved that ruling was unimplementable for exactly
the case it was written for, and destroy would have been permanently
un-allowable in production, failing closed forever on a check that could
never pass.

flex-auth owns the canonical request digest, so the fix is ours.
binding.approval_binding_digest is the same material with
context.approval removed, emitted only when a claim was carried. An
approval issued against a claim-free Check records that Check's
request_digest; the claim-bearing request reproduces it here.

DELIBERATELY ADDITIVE, and the reason matters. The tempting fix is to
drop context.approval from request_digest entirely. That is wrong:
request_digest is the replay identity, and two requests differing only in
which approval was presented must not share one, because their decisions
differ -- one allows, the other denies dual_control_required. Collapsing
them would let an allow obtained with a valid claim be replayed against a
request carrying none. So request_digest still covers the claim and still
moves; approval_binding_digest deliberately does not, and is documented
as not a replay identity. The tests assert the two functions DISAGREE on
a claim-bearing request, which is approval-engine's formulation of how to
defend a distinction that looks like duplication.

The fixture now demonstrates the property rather than asserting it: its
claim's pdp_digest equals the envelope's approval_binding_digest with
pdp_path true, and changing the claim's contents moved request_digest
while leaving approval_binding_digest untouched. Two files a consumer can
diff.

Also picked up approval-engine's new required binding.pdp_path via the
cross-repo schema test added yesterday -- which is the test doing exactly
what it was built for, one day later.

T03 is done. secrets-engine's own digest-material defect, which our two
real envelopes caught, is recorded in the workplan.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 412054@bnt-lap001
Assistant-Session: 3968fae1-8d59-4209-9bd6-c22594b8ab19
2026-09-06 14:52:33 +02:00
c3ede0b494 Review the stance register's second row; record why T04 cannot proceed
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
TWO FINDINGS, BOTH FROM READING OTHER REPOSITORIES RATHER THAN OURS.

The §13.1 stance register has a second row. secrets-engine published
pep-stance.yaml -- total over catalog stage plus unknown, runtime-read
and pinned to SHIPPED_STANCE by test. SCOPE.md claimed ops-warden's was
the estate's only published map and that the register's single row was
itself the finding; that is no longer true and is corrected.

docs/stance-register-review.md is the first exercise of the
aggregate-divergence capability flex-auth claimed on 2026-08-29 and then
recorded as unexercised because one row cannot diverge from anything.
Three findings:

The two maps take opposite stances on unknown -- ops-warden fail_open by
versioned build profile under ADR-0009, secrets-engine fail_closed. Both
conformant, neither a defect, and they disagree about the one case nobody
planned for. Reported as an observation for gate-house's register, not as
a request that either repository change: a PDP does not set a consumer's
stance, and §9.3's two-owner split is our own finding.

The rows are not comparable. ops-warden scopes by security-zone,
secrets-engine by catalog-stage. §6.4 permits both, but the register
cannot then answer what the estate's stance is for a z2 workload. Worth
recording before a third row arrives.

secrets-engine's map defines fail_closed in terms of a durable
ActionAuthorization record, which GH-DEC-2026-005 shelved. The stance is
unaffected -- only the artifact name is stale -- but the file is read at
runtime and pinned by test, so the stale name outlives a comment.

SEPARATELY, T04 IS BLOCKED AND THE REASON IS STRUCTURAL. The task assumed
the ops-warden/tenant-engine/user-engine pattern, where the pin's
default-deny NetworkPolicy admits one approved consumer workload.
secrets-engine has no Kubernetes deployment at all -- it is a CLI. There
is no pod selector to write, and inventing one would repeat the error
corrected in T02. Three possible shapes recorded in the workplan and
raised with them; callerAuth, not the NetworkPolicy, becomes the real
boundary if an operator CLI is the caller, so the FLEX-WP-0016 precedent
does not transfer unexamined.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 412054@bnt-lap001
Assistant-Session: 3968fae1-8d59-4209-9bd6-c22594b8ab19
2026-09-06 09:31:40 +02:00
9e10d1cec1 Emit T03 replay fixtures from the published package
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 37s
Two real DecisionEnvelopes in examples/secrets-engine/replay/ for
secrets-engine to verify its digest join unchanged: a plain allow
(rotate, empty context) and the dual-control allow (destroy with a valid
approval-claim).

Both are included deliberately. input_claim_digests.context appears only
when the request carries a non-empty context, so a consumer asserting the
field is always present would pass on destroy and fail on rotate. One
fixture would have hidden that.

request_digest, policy_package_digest, registry_snapshot_digest and the
context claim digest are verified identical across two runs and are the
fields to pin. id, decision_time and the lifetime bounds move with the
clock; the README says so rather than leaving a consumer to discover it
by flake. lifetime.ttl is 15m from the package allow_ttl.

Emitted from flex-auth/local in standalone mode, not from a cluster pin.
T03 is progress, not done -- it closes when secrets-engine confirms their
validator accepts the records unchanged.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 412054@bnt-lap001
Assistant-Session: 3968fae1-8d59-4209-9bd6-c22594b8ab19
2026-09-06 08:14:14 +02:00
f75db59a8c Publish secrets-engine.catalog-lane.lifecycle v1 (FLEX-WP-0021 T01, T02)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 52s
secrets-engine delivered the action vocabulary T01 asked for: twelve
actions read out of cli.py, not the example vocabulary. The gate earned
its keep -- four would have been inferred wrongly, and revoke, the
obvious thirteenth, does not exist as an action at all. It gates as
deactivate, which is also reached from lifecycle deactivate.

docs/secrets-engine-action-vocabulary.md records the list and the four
traps. examples/secrets-engine/ carries the package, both manifests, a
loadable registry snapshot, 26 fixtures, five check requests, and a
README. validate -kind policy is valid with 22/22 Rego tests and 26/26
fixtures.

allow_ttl is 15m, stated in the package rather than inherited from the
engine default: these decisions authorize live secret operations, so the
reliance window belongs where a reviewer sees it. Per FLEX-DEC-2026-004
it is authority to issue, not authority to keep using what the operation
produced.

destroy is the dual-control case, gated on a context.approval claim
marked approved with two distinct approvers, repeated entries counting
once. flex-auth checks what the claim says and deliberately does not
re-derive its temporal validity, signature, or supersession -- those are
approval-engine's to assert and the PEP's to verify against the live
claim, per the split accepted in FLEX-DEC-2026-006. It stays in the
package though its handler raises before the gate, so the rule is
reviewed and fixtured before SECRETS-WP-0007-T04 opens the path.

Following the FLEX-WP-0010-T02 precedent the denial ladder has no
action_not_granted branch: one subject holding all twelve actions could
never reach it, and a rule that cannot fail reads as control that is not
there. Registering a second calling identity is the revisit trigger.

Not deployed. No flex-auth-secrets-engine pin exists yet (T04), so their
policy pin stays unset and fail-closed until T05.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 412054@bnt-lap001
Assistant-Session: 3968fae1-8d59-4209-9bd6-c22594b8ab19
2026-09-06 08:02:52 +02:00
d144feb2e9 chore(consistency): write back State Hub identifiers [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JTbVXpEiXA7mNJVpDnEPcB

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 412054@bnt-lap001
Assistant-Session: 3968fae1-8d59-4209-9bd6-c22594b8ab19
2026-09-06 01:14:52 +02:00
6a6464fcc9 Answer ops-warden and secrets-engine; open FLEX-WP-0021
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 1s
Two cross-repo questions arrived in the flex-auth inbox and both are
answered as decision records rather than as prose in a message.

FLEX-DEC-2026-004 answers ops-warden WARDEN-WP-0034-T05. A decision
lifetime shorter than the SSH certificate TTL is meaningful, but only as
authority to issue, never as authority to use an already-issued
certificate. The pre-sign gate is the only consumer of the shorter
lifetime: no replay past expires_at, fresh Check per sign. The lever that
shortens effective access is the requested TTL as a policy input, which
is already deployed as the ttl_out_of_bounds deny. ops-warden's section
9.7.2 window through certificate TTL is correct as written and correctly
owned by the PEP; flex-auth does not want that residue moved to the PDP.

docs/decision-input-freshness.md gains the same boundary as published
contract text, so the ruling is not only in the decision log.

FLEX-DEC-2026-005 answers secrets-engine. A real policy package is
expected and flex-auth authors it here as it does for every consumer; the
reserved coordinate is secrets-engine.catalog-lane.lifecycle v1 and it
does not exist yet. Their choice not to default the pin was correct and
is endorsed explicitly. POST /v1/check is deployed but has no
estate-wide address by design -- per-consumer cluster-local pins with
default-deny ingress -- so their 2026-09-06 probe found the design
working, not an outage.

FLEX-WP-0021 carries that work: obtain the real action vocabulary from
secrets-engine, publish the package with fixtures, confirm the digest
join against a real decision record, then stand up a
flex-auth-secrets-engine pin in warn without moving the other two pins.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 412054@bnt-lap001
Assistant-Session: 3968fae1-8d59-4209-9bd6-c22594b8ab19
2026-09-06 01:12:50 +02:00