Add attended signer adapter with phase-aware read-only recovery
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
parent
7be8b6e76d
commit
a3e8eb64ba
11 changed files with 1385 additions and 1 deletions
|
|
@ -0,0 +1,128 @@
|
||||||
|
id: CCR-2026-0050
|
||||||
|
kind: credential-change-request
|
||||||
|
schema_version: 1
|
||||||
|
request_type: workload-kv-read
|
||||||
|
title: Retained flex-auth lifecycle PDP Ed25519 seed ESO read lane
|
||||||
|
status: proposed
|
||||||
|
created: '2026-10-16'
|
||||||
|
updated: '2026-10-16'
|
||||||
|
requester:
|
||||||
|
agent: railiance-platform
|
||||||
|
reason: CUST-WP-0080 / RPF-WP-0035-T12 / FLEX-WP-0024-T06 source proposal. Exact
|
||||||
|
new custody/read scope requires founder review; no approval or native binding
|
||||||
|
is inferred.
|
||||||
|
review:
|
||||||
|
required: true
|
||||||
|
required_approvers:
|
||||||
|
- founder
|
||||||
|
- railiance-platform-owner
|
||||||
|
- access-engine-owner
|
||||||
|
- secrets-engine-owner
|
||||||
|
- cluster-gitops-owner
|
||||||
|
comments: []
|
||||||
|
target:
|
||||||
|
domain: infotech
|
||||||
|
tenant: platform
|
||||||
|
workload: flex-auth-secrets-engine
|
||||||
|
environment: production
|
||||||
|
purpose: Deliver the Ed25519 seed only to the retained lifecycle PDP startup file.
|
||||||
|
No taskpilot enablement, caller policy changes or private consumer trust discovery.
|
||||||
|
openbao:
|
||||||
|
mount: platform
|
||||||
|
kv_path: platform/workloads/flex-auth/envelope-signing
|
||||||
|
fields:
|
||||||
|
- ED25519_PRIVATE_KEY
|
||||||
|
metadata_read: false
|
||||||
|
token_self_lifecycle: true
|
||||||
|
policy_name: workload-kv-read-flex-auth-envelope-signing
|
||||||
|
policy_file: openbao/policies/workload-kv-read-flex-auth-envelope-signing.hcl
|
||||||
|
auth:
|
||||||
|
method: kubernetes
|
||||||
|
mount: kubernetes
|
||||||
|
role: external-secrets-flex-auth-envelope-signing
|
||||||
|
bound_claims:
|
||||||
|
service_account_names:
|
||||||
|
- flex-auth-envelope-signing-eso
|
||||||
|
service_account_namespaces:
|
||||||
|
- flex-auth
|
||||||
|
bound_claims_confirmed: false
|
||||||
|
policies:
|
||||||
|
- workload-kv-read-flex-auth-envelope-signing
|
||||||
|
ttl: 15m
|
||||||
|
audience: openbao
|
||||||
|
token_max_ttl: 15m
|
||||||
|
token_explicit_max_ttl: 15m
|
||||||
|
token_no_default_policy: true
|
||||||
|
access_frontdoor:
|
||||||
|
type: external-secrets
|
||||||
|
catalog_id: flex-auth-decision-envelope-signing-key
|
||||||
|
selector: Retained flex-auth/flex-auth-secrets-engine signer only
|
||||||
|
readiness: pending-review
|
||||||
|
resolvable: false
|
||||||
|
delivery:
|
||||||
|
surface: external-secrets
|
||||||
|
target: Namespace flex-auth; dedicated SA flex-auth-envelope-signing-eso; SecretStore
|
||||||
|
openbao-flex-auth-envelope-signing; ExternalSecret/Secret flex-auth-secrets-engine-envelope-signing;
|
||||||
|
seed property only -> startup file /var/run/secrets/decision-envelope-signing/seed0440,
|
||||||
|
readOnly uid/gid/fsGroup65532; no envFrom
|
||||||
|
bootstrap_command: Attended writer is a distinct exact-source action proposal in
|
||||||
|
docs/credential-lane-designs/flex-auth-envelope-signing-native-plan.json; this
|
||||||
|
read CCR authorizes neither seed writes nor any execution by itself.
|
||||||
|
risk:
|
||||||
|
classification: high
|
||||||
|
notes:
|
||||||
|
- Possession of seed can forge lifecycle PDP envelopes; independent consumer public
|
||||||
|
trust admission and fixed policy pins remain required.
|
||||||
|
- 'KV read is document-granular: dedicated ESO identity can read all three fields;
|
||||||
|
only private field is projected. Signer and Secrets consumer have no KV read role.'
|
||||||
|
- No parent/list/metadata/write/delete capability on ESO role. No token/Secret read
|
||||||
|
or ExternalSecret mutation granted to signer/consumer.
|
||||||
|
- Dedicated role is new scope; founder approval required. No workload rapp tuple
|
||||||
|
invented; absent managed package declaration remains explicit migration debt.
|
||||||
|
- Controlled outage fence stops every signer pod and disables autoscaling before
|
||||||
|
creation/rotation. Recreate single writer and retained PVC remain Access-owned
|
||||||
|
review gates.
|
||||||
|
- fields declares only projected ED25519_PRIVATE_KEY. The same KV document also
|
||||||
|
has public key/kid, and OpenBao data-read is not field-filtered; do not claim
|
||||||
|
field-level authorization.
|
||||||
|
verification:
|
||||||
|
positive:
|
||||||
|
- Exact SA/namespace/audience login; native exact data read; ESO Ready/SecretSynced;
|
||||||
|
safe metadata resourceVersion and admitted KV version correspondence.
|
||||||
|
- All retained signer replicas restart with admitted kid/source/image and seed/public
|
||||||
|
correspondence; signed non-effect lifecycle allow/deny with durable originals
|
||||||
|
and independent consumer signature verification.
|
||||||
|
negative:
|
||||||
|
- Wrong SA/namespace/audience login refused; sibling/list/metadata/write/delete
|
||||||
|
denied; signer/consumer TokenRequest and Secret/ExternalSecret mutation denied.
|
||||||
|
- Unknown/old kid, wrong policy, tamper, expired/unsigned envelope, wrong caller
|
||||||
|
and missing seed refuse; no taskpilot activation.
|
||||||
|
- Receipt write failure/lost backend response retained unknown; no blind retry/remint;
|
||||||
|
stale CAS cannot overwrite version.
|
||||||
|
activation_conditions:
|
||||||
|
- All five required approvers independently dispose the exact CCR and native writer/fence
|
||||||
|
proposal; founder exact new custody scope approved.
|
||||||
|
- Current retained identity919ce25, immutable published producer image and explicit
|
||||||
|
new flex-auth-secrets-engine-outbox PVC1Gi/local-path, retention/reclaim and bounded-runtime
|
||||||
|
plan reviewed by Access; missing managed package migration debt explicitly accepted
|
||||||
|
for this retained workload only.
|
||||||
|
- Exact native bound claims verified without printing JWT/Secret; read grant policy
|
||||||
|
and role native readback match source.
|
||||||
|
- Protected child source pinned, silent attended OIDC context verified, target cluster/backend
|
||||||
|
pinned, fence verified and durable receipt custody installed.
|
||||||
|
- Consumer independently admits owner-authenticated public receipt and exact policy/image/trust
|
||||||
|
pins before protected acceptance.
|
||||||
|
evidence: []
|
||||||
|
lifecycle:
|
||||||
|
deactivate: Fence all retained signer processes, stop consumers, disable delivery
|
||||||
|
role/ESO and online trust; retain private KV and audit receipts under explicit
|
||||||
|
owner retention until reviewed destruction.
|
||||||
|
rotate: Pre-admit successor public receipt offline; fence consumers/all signer replicas/no
|
||||||
|
HPA; CAS exact version with new in-memory seed; wait exact ESO version, restart
|
||||||
|
single writer, verify native allow/deny and old-key refusal before reopening.
|
||||||
|
compromised: Immediately remove old online trust and stop all signer processes;
|
||||||
|
forward-only CAS recovery with independently approved successor. Never restore
|
||||||
|
compromised seed; archive historical public keys for offline audit only.
|
||||||
|
state_hub:
|
||||||
|
workplan_id: RPF-WP-0035
|
||||||
|
task_id: RPF-WP-0035-T12
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
# Attended native signer custody adapter source
|
||||||
|
|
||||||
|
RPF-WP-0035-T12 / CUST-WP-0080. This source implements the protected native
|
||||||
|
adapter described by the separate frozen native-preparation R1. It does not
|
||||||
|
admit an execution. Its CLI is silent and intended only as the exact reviewed
|
||||||
|
child of `warden access openbao-platform-admin-login --exec` in the founder's
|
||||||
|
attended OIDC envelope. The existing Warden owner contains the token helper,
|
||||||
|
checks child output and revokes/removes the session on exit. No new service,
|
||||||
|
credential broker, standing grant, API or key-returning interface is created.
|
||||||
|
|
||||||
|
## Current routing blocker
|
||||||
|
|
||||||
|
During preparation the required `warden plan` query for attended OpenBao
|
||||||
|
platform administration of signer CAS0/metadata selected `openbao-structure-survey`
|
||||||
|
with an approve ceremony, rather than `openbao-platform-admin-login` with
|
||||||
|
`oidc_login`. The installed CLI reports wheel-bundled catalog0.1.2 fallback.
|
||||||
|
This is preserved as a live refusal, not routed around. Exact `route show` names
|
||||||
|
the existing active attended login, but that pointer does not cure dispatch.
|
||||||
|
The adapter requires an independently retained routing receipt selecting the
|
||||||
|
correct lane/verdict/identity act. No founder execution step is ready until the
|
||||||
|
routing defect and all other native approvals are resolved by their owners.
|
||||||
|
|
||||||
|
## Independent input and authority boundary
|
||||||
|
|
||||||
|
The CLI requires an absolute owner-private admission file, its independently
|
||||||
|
approved exact SHA256, the exact private receipt directory selected in the
|
||||||
|
reviewed native proposal, and an original receipt only for recovery. No default
|
||||||
|
image/package/caller trust, founder identity, approval, route or source pin is
|
||||||
|
usable. The admission schema is closed in `validate_admission`; it includes the
|
||||||
|
immutable published producer image, retained caller, deployment projection,
|
||||||
|
selected consumer trust baseline, exact native plan/CCR/source bundle, fresh
|
||||||
|
UTC authority expiry (5..120seconds), founder entity and five distinct externally
|
||||||
|
reviewed owner dispositions. Each disposition binds the same approval-free
|
||||||
|
scope digest. An admission shape, local digest or review string never approves
|
||||||
|
itself: the founder and owners must independently review the exact supplied
|
||||||
|
command/file hash through existing governance before attended invocation.
|
||||||
|
|
||||||
|
The trust hash selects the independently reviewed consumer verifier/policy/trust
|
||||||
|
baseline for this custody action. It is not a claim that the newly generated
|
||||||
|
public key is already online: generation happens later, and independent delivery
|
||||||
|
and successor trust installation are required after the public receipt. The
|
||||||
|
writer never reads consumer private credentials or discovers trust from KV.
|
||||||
|
All successful custody states remain pending independent public admission.
|
||||||
|
|
||||||
|
`checked_sources` verifies the actual selected native action plan, exact proposed
|
||||||
|
kid, read CCR hash/status/binding, generated policy source, and source bundle.
|
||||||
|
The read CCR must independently be approved/binding-confirmed; the canonical
|
||||||
|
proposed CCR remains unapproved, so ordinary source checkout refuses. Source
|
||||||
|
bundle pins execution modules, not arbitrary request-selected code. The native
|
||||||
|
image/caller/seed projection is compared to the actual stopped Deployment.
|
||||||
|
The lifecycle policy digest/version stays unchanged; execute-ccr probes must
|
||||||
|
prove no ALLOW/effect. Current generic runtime guard can return HTTP503 before
|
||||||
|
the lifecycle evaluator, so signed unknown_action is not promised.
|
||||||
|
|
||||||
|
## Actual checks and effects
|
||||||
|
|
||||||
|
Before private work, the child checks OpenBao cluster ID, unsealed state, exact
|
||||||
|
non-root platform-admin OIDC entity, enabled audit, actual exact read-policy HCL
|
||||||
|
and role bindings/15m limits. The selected fixed loopback transport is the existing
|
||||||
|
owner-admin bridge; Kubernetes reads use fixed strict-host SSH to railiance01.
|
||||||
|
The OpenBao token lookup-self identifier stays only in the contained custody
|
||||||
|
process to authenticate fixed HTTP calls; it never reaches the agent, a receipt,
|
||||||
|
argv or another token file. This is a native protected-process implementation,
|
||||||
|
not evidence that agent runtime isolation has been promoted or granted.
|
||||||
|
|
||||||
|
The fence checks the pinned kube-system UID, stopped Deployment generation and
|
||||||
|
replica status, exact immutable image/caller/seed mount/kid projection, zero owned
|
||||||
|
ReplicaSet replicas, no live or terminating signer-SA/owned/seed-mount pods, and
|
||||||
|
no targeting HPA. It rechecks before metadata setup and immediately before CAS.
|
||||||
|
A private filesystem lock excludes a second local custody window. The lock and
|
||||||
|
observations do not provide atomic Kubernetes fencing: the independently reviewed
|
||||||
|
exclusive window must keep GitOps/Helm/other creators stopped. No rollout or
|
||||||
|
cluster mutation is performed by this adapter, and no concurrency guarantee is
|
||||||
|
inferred from source alone. Access/cluster owners retain that admission.
|
||||||
|
|
||||||
|
Bootstrap requires the exact OpenBao empty-errors HTTP404 metadata absence. A
|
||||||
|
durable private intent precedes exact CAS-required/max_versions2/no-auto-delete
|
||||||
|
metadata setup, followed by exact native readback. Only then does the shared
|
||||||
|
Ed25519 primitive generate one in-memory seed and CAS0 the fixed three-field
|
||||||
|
KV document. Independent version1 private readback and final metadata verification
|
||||||
|
precede the durable success receipt. The seed is never put in argv/env/local
|
||||||
|
files or output. The receipt carries public key, ASCII-base64url fingerprint,
|
||||||
|
version, bindings and closed phases only. The old fixture's live-dev-process gate
|
||||||
|
is preserved; its shared receipt initialization was extracted without weakening
|
||||||
|
that gate. Python cannot promise seed zeroization.
|
||||||
|
|
||||||
|
Every backend/body/close and tool call has a two-second absolute budget and
|
||||||
|
1MiB combined output bound. The complete operation is bounded by the supplied
|
||||||
|
fresh expiry. CLI validation and runtime failures remain silent. Existing0600/
|
||||||
|
0700 no-follow/ownership checks, exclusive creation, fsync/atomic replacement and
|
||||||
|
directory fsync retain evidence. Same-UID hostile code is not isolated by modes;
|
||||||
|
Warden's contained home, trusted runtime and independently reviewed custody
|
||||||
|
location are required, not inferred from approval frequency or shell prefixes.
|
||||||
|
|
||||||
|
## Process loss and independent read-only recovery
|
||||||
|
|
||||||
|
Any failed/ambiguous metadata/write/readback/final receipt outcome remains
|
||||||
|
unconfirmed. A success receipt is withheld until final metadata verification.
|
||||||
|
A child death immediately after the real native CAS leaves the earlier public
|
||||||
|
intent with its derived public key and no private value. A fresh `recover`
|
||||||
|
admission must bind that exact original receipt SHA and the same immutable scope.
|
||||||
|
It reserves a new durable recovery intent before private reads, verifies current
|
||||||
|
version1 metadata and exact private seed/public/kid correspondence, and records
|
||||||
|
separate public reconciliation. It never creates a writer, generates a seed,
|
||||||
|
POSTs metadata/data, restores a seed, consumes approval or dispatches business
|
||||||
|
operations. Original unknown evidence is never rewritten. Changed versions,
|
||||||
|
damaged/typed-invalid receipts, missing fresh reviews and denied reads refuse;
|
||||||
|
replaying a recovery ID cannot repeat even the private backend read.
|
||||||
|
|
||||||
|
This source implements bootstrap and read-only version1 reconciliation only.
|
||||||
|
Rotation needs a separately reviewed exact-version native action; the disposable
|
||||||
|
primitive's CAS1 test is not a native rotation grant. No automatic retry, remint,
|
||||||
|
rollback or general recovery API exists. Metadata-only setup uncertainty without
|
||||||
|
a public seed intent requires separate attended reconciliation, not this path.
|
||||||
|
|
||||||
|
Access observed zero PVCs in flex-auth. Its proposed new1Gi/local-path outbox,
|
||||||
|
PV Retain/reclaim disposition and runtime process-lock/capacity/startup bounds
|
||||||
|
remain separate reviews. This writer grants no PVC, audit egress, policy role,
|
||||||
|
SecretStore/Secret or producer/consumer deployment admission.
|
||||||
|
|
||||||
|
## R2 phase-aware recovery and source lineage
|
||||||
|
|
||||||
|
Frozen R1's metadata-only crash counterexample remains valid historical evidence.
|
||||||
|
R2 accepts a strict original metadata-setup receipt with no public key, or a
|
||||||
|
seed-cas receipt with a canonical derived public key. Fresh recovery reserves
|
||||||
|
durable evidence before target reads. Exact current metadata absence AND exact
|
||||||
|
version1 data absence yields `current-custody-absent`. Exact configured metadata
|
||||||
|
with current_version0 AND exact version1 data absence yields
|
||||||
|
`metadata-only-confirmed`. Both results have no public key/fingerprint/KV version,
|
||||||
|
key_custody_verified=false, historical_effect=unresolved, original_unknown_preserved,
|
||||||
|
and an explicit fresh independently reviewed action requirement. A locally
|
||||||
|
generated but unstored seed's public key is not promoted into public admission.
|
||||||
|
Neither observation means the original attempt historically made no effects.
|
||||||
|
|
||||||
|
Denied/malformed/misleading404 metadata or data, timeout, changed configuration,
|
||||||
|
unexpected versions, or version1 after a metadata-only/no-public phase keep the
|
||||||
|
new recovery attempt unconfirmed. They never grant generation, write, retry,
|
||||||
|
remint, consume or business authority. A subsequent metadata-only bootstrap is
|
||||||
|
not silently enabled: existing metadata still refuses CAS0 bootstrap admission.
|
||||||
|
A distinct exact reviewed action/source revision must choose any cleanup/reuse
|
||||||
|
or generation step. The original receipt remains byte-for-byte unchanged.
|
||||||
|
|
||||||
|
Native plan bootstrap.source_primitive_sha256 now equals the actual shared
|
||||||
|
primitive in the admitted source bundle and writer receipt; checked_sources
|
||||||
|
enforces the join. The prior8ed pin is explicitly historical. The plan pins the
|
||||||
|
shared primitive, not itself or the complete native executor, so no self-hash
|
||||||
|
cycle exists. The exact native executor is independently pinned by source_bundle
|
||||||
|
and its digest in bindings. A recover admission must additionally supply
|
||||||
|
original_execution (original receipt bindings, writer SHA and CCR SHA). Fresh
|
||||||
|
owner reviews bind both original and current lineages. This can describe an older
|
||||||
|
executed source/proposal without labeling it current. Image/deployment/policy/
|
||||||
|
consumer trust bindings must still match; recovery cannot change that authority.
|
||||||
|
|
||||||
|
## Exact120-second expiry and attended scheduling assessment
|
||||||
|
|
||||||
|
The unchanged validate_admission rule requires an absolute UTC expires_at with
|
||||||
|
5 < expires_at-now <=120seconds, and the entire child runs beneath that fixed
|
||||||
|
remaining absolute/monotonic budget. The five external reviews cover the scope
|
||||||
|
digest including expires_at. No deadline is generated, refreshed or widened by
|
||||||
|
the child; rechecking cannot extend it.
|
||||||
|
|
||||||
|
Existing ops-warden proxy.proxy_attended_login_exec runs the OIDC client before the child.
|
||||||
|
Its _contained_run captures output through subprocess.run without a timeout.
|
||||||
|
Therefore neither OIDC/MFA nor five external reviews have a demonstrated bound
|
||||||
|
that fits120seconds from preparing an admission. Creating a now+120seconds
|
||||||
|
admission before unbounded review/login is not a reliable operational ceremony.
|
||||||
|
Source success tests start from synthetic ready identities; they prove no human
|
||||||
|
OIDC/review timing SLO.
|
||||||
|
|
||||||
|
A coherent ceremony under the current rule is to pre-review one exact future
|
||||||
|
absolute expiry/window, including all artifact/source/image/trust/identity and
|
||||||
|
review references, then arrange attended login so the child starts within its
|
||||||
|
implicit [expires_at-120seconds, expires_at-5seconds) interval. Review may occur
|
||||||
|
before that interval; it does not change runtime authority. The founder must aim
|
||||||
|
for the beginning of the interval, not its last five seconds. The fixed path has
|
||||||
|
26 bounded native/tool requests before success (52seconds at their2second caps),
|
||||||
|
plus key/source/receipt work. Its expiry still wins if those operations cannot
|
||||||
|
finish; no completion or filesystem timing guarantee is inferred.
|
||||||
|
|
||||||
|
If login completes too early/late, a review changes any bytes, or adequate time
|
||||||
|
remains uncertain, refuse this revision. A new expires_at changes the approved
|
||||||
|
scope digest and needs explicit dispositions for the new revision; existing
|
||||||
|
source-only reviews cannot be copied into native approval. Do not silently
|
||||||
|
refresh after OIDC, reinterpret expiry as start-only, widen120seconds, skip
|
||||||
|
reviews or start a general window-grant service. An alternative authority/window
|
||||||
|
contract would itself require separate owner/founder design and is not supplied
|
||||||
|
by R2. Current native admission remains unapproved.
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
{
|
||||||
|
"schema": "railiance-platform.signer-native-action-proposal.v1",
|
||||||
|
"proposal_id": "RPF35-PDP-SIGNER-NATIVE-20261016-R1",
|
||||||
|
"status": "prepared-unapproved",
|
||||||
|
"read_ccr": "CCR-2026-0050",
|
||||||
|
"proposal_scope": "attended bootstrap of retained lifecycle PDP seed; no taskpilot enablement, rotation requires a new exact-version action revision",
|
||||||
|
"actor": "founder attended OIDC via warden openbao-platform-admin-login; reviewed silent Platform custody child",
|
||||||
|
"executor_ownership": "railiance-platform; founder executes exact child; agent receives closed metadata only",
|
||||||
|
"abort_owner": "founder; Platform preserves unknown custody/evidence, Access keeps signer/consumers fenced",
|
||||||
|
"target": {
|
||||||
|
"cluster": "primary/railiance01",
|
||||||
|
"kube_system_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786",
|
||||||
|
"openbao_cluster_id": "fd28df5d-98ec-57dd-42ec-9b3e4f4e53bf",
|
||||||
|
"openbao_origin": "http://127.0.0.1:18200",
|
||||||
|
"namespace": "flex-auth",
|
||||||
|
"deployment": "flex-auth-secrets-engine",
|
||||||
|
"signer_sa": "flex-auth-secrets-engine"
|
||||||
|
},
|
||||||
|
"credential_route": "openbao-platform-admin-login; flex-auth-decision-envelope-signing-key remains draft/nonresolvable; route is not a seed vending service",
|
||||||
|
"bootstrap": {
|
||||||
|
"action": "CAS0 fresh in-memory Ed25519 seed",
|
||||||
|
"kid": "flex-auth-lifecycle-20261016-r1",
|
||||||
|
"expected_version": 0,
|
||||||
|
"data_path": "platform/data/workloads/flex-auth/envelope-signing",
|
||||||
|
"document_fields": [
|
||||||
|
"ED25519_PRIVATE_KEY",
|
||||||
|
"ED25519_PUBLIC_KEY",
|
||||||
|
"kid"
|
||||||
|
],
|
||||||
|
"metadata_path": "platform/metadata/workloads/flex-auth/envelope-signing",
|
||||||
|
"metadata_contract": {
|
||||||
|
"cas_required": true,
|
||||||
|
"max_versions": 2,
|
||||||
|
"delete_version_after": "0s"
|
||||||
|
},
|
||||||
|
"allowed_native_methods": [
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "platform/metadata/workloads/flex-auth/envelope-signing",
|
||||||
|
"purpose": "CAS-required/version-retention configuration after absence/no-drift validation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "platform/metadata/workloads/flex-auth/envelope-signing",
|
||||||
|
"purpose": "exact metadata readback, no private value output"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "platform/data/workloads/flex-auth/envelope-signing",
|
||||||
|
"purpose": "single CAS0 seed document write"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "platform/data/workloads/flex-auth/envelope-signing?version=1",
|
||||||
|
"purpose": "exact private readback in custody, closed public receipt only"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source_primitive_sha256": "sha256:f169ad12f90d729b647085c3bed2c0d8d005db23a2341b6d3d5f1793bcfd110c",
|
||||||
|
"historical_source_primitive_sha256": "sha256:8ed281a49c1f1cdb23772efe3593e75c62572a12b8d7a6a4c574f27b38722e75",
|
||||||
|
"source_lineage_rule": "source_primitive_sha256 must equal current admitted source_bundle[scripts/flex_auth_signer_custody.py] and exact writer receipt; historical_source_primitive_sha256 is non-authoritative R1 evidence only"
|
||||||
|
},
|
||||||
|
"fencing": {
|
||||||
|
"before_key_generation": [
|
||||||
|
"Verify cluster/backend IDs, exact current source/CCR and founder disposition hashes.",
|
||||||
|
"Read exact deployment/ReplicaSets/pods by ownerReference and SA; refuse a second workload/pod using signer SA or selecting signer Secret.",
|
||||||
|
"Access stops protected consumers and scales retained deployment0 through its reviewed release path; this child does not mutate replicas.",
|
||||||
|
"Verify deployment spec replicas0, observed generation caught up, no live or terminating owned ReplicaSet/pods, no matching signer-SA pods and no HPA targeting deployment.",
|
||||||
|
"Reject a differing/previously existing KV entry at bootstrap. Hold exclusive owner action receipt lock and recheck fence immediately before CAS."
|
||||||
|
],
|
||||||
|
"after_cas": "Keep consumers/signer stopped until independent public admission and exact ESO/readback/source/image verification. Recreate1 is Access-owned; any drift or uncertain outcome keeps fence closed. Access reports current outbox lacks OS process lock, byte ceiling and bounded startup/compaction; Recreate1 alone is not bounded durable acceptance. Keep protected acceptance closed until reviewed source hardening or explicit bounded attended fixture disposition.",
|
||||||
|
"concurrency": "Single attended owner window/exclusive receipt; GitOps/Helm autosync/other writers must be paused by Access/cluster owner under distinct reviewed action. Plan is not a Kubernetes atomic fence or same-UID isolation."
|
||||||
|
},
|
||||||
|
"protected_adapter": {
|
||||||
|
"state": "implementation/independent tests still required before executable approval; fixture code must never be promoted by removing its dev-process check",
|
||||||
|
"reuse": [
|
||||||
|
"scripts/state_hub_preflight_lane.py attended silent child/cluster/platform-admin/fencing patterns",
|
||||||
|
"scripts/flex_auth_signer_custody.py protected Ed25519 CAS/readback and receipt state machine",
|
||||||
|
"scripts/task_execution_native_fixture.py absolute main-thread2s I/O budget",
|
||||||
|
"scripts/task_execution_recovery.py strict bounded JSON/no-follow/private custody patterns"
|
||||||
|
],
|
||||||
|
"requirements": [
|
||||||
|
"New fixed native adapter consumes exact approved CCR/source/native-action digest; cannot accept arbitrary URL/path/command/payload/seed.",
|
||||||
|
"Read OIDC token only within warden child from attended context; require platform-admin and reject root; never print/store bearer. No credential comes back to agent.",
|
||||||
|
"Pin exact kube-system/OpenBao cluster IDs before private calls, refuse ambient address override; enforce2s absolute reads and1MiB cap; no redirect/shell/tracing.",
|
||||||
|
"Verify metadata absence/exact admitted values before CAS-required change; any differing existing object refuses. Exact response contracts and duplicate/nonfinite JSON rejection.",
|
||||||
|
"Private seed is generated once, never imported or persisted outside OpenBao; no subprocess argv/env seed; readback compares exact version/fields and derived public correspondence.",
|
||||||
|
"Receipt intent is durable before native effect; final success requires exact KV/metadata correspondence and receipt fsync. Process loss/error remains unknown without remint/retry."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"receipts": {
|
||||||
|
"directory": "/home/worsch/.local/state/railiance-platform/flex-auth-envelope-signing",
|
||||||
|
"mode_directory": "0700",
|
||||||
|
"mode_file": "0600",
|
||||||
|
"native_execution_location": "founder workstation inside reviewed warden child; no agent-owned shared /tmp custody",
|
||||||
|
"basename": "RPF35-PDP-SIGNER-NATIVE-20261016-R1.json",
|
||||||
|
"maximum_bytes": 65536,
|
||||||
|
"atomic": "exclusive initial receipt, fsync-before-effect, atomic replacement and directory fsync, no-follow/owner/mode/nlink validation",
|
||||||
|
"public_fields": [
|
||||||
|
"proposal/executed source/CCR/identity/cluster/image/policy/trust digests",
|
||||||
|
"actor/time",
|
||||||
|
"closed phase/status",
|
||||||
|
"exact KV path/version/kid",
|
||||||
|
"public key and explicitly encoded fingerprint",
|
||||||
|
"fence/metadata/readback checks",
|
||||||
|
"rollback or unknown recovery disposition"
|
||||||
|
],
|
||||||
|
"fingerprint": "sha256: lowercase SHA256 of canonical unpadded base64url public-key ASCII, exactly43bytes; independent Secrets owner agreement before admission",
|
||||||
|
"delivery": "Founder/Platform separately authenticate metadata receipt to Secrets owner; public receipt shape/file does not create consumer trust; raw private receipt directory is not a shared agent intake"
|
||||||
|
},
|
||||||
|
"rollback": {
|
||||||
|
"before_cas": "Abort without private write; retain receipt/fence; restore prior Access image/values only via reviewed Access action.",
|
||||||
|
"after_cas_unknown": "Retain unknown and fence; independently attended owner reconciles exact metadata/version/public correspondence. Never rerun CAS0 or restore prior seed automatically.",
|
||||||
|
"compromise": "Remove online old public key immediately; stop signer, independent new reviewed forward-only action; old private seed never restored."
|
||||||
|
},
|
||||||
|
"requested_scope": {
|
||||||
|
"read_lane": "Exact CCR0050 policy/role/delivery source only after separate founder review. No extra ESO RBAC permitted.",
|
||||||
|
"custody_writer": "One attended exact-target CAS0 plus fixed KV metadata setup/readback; no standing writer grant or broker role. This authority is separate from read CCR.",
|
||||||
|
"kubernetes": "Metadata-only fence observations by child; Access/cluster separately approve stop/restart/delivery manifests and sync control.",
|
||||||
|
"new_infrastructure_spend_eur": 0,
|
||||||
|
"new_resources": "Platform creates no storage resources. Access reports zero flex-auth PVCs; Access proposes separate exact new flex-auth-secrets-engine-outbox1Gi local-path Helmkeep. Provisioning, PV reclaim/Delete/WaitForFirstConsumer retention and runtime log bounds require separate founder/cluster/Access review; no durable outbox acceptance yet.",
|
||||||
|
"risk_bound": "Controlled loss of retained PDP availability during explicit attended fence; seed compromise can forge lifecycle decisions until independent public trust withdrawal; founder must approve this concrete high-risk scope."
|
||||||
|
},
|
||||||
|
"reviews_required": [
|
||||||
|
{
|
||||||
|
"role": "railiance-platform-owner",
|
||||||
|
"disposition": "pending exact source/artifact review; no approval recorded"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "access-engine-owner",
|
||||||
|
"disposition": "pending exact source/artifact review; no approval recorded"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "secrets-engine-owner",
|
||||||
|
"disposition": "pending exact source/artifact review; no approval recorded"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "cluster-gitops-owner",
|
||||||
|
"disposition": "pending exact source/artifact review; no approval recorded"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "founder",
|
||||||
|
"disposition": "pending exact source/artifact review; no approval recorded"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"authority_granted": false,
|
||||||
|
"candidate_delivery_amendments": {
|
||||||
|
"supersedes_unadmitted_R3_role": "external-secrets-flex-auth-envelope-signing replaces flex-auth-envelope-signing-eso to fit existing delegated-applier role convention; dedicated SA unchanged",
|
||||||
|
"read_ccr_field_projection": "Only ED25519_PRIVATE_KEY is delivered; public sibling fields remain visible to dedicated document-reader identity but never become consumer trust discovery",
|
||||||
|
"projection_source": "docs/credential-lane-designs/flex-auth-envelope-signing-native-projection.yaml.proposed",
|
||||||
|
"managed_declaration": "Missing rapp declaration remains migration debt; retain actual Access919ce25 workload identity, do not create package tuple"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Native signer action preparation
|
||||||
|
|
||||||
|
CCR-2026-0050 is a schema-valid proposed ESO read grant. It does not authorize
|
||||||
|
secret writes. The separate exact native-action JSON requests one attended CAS0
|
||||||
|
and fixed metadata setup/readback; founder disposition is still pending. No
|
||||||
|
standing writer/broker grant, private consumer lookup, rapp declaration or policy
|
||||||
|
change is introduced. Native private custody remains in the founder's reviewed
|
||||||
|
warden child, not an agent prompt or temporary file.
|
||||||
|
|
||||||
|
The JSON fixes the target/cluster/backend, new kid, CAS version, exact native
|
||||||
|
methods, retention/CAS metadata, existing custody machinery, signer fence,
|
||||||
|
receipt location/encoding, abort owner and forward-only recovery. It separately
|
||||||
|
identifies Access/cluster-owned stop/restart/ESO/PVC and Secrets-owned independent
|
||||||
|
public admission. These owners must review their exact inputs; Platform cannot
|
||||||
|
sign their dispositions. The program that enforces this native contract still
|
||||||
|
requires source implementation and independent tests before executable approval.
|
||||||
|
The existing fixture adapter must not be promoted by deleting its process check.
|
||||||
|
|
||||||
|
Source preparation validated the real credential-change schema and generated
|
||||||
|
HCL. The expected unconfirmed-bound-claims warning and proposed status preserve
|
||||||
|
apply refusal. No live observation/identity confirmation, credential read, CAS,
|
||||||
|
policy write, role, Kubernetes or route mutation occurred. Missing managed
|
||||||
|
package declaration remains explicit migration debt for the retained workload.
|
||||||
|
|
||||||
|
The native candidate explicitly amends two unadmitted R3 read-lane choices:
|
||||||
|
role `external-secrets-flex-auth-envelope-signing` follows the existing delegated
|
||||||
|
applier convention; read CCR fields names the projected ED25519_PRIVATE_KEY only.
|
||||||
|
The same document has public sibling fields; document-granular read means ESO
|
||||||
|
can read those too, so this is not field-level authorization. The sibling native
|
||||||
|
projection proposal and CCR agree on the new role. Historical R3 is not mutated.
|
||||||
|
|
||||||
|
Access metadata observation found zero PVCs in flex-auth. Its exact proposed
|
||||||
|
new outbox PVC1Gi/local-path is a separate resource/retention scope, with Delete/
|
||||||
|
WaitForFirstConsumer PV behavior needing explicit treatment. Existing outbox code
|
||||||
|
lacks process locking/byte ceiling/bounded startup and compaction. Neither
|
||||||
|
Recreate1 nor Helmkeep supplies these guarantees; production durable acceptance
|
||||||
|
remains closed until Access-owned hardening and cluster/storage review.
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
# SOURCE PROPOSAL ONLY: no CCR admission, no apply, no plaintext key.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: flex-auth-envelope-signing-eso
|
||||||
|
namespace: flex-auth
|
||||||
|
automountServiceAccountToken: false
|
||||||
|
---
|
||||||
|
apiVersion: external-secrets.io/v1
|
||||||
|
kind: SecretStore
|
||||||
|
metadata:
|
||||||
|
name: openbao-flex-auth-envelope-signing
|
||||||
|
namespace: flex-auth
|
||||||
|
spec:
|
||||||
|
provider:
|
||||||
|
vault:
|
||||||
|
server: http://openbao.openbao.svc:8200
|
||||||
|
path: platform
|
||||||
|
version: v2
|
||||||
|
auth:
|
||||||
|
kubernetes:
|
||||||
|
mountPath: kubernetes
|
||||||
|
role: external-secrets-flex-auth-envelope-signing
|
||||||
|
serviceAccountRef:
|
||||||
|
name: flex-auth-envelope-signing-eso
|
||||||
|
audiences: [openbao]
|
||||||
|
---
|
||||||
|
apiVersion: external-secrets.io/v1
|
||||||
|
kind: ExternalSecret
|
||||||
|
metadata:
|
||||||
|
name: flex-auth-secrets-engine-envelope-signing
|
||||||
|
namespace: flex-auth
|
||||||
|
spec:
|
||||||
|
refreshInterval: 5m
|
||||||
|
secretStoreRef:
|
||||||
|
kind: SecretStore
|
||||||
|
name: openbao-flex-auth-envelope-signing
|
||||||
|
target:
|
||||||
|
name: flex-auth-secrets-engine-envelope-signing
|
||||||
|
creationPolicy: Owner
|
||||||
|
deletionPolicy: Retain
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
railiance.io/custody-proposal: RPF35-PDP-SIGNER-NATIVE-20261016-R1
|
||||||
|
data:
|
||||||
|
- secretKey: seed
|
||||||
|
remoteRef:
|
||||||
|
key: workloads/flex-auth/envelope-signing
|
||||||
|
property: ED25519_PRIVATE_KEY
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
path "platform/data/workloads/flex-auth/envelope-signing" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "auth/token/lookup-self" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "auth/token/revoke-self" {
|
||||||
|
capabilities = ["update"]
|
||||||
|
}
|
||||||
|
|
@ -58,6 +58,10 @@ class SignerWriter:
|
||||||
or '-dev' not in process.args or '-dev-no-store-token' not in process.args
|
or '-dev' not in process.args or '-dev-no-store-token' not in process.args
|
||||||
or '-dev-listen-address=127.0.0.1:'+str(admission.port) not in process.args):
|
or '-dev-listen-address=127.0.0.1:'+str(admission.port) not in process.args):
|
||||||
raise Refused('live owner-spawned disposable dev process required')
|
raise Refused('live owner-spawned disposable dev process required')
|
||||||
|
self.admission=admission
|
||||||
|
self._initialize_receipt(directory,operation_id,kid=kid,expected_version=expected_version,bindings=bindings)
|
||||||
|
|
||||||
|
def _initialize_receipt(self,directory,operation_id,*,kid,expected_version,bindings):
|
||||||
if (type(operation_id) is not str or not re.fullmatch(r'[a-zA-Z0-9_-]{1,80}',operation_id)
|
if (type(operation_id) is not str or not re.fullmatch(r'[a-zA-Z0-9_-]{1,80}',operation_id)
|
||||||
or type(kid) is not str or not re.fullmatch(r'[a-zA-Z0-9_-]{1,80}',kid)
|
or type(kid) is not str or not re.fullmatch(r'[a-zA-Z0-9_-]{1,80}',kid)
|
||||||
or type(expected_version) is not int or not 0<=expected_version<2**31
|
or type(expected_version) is not int or not 0<=expected_version<2**31
|
||||||
|
|
@ -66,7 +70,7 @@ class SignerWriter:
|
||||||
raise Refused('exact signer operation contract required')
|
raise Refused('exact signer operation contract required')
|
||||||
if directory.absolute()!=directory.resolve() or directory.is_symlink():raise Refused('indirect receipt custody refused')
|
if directory.absolute()!=directory.resolve() or directory.is_symlink():raise Refused('indirect receipt custody refused')
|
||||||
Journal._private(directory.stat(),directory=True)
|
Journal._private(directory.stat(),directory=True)
|
||||||
self.admission=admission;self.directory=directory;self.path=directory/(operation_id+'.json')
|
self.directory=directory;self.path=directory/(operation_id+'.json')
|
||||||
self.expected_version=expected_version;self.kid=kid;self.attempted=False
|
self.expected_version=expected_version;self.kid=kid;self.attempted=False
|
||||||
self.receipt={'schema':'railiance-platform.signer-cas-receipt.v1','operation_id':operation_id,
|
self.receipt={'schema':'railiance-platform.signer-cas-receipt.v1','operation_id':operation_id,
|
||||||
'bindings':dict(bindings),'writer_sha256':'sha256:'+hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
'bindings':dict(bindings),'writer_sha256':'sha256:'+hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
||||||
|
|
|
||||||
417
scripts/flex_auth_signer_native.py
Normal file
417
scripts/flex_auth_signer_native.py
Normal file
|
|
@ -0,0 +1,417 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Silent attended retained-lifecycle signer custody; never a credential front door.
|
||||||
|
|
||||||
|
Invoke only through the reviewed warden platform-admin envelope. The externally
|
||||||
|
reviewed admission SHA is an exact-command input, not approval by JSON shape.
|
||||||
|
No private value crosses stdout/stderr, argv, receipt or persistent local storage.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
from datetime import datetime,timezone
|
||||||
|
import fcntl
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import http.client
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import selectors
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||||
|
from flex_auth_signer_custody import SignerWriter,PATH,FIELDS,BINDINGS,encoded,b64
|
||||||
|
from task_execution_journal import Journal,Refused
|
||||||
|
from task_execution_native_fixture import _absolute_budget
|
||||||
|
from task_execution_recovery import _json_unique,_invalid_constant,_policy_absent,_same_policy
|
||||||
|
|
||||||
|
ROOT=Path(__file__).resolve().parents[1]
|
||||||
|
META='platform/metadata/workloads/flex-auth/envelope-signing'
|
||||||
|
CCR='credential-change-requests/CCR-2026-0050-flex-auth-envelope-signing.yaml'
|
||||||
|
PLAN='docs/credential-lane-designs/flex-auth-envelope-signing-native-plan.json'
|
||||||
|
CLUSTER='a553c742-0115-43d4-99a4-a5ca56fe0786'
|
||||||
|
BAO_CLUSTER='fd28df5d-98ec-57dd-42ec-9b3e4f4e53bf'
|
||||||
|
NAME='flex-auth-secrets-engine'
|
||||||
|
SECRET='flex-auth-secrets-engine-envelope-signing'
|
||||||
|
ROLE='external-secrets-flex-auth-envelope-signing'
|
||||||
|
LIFECYCLE='sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4'
|
||||||
|
REVIEWS={'founder','railiance-platform-owner','access-engine-owner','secrets-engine-owner','cluster-gitops-owner'}
|
||||||
|
SOURCE_FILES={'scripts/flex_auth_signer_native.py','scripts/flex_auth_signer_custody.py','scripts/task_execution_native_fixture.py','scripts/task_execution_journal.py','scripts/task_execution_recovery.py','scripts/credential-change.py'}
|
||||||
|
|
||||||
|
|
||||||
|
def require(ok,reason):
|
||||||
|
if not ok:raise Refused(reason)
|
||||||
|
|
||||||
|
|
||||||
|
def digest(raw):return 'sha256:'+hashlib.sha256(raw).hexdigest()
|
||||||
|
def is_digest(value):return type(value) is str and re.fullmatch(r'sha256:[0-9a-f]{64}',value) is not None
|
||||||
|
|
||||||
|
def private_read(path,limit=65536):
|
||||||
|
path=Path(path)
|
||||||
|
require(path.is_absolute() and path.absolute()==path.resolve() and not path.is_symlink(),'indirect private input')
|
||||||
|
Journal._private(path.parent.stat(),directory=True)
|
||||||
|
fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK)
|
||||||
|
try:
|
||||||
|
before=os.fstat(fd);Journal._private(before)
|
||||||
|
raw=os.read(fd,limit+1);after=os.fstat(fd)
|
||||||
|
require((before.st_dev,before.st_ino,before.st_size,before.st_mtime_ns,before.st_ctime_ns)==
|
||||||
|
(after.st_dev,after.st_ino,after.st_size,after.st_mtime_ns,after.st_ctime_ns)
|
||||||
|
and len(raw)==before.st_size and len(raw)<=limit,'private input changed or oversized')
|
||||||
|
return raw
|
||||||
|
finally:os.close(fd)
|
||||||
|
|
||||||
|
|
||||||
|
def parsed(raw):
|
||||||
|
try:return json.loads(raw,object_pairs_hook=_json_unique,parse_constant=_invalid_constant)
|
||||||
|
except Exception:raise Refused('invalid closed JSON input') from None
|
||||||
|
|
||||||
|
|
||||||
|
def validate_admission(value):
|
||||||
|
fields={'schema','action','operation_id','kid','expires_at','founder_entity_id','ccr_sha256','source_bundle','bindings',
|
||||||
|
'producer_image','caller_binding','deployment_projection_sha256','authority','routing','owner_reviews',
|
||||||
|
'exclusive_window_sha256','consumer_trust_sha256','original_receipt_sha256','original_execution'}
|
||||||
|
require(type(value) is dict and set(value)==fields,'exact admission schema required')
|
||||||
|
require(value['schema']=='railiance-platform.signer-native-admission.v1' and value['action'] in {'bootstrap','recover'},'native admission action refused')
|
||||||
|
require(type(value['operation_id']) is str and re.fullmatch(r'[A-Za-z0-9_-]{1,80}',value['operation_id']) and
|
||||||
|
type(value['kid']) is str and re.fullmatch(r'[A-Za-z0-9_-]{1,80}',value['kid']),'native operation identity required')
|
||||||
|
require(type(value['founder_entity_id']) is str and re.fullmatch(r'[a-f0-9-]{36}',value['founder_entity_id']),'explicit attended identity required')
|
||||||
|
require(all(is_digest(value[x]) for x in ['ccr_sha256','deployment_projection_sha256','exclusive_window_sha256','consumer_trust_sha256']),'explicit custody/image/trust selections required')
|
||||||
|
require(type(value['producer_image']) is str and re.fullmatch(r'forgejo\.coulomb\.social/coulomb/flex-auth@sha256:[a-f0-9]{64}',value['producer_image']),'immutable producer image required')
|
||||||
|
require(value['caller_binding']=='secrets-engine=system:serviceaccount:secrets-engine:secrets-engine','retained caller binding required')
|
||||||
|
require(value['authority']=='founder-attended-exact-source-scope; no standing grant','independent attended authority required')
|
||||||
|
route=value['routing'];require(type(route) is dict and set(route)=={'lane_id','verdict','founder_act','receipt_sha256'} and
|
||||||
|
route['lane_id']=='openbao-platform-admin-login' and route['verdict']=='founder_required' and route['founder_act']=='oidc_login' and is_digest(route['receipt_sha256']),'routing mismatch refused')
|
||||||
|
reviews=value['owner_reviews'];require(type(reviews) is dict and set(reviews)==REVIEWS,'distinct owner reviews required')
|
||||||
|
for role,review in reviews.items():
|
||||||
|
require(type(review) is dict and set(review)=={'approved_admission_sha256','approval_ref'} and
|
||||||
|
is_digest(review['approved_admission_sha256']) and type(review['approval_ref']) is str and
|
||||||
|
0<len(review['approval_ref'])<=200,'exact external review required')
|
||||||
|
# Dispositions bind a claim-free scope digest, avoiding self-referential JSON.
|
||||||
|
scope={k:v for k,v in value.items() if k!='owner_reviews'};scope_digest=digest(encoded(scope))
|
||||||
|
require(all(x['approved_admission_sha256']==scope_digest for x in reviews.values()),'owner review scope mismatch')
|
||||||
|
require(type(value['source_bundle']) is dict and set(value['source_bundle'])==SOURCE_FILES and
|
||||||
|
all(is_digest(x) for x in value['source_bundle'].values()),'exact execution source bundle required')
|
||||||
|
require(type(value['bindings']) is dict and set(value['bindings'])==BINDINGS and all(is_digest(x) for x in value['bindings'].values()),'exact execution bindings required')
|
||||||
|
require(value['bindings']['policy_sha256']==LIFECYCLE and value['bindings']['image_sha256']=='sha256:'+value['producer_image'].rsplit('sha256:',1)[1]
|
||||||
|
and value['bindings']['trust_sha256']==value['consumer_trust_sha256']
|
||||||
|
and value['bindings']['deployment_sha256']==value['deployment_projection_sha256']
|
||||||
|
and value['bindings']['executed_revision_sha256']==digest(encoded(value['source_bundle'])),'immutable lifecycle/image/trust/source mismatch')
|
||||||
|
require((value['action']=='bootstrap' and value['original_receipt_sha256'] is None) or
|
||||||
|
(value['action']=='recover' and is_digest(value['original_receipt_sha256'])),'separate recovery disposition required')
|
||||||
|
original=value['original_execution']
|
||||||
|
if value['action']=='bootstrap':require(original is None,'bootstrap cannot import execution lineage')
|
||||||
|
else:
|
||||||
|
require(type(original) is dict and set(original)=={'bindings','writer_sha256','source_ccr_sha256'} and
|
||||||
|
is_digest(original['writer_sha256']) and is_digest(original['source_ccr_sha256']) and
|
||||||
|
type(original['bindings']) is dict and set(original['bindings'])==BINDINGS and
|
||||||
|
all(is_digest(x) for x in original['bindings'].values()),'independently reviewed original execution lineage required')
|
||||||
|
require(all(original['bindings'][key]==value['bindings'][key] for key in BINDINGS-{'proposal_sha256','executed_revision_sha256'}),
|
||||||
|
'recovery cannot change original image/deployment/policy/trust authority')
|
||||||
|
try:expires=datetime.fromisoformat(value['expires_at'].replace('Z','+00:00'));remaining=(expires-datetime.now(timezone.utc)).total_seconds()
|
||||||
|
except Exception:raise Refused('invalid authority expiry') from None
|
||||||
|
require(expires.utcoffset().total_seconds()==0 and 5<remaining<=120,'bounded fresh attended window required')
|
||||||
|
return remaining
|
||||||
|
|
||||||
|
|
||||||
|
class NativeIO:
|
||||||
|
"""All tools capture/discard private bytes; fixed transport and no shell."""
|
||||||
|
def captured(self,argv,payload=None,*,allowed=(0,)):
|
||||||
|
env=os.environ.copy();env.update(BAO_ADDR='http://127.0.0.1:18200',VAULT_ADDR='http://127.0.0.1:18200')
|
||||||
|
for key in ('BAO_LOG_LEVEL','VAULT_LOG_LEVEL'):
|
||||||
|
require(env.get(key,'').lower() not in {'debug','trace'},'verbose custody environment refused')
|
||||||
|
# A hostile inherited CLI config is not an alternate identity/endpoint.
|
||||||
|
require(not env.get('BAO_NAMESPACE') and not env.get('VAULT_NAMESPACE') and not env.get('VAULT_SKIP_VERIFY'),'ambient backend override refused')
|
||||||
|
proc=subprocess.Popen(argv,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,env=env)
|
||||||
|
try:
|
||||||
|
if payload is not None:proc.stdin.write(encoded(payload));proc.stdin.flush()
|
||||||
|
proc.stdin.close()
|
||||||
|
sel=selectors.DefaultSelector();data={proc.stdout:bytearray(),proc.stderr:bytearray()}
|
||||||
|
try:
|
||||||
|
for stream in data:sel.register(stream,selectors.EVENT_READ)
|
||||||
|
deadline=time.monotonic()+2
|
||||||
|
while sel.get_map():
|
||||||
|
remaining=deadline-time.monotonic();require(remaining>0,'absolute custody command timeout')
|
||||||
|
for item,_ in sel.select(remaining):
|
||||||
|
chunk=os.read(item.fileobj.fileno(),65536)
|
||||||
|
if not chunk:sel.unregister(item.fileobj)
|
||||||
|
else:
|
||||||
|
data[item.fileobj].extend(chunk)
|
||||||
|
require(sum(map(len,data.values()))<=1048576,'custody command output bound')
|
||||||
|
code=proc.wait(timeout=max(.001,deadline-time.monotonic()))
|
||||||
|
require(code in allowed,'custody command unconfirmed')
|
||||||
|
return code,bytes(data[proc.stdout])
|
||||||
|
finally:sel.close()
|
||||||
|
except BaseException:
|
||||||
|
proc.kill();proc.wait(timeout=2);raise Refused('custody command unconfirmed') from None
|
||||||
|
finally:
|
||||||
|
proc.stdout.close();proc.stderr.close()
|
||||||
|
def bao(self,args,payload=None,allowed=(0,)):
|
||||||
|
with _absolute_budget(2):
|
||||||
|
code,raw=self.captured(['bao',*args],payload,allowed=allowed)
|
||||||
|
return code,parsed(raw)
|
||||||
|
def kube(self,resource,name=None):
|
||||||
|
args=['kubectl','--request-timeout=2s']
|
||||||
|
if resource!='namespace':args+=['-n','flex-auth']
|
||||||
|
args+=['get',resource]
|
||||||
|
if name:args.append(name)
|
||||||
|
args+=['-o','json']
|
||||||
|
with _absolute_budget(2):
|
||||||
|
_,raw=self.captured(['ssh','-o','BatchMode=yes','-o','StrictHostKeyChecking=yes','-o','ConnectTimeout=2','railiance01',shlex.join(args)])
|
||||||
|
return parsed(raw)
|
||||||
|
def http(self,method,path,body=None):
|
||||||
|
require(method in {'GET','POST'} and path in {META,PATH,PATH+'?version=1'},'fixed custody path required')
|
||||||
|
require(type(getattr(self,'_token',None)) is str and self._token,'contained attended session required')
|
||||||
|
with _absolute_budget(2):
|
||||||
|
conn=http.client.HTTPConnection('127.0.0.1',18200,timeout=2)
|
||||||
|
try:
|
||||||
|
conn.request(method,'/v1/'+path,body=None if body is None else encoded(body),
|
||||||
|
headers={'X-Vault-Token':self._token,'Content-Type':'application/json'})
|
||||||
|
response=conn.getresponse();raw=response.read(1048577)
|
||||||
|
require(len(raw)<=1048576,'custody response bound')
|
||||||
|
return response.status,parsed(raw) if raw else {}
|
||||||
|
finally:conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def deployment_projection(deployment):
|
||||||
|
spec=deployment['spec']['template']['spec'];containers=spec.get('containers',[])
|
||||||
|
require(type(containers) is list and len(containers)==1 and not spec.get('initContainers'),'single fixed signer container required')
|
||||||
|
item=containers[0]
|
||||||
|
# Never hash arbitrary env values or an entire Secret/spec response.
|
||||||
|
return {'service_account':spec.get('serviceAccountName'),'image':item.get('image'),
|
||||||
|
'args':item.get('args'),'volume_mounts':item.get('volumeMounts',[]),
|
||||||
|
'volumes':spec.get('volumes',[]),'security_context':spec.get('securityContext')}
|
||||||
|
|
||||||
|
|
||||||
|
def fence(io,contract):
|
||||||
|
namespace=io.kube('namespace','kube-system')
|
||||||
|
require(namespace.get('metadata',{}).get('uid')==CLUSTER,'wrong target cluster')
|
||||||
|
deployment=io.kube('deployment',NAME);spec=deployment.get('spec',{});meta=deployment.get('metadata',{})
|
||||||
|
require(type(spec.get('replicas')) is int and spec['replicas']==0 and
|
||||||
|
deployment.get('status',{}).get('observedGeneration',-1)==meta.get('generation') and
|
||||||
|
not deployment.get('status',{}).get('replicas',0),'all signer replicas must be stopped')
|
||||||
|
projection=deployment_projection(deployment)
|
||||||
|
require(digest(encoded(projection))==contract['deployment_projection_sha256'] and
|
||||||
|
projection['service_account']==NAME and projection['image']==contract['producer_image'],'fenced deployment source/image mismatch')
|
||||||
|
args=projection['args'];require(type(args) is list and all(type(x) is str for x in args),'exact signer args required')
|
||||||
|
def option(flag):
|
||||||
|
require(args.count(flag)==1 and args.index(flag)+1<len(args),'fixed signer flag required')
|
||||||
|
return args[args.index(flag)+1]
|
||||||
|
require(option('--caller-auth-mode')=='enforce' and option('--caller-audience')=='flex-auth' and
|
||||||
|
option('--caller-binding')==contract['caller_binding'],'retained caller guard required')
|
||||||
|
require(not any(x.startswith(flag+'=') for x in args for flag in ['--caller-auth-mode','--caller-binding','--caller-audience','--envelope-signing-key-file','--envelope-signing-kid']),'ambiguous protected args refused')
|
||||||
|
require(option('--envelope-signing-key-file')=='/var/run/secrets/decision-envelope-signing/seed' and
|
||||||
|
option('--envelope-signing-kid')==contract['kid'],'exact seed mount and startup kid required')
|
||||||
|
volumes=projection['volumes'];mounts=projection['volume_mounts']
|
||||||
|
matching=[x for x in volumes if x.get('name')=='flex-auth-envelope-signing']
|
||||||
|
require(len(matching)==1 and matching[0].get('secret')=={'secretName':SECRET,'defaultMode':288,'items':[{'key':'seed','path':'seed'}]},'exact seed volume required')
|
||||||
|
require(any(x=={'name':'flex-auth-envelope-signing','mountPath':'/var/run/secrets/decision-envelope-signing','readOnly':True} for x in mounts),'readonly seed mount required')
|
||||||
|
uid=meta.get('uid');require(type(uid) is str and uid,'deployment UID required')
|
||||||
|
sets=io.kube('replicasets').get('items');pods=io.kube('pods').get('items');hpas=io.kube('hpa').get('items')
|
||||||
|
require(type(sets) is list and type(pods) is list and type(hpas) is list,'complete fence observations required')
|
||||||
|
rsids=[]
|
||||||
|
for item in sets:
|
||||||
|
if any(x.get('uid')==uid for x in item.get('metadata',{}).get('ownerReferences',[])):
|
||||||
|
require(type(item.get('spec',{}).get('replicas')) is int and item['spec']['replicas']==0 and
|
||||||
|
type(item.get('status',{}).get('replicas',0)) is int and not item.get('status',{}).get('replicas',0),'owned ReplicaSet still running')
|
||||||
|
rsids.append(item['metadata']['uid'])
|
||||||
|
for item in pods:
|
||||||
|
podspec=item.get('spec',{})
|
||||||
|
uses_secret=any(x.get('secret',{}).get('secretName')==SECRET for x in podspec.get('volumes',[]))
|
||||||
|
owned=any(x.get('uid') in [uid,*rsids] for x in item.get('metadata',{}).get('ownerReferences',[]))
|
||||||
|
require(not owned and podspec.get('serviceAccountName')!=NAME and not uses_secret,'live or terminating signer pod refused')
|
||||||
|
require(not any(x.get('spec',{}).get('scaleTargetRef',{}).get('name')==NAME for x in hpas),'signer autoscaler refused')
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def checked_sources(contract):
|
||||||
|
for name,pin in contract['source_bundle'].items():
|
||||||
|
path=ROOT/name
|
||||||
|
require(path.resolve()==path.absolute() and not path.is_symlink() and path.stat().st_mode&0o022==0
|
||||||
|
and path.is_file() and path.stat().st_size<=1048576 and digest(path.read_bytes())==pin,'exact admitted execution source required')
|
||||||
|
planraw=(ROOT/PLAN).read_bytes();require(digest(planraw)==contract['bindings']['proposal_sha256'],'exact native action proposal required')
|
||||||
|
plan=parsed(planraw);require(plan.get('bootstrap',{}).get('source_primitive_sha256')==contract['source_bundle']['scripts/flex_auth_signer_custody.py'],'proposal executed primitive lineage mismatch')
|
||||||
|
require(plan.get('proposal_id')=='RPF35-PDP-SIGNER-NATIVE-20261016-R1' and plan.get('bootstrap',{}).get('kid')==contract['kid'],'exact proposed bootstrap kid required')
|
||||||
|
ccrraw=(ROOT/CCR).read_bytes();require(digest(ccrraw)==contract['ccr_sha256'],'reviewed read CCR source mismatch')
|
||||||
|
spec=importlib.util.spec_from_file_location('native_signer_ccr',ROOT/'scripts/credential-change.py');module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
|
||||||
|
ccr,errors,_=module.validate_ccr(ROOT/CCR)
|
||||||
|
require(not errors and ccr['status'] in {'approved','applied','verified','active'} and
|
||||||
|
ccr['openbao']['auth']['bound_claims_confirmed'] is True,'approved exact read CCR required')
|
||||||
|
scope=ccr['openbao'];require(scope['kv_path']=='platform/workloads/flex-auth/envelope-signing' and
|
||||||
|
scope['fields']==['ED25519_PRIVATE_KEY'] and scope['metadata_read'] is False and scope['policy_name']=='workload-kv-read-flex-auth-envelope-signing' and
|
||||||
|
scope['auth']['role']==ROLE and scope['auth']['bound_claims']=={'service_account_names':['flex-auth-envelope-signing-eso'],'service_account_namespaces':['flex-auth']},'read scope correspondence refused')
|
||||||
|
require((ROOT/scope['policy_file']).read_text()==module.generated_policy_hcl(ccr),'read policy source drift')
|
||||||
|
|
||||||
|
|
||||||
|
def attended(io,contract):
|
||||||
|
_,status=io.bao(['status','-format=json']);require(status.get('cluster_id')==BAO_CLUSTER and status.get('sealed') is False,'backend cluster pin required')
|
||||||
|
_,lookup=io.bao(['token','lookup','-format=json']);who=lookup.get('data',{})
|
||||||
|
require('platform-admin' in who.get('policies',[]) and 'root' not in who.get('policies',[]) and
|
||||||
|
type(who.get('path')) is str and who['path'].startswith('auth/netkingdom/') and who.get('entity_id')==contract['founder_entity_id'],'exact attended non-root founder required')
|
||||||
|
_,audit=io.bao(['audit','list','-format=json']);require(type(audit) is dict and 0<len(audit)<=32,'enabled backend audit required')
|
||||||
|
# Token lookup-self is inside the contained warden child, never an agent
|
||||||
|
# retrieval. Its identifier stays in custody-process memory for fixed API I/O.
|
||||||
|
token=who.get('id');require(type(token) is str and 0<len(token)<=4096,'contained session handle unavailable')
|
||||||
|
io._token=token
|
||||||
|
_,policy=io.bao(['read','-format=json','sys/policies/acl/workload-kv-read-flex-auth-envelope-signing'])
|
||||||
|
expected_policy=(ROOT/'openbao/policies/workload-kv-read-flex-auth-envelope-signing.hcl').read_text()
|
||||||
|
require(_same_policy(policy.get('data',{}).get('policy'),expected_policy),'native reader policy drift')
|
||||||
|
_,role=io.bao(['read','-format=json','auth/kubernetes/role/'+ROLE]);role=role.get('data',{})
|
||||||
|
required={'bound_service_account_names':['flex-auth-envelope-signing-eso'],
|
||||||
|
'bound_service_account_namespaces':['flex-auth'],'bound_service_account_namespace_selector':'',
|
||||||
|
'audience':'openbao','token_policies':['workload-kv-read-flex-auth-envelope-signing'],
|
||||||
|
'token_ttl':900,'token_max_ttl':900,'token_explicit_max_ttl':900,'token_no_default_policy':True,
|
||||||
|
'token_period':0,'token_num_uses':0,'token_bound_cidrs':[]}
|
||||||
|
require(all(type(role.get(key)) is type(expected) and role.get(key)==expected for key,expected in required.items()),'native delivery role drift')
|
||||||
|
|
||||||
|
|
||||||
|
class NativeWriter(SignerWriter):
|
||||||
|
def __init__(self,io,contract,directory):
|
||||||
|
self.io=io;self.contract=contract;self.final_ready=False
|
||||||
|
self._initialize_receipt(directory,contract['operation_id'],kid=contract['kid'],expected_version=0,bindings=contract['bindings'])
|
||||||
|
def _save(self,*,initial=False):
|
||||||
|
if not self.final_ready and self.receipt['status']=='custody-verified-pending-independent-public-admission':
|
||||||
|
self.receipt['status']='seed-verified-pending-metadata'
|
||||||
|
self.receipt.update(actor='founder-attended-platform-admin',native_custody=True,
|
||||||
|
admission_sha256=digest(encoded(self.contract)),source_ccr_sha256=self.contract['ccr_sha256'])
|
||||||
|
return super()._save(initial=initial)
|
||||||
|
def _http(self,method,path,body=None):
|
||||||
|
if method=='POST':
|
||||||
|
validate_admission(self.contract);checked_sources(self.contract);fence(self.io,self.contract)
|
||||||
|
return self.io.http(method,path,body)
|
||||||
|
|
||||||
|
|
||||||
|
def exact_metadata(status,value,*,created):
|
||||||
|
require(status==200 and type(value) is dict and 'errors' not in value and type(value.get('data')) is dict,'metadata readback unconfirmed')
|
||||||
|
data=value['data'];require(type(data.get('cas_required')) is bool and data['cas_required'] is True and
|
||||||
|
type(data.get('max_versions')) is int and data['max_versions']==2 and data.get('delete_version_after')=='0s' and
|
||||||
|
type(data.get('current_version')) is int and data['current_version']==(1 if created else 0),'metadata contract drift')
|
||||||
|
|
||||||
|
|
||||||
|
def reconcile(io,contract,directory,original_path):
|
||||||
|
# A fresh independently approved recover action may read/reconcile only;
|
||||||
|
# original write receipt remains untouched and no writer/keygen is created.
|
||||||
|
raw=private_read(original_path);require(digest(raw)==contract['original_receipt_sha256'],'exact original receipt required')
|
||||||
|
receipt=parsed(raw)
|
||||||
|
receipt_fields={'schema','operation_id','bindings','writer_sha256','actor','created_at','path','expected_version','kid','status','production_admission','rollback','public_key','public_fingerprint','kv_version','exact_readback','native_custody','admission_sha256','source_ccr_sha256','phase'}
|
||||||
|
require(type(receipt) is dict and set(receipt)==receipt_fields and receipt.get('schema')=='railiance-platform.signer-cas-receipt.v1' and
|
||||||
|
receipt.get('status') in {'write-intent-unknown','seed-verified-pending-metadata'} and receipt.get('path')==PATH and
|
||||||
|
receipt.get('kid')==contract['kid'] and type(receipt.get('expected_version')) is int and receipt['expected_version']==0 and
|
||||||
|
(receipt.get('kv_version') is None or (type(receipt['kv_version']) is int and receipt['kv_version']==1)) and
|
||||||
|
type(receipt.get('exact_readback')) is bool and receipt.get('production_admission') is False and
|
||||||
|
receipt.get('source_ccr_sha256')==contract['original_execution']['source_ccr_sha256'] and receipt.get('writer_sha256')==contract['original_execution']['writer_sha256'] and
|
||||||
|
receipt.get('native_custody') is True and receipt.get('bindings')==contract['original_execution']['bindings'] and
|
||||||
|
receipt.get('phase') in {'metadata-setup','seed-cas'},'original custody receipt mismatch')
|
||||||
|
has_public=(type(receipt.get('public_key')) is str and re.fullmatch(r'[A-Za-z0-9_-]{43}',receipt['public_key']) is not None
|
||||||
|
and receipt.get('public_fingerprint')==digest(receipt['public_key'].encode()))
|
||||||
|
no_public=(receipt.get('public_key') is None and receipt.get('public_fingerprint') is None)
|
||||||
|
require((receipt['phase']=='metadata-setup' and no_public and receipt['kv_version'] is None and receipt['exact_readback'] is False)
|
||||||
|
or (receipt['phase']=='seed-cas' and has_public),'original phase/public lineage mismatch')
|
||||||
|
# Reserve recovery evidence before private native reads. A crash or failure
|
||||||
|
# leaves this independent attempt unconfirmed and cannot replay its ID.
|
||||||
|
recovery={'schema':'railiance-platform.signer-cas-recovery.v1','operation_id':contract['operation_id'],
|
||||||
|
'original_receipt_sha256':digest(raw),'original_execution':contract['original_execution'],
|
||||||
|
'admission_sha256':digest(encoded(contract)),
|
||||||
|
'status':'recovery-read-intent-unconfirmed','original_phase':receipt['phase'],
|
||||||
|
'kid':contract['kid'],'kv_version':None,'public_key':None,'public_fingerprint':None,
|
||||||
|
'key_custody_verified':False,'historical_effect':'unresolved',
|
||||||
|
'next_action':'fresh independently reviewed exact action required; no generation/write/retry authority',
|
||||||
|
'no_write_or_remint':True,'original_unknown_preserved':True}
|
||||||
|
store=object.__new__(SignerWriter);store.directory=directory;store.path=directory/(contract['operation_id']+'.json');store.receipt=recovery
|
||||||
|
store._save(initial=True)
|
||||||
|
status,value=io.http('GET',META)
|
||||||
|
if _policy_absent(status,value):
|
||||||
|
absent=True
|
||||||
|
else:
|
||||||
|
require(status==200 and type(value) is dict and 'errors' not in value and type(value.get('data')) is dict,'recovery metadata unconfirmed')
|
||||||
|
current=value['data'].get('current_version')
|
||||||
|
require(type(current) is int and current in {0,1},'recovery metadata version ambiguous')
|
||||||
|
exact_metadata(status,value,created=current==1)
|
||||||
|
absent=False
|
||||||
|
if current==1:require(has_public and receipt['phase']=='seed-cas','unexpected key custody after metadata-only phase')
|
||||||
|
if absent or current==0:
|
||||||
|
status,value=io.http('GET',PATH+'?version=1')
|
||||||
|
require(_policy_absent(status,value),'recovery data absence unconfirmed')
|
||||||
|
store.receipt.update(status='current-custody-absent' if absent else 'metadata-only-confirmed')
|
||||||
|
store._save()
|
||||||
|
return dict(store.receipt)
|
||||||
|
status,value=io.http('GET',PATH+'?version=1');require(status==200 and type(value) is dict and 'errors' not in value,'recovery private readback unconfirmed')
|
||||||
|
data=value.get('data',{});document=data.get('data');metadata=data.get('metadata',{})
|
||||||
|
require(type(document) is dict and set(document)==FIELDS and all(type(x) is str for x in document.values()) and
|
||||||
|
type(metadata.get('version')) is int and metadata['version']==1 and document['kid']==contract['kid'],'recovery version/shape mismatch')
|
||||||
|
try:
|
||||||
|
seed=base64.urlsafe_b64decode(document['ED25519_PRIVATE_KEY']+'=')
|
||||||
|
require(len(seed)==32 and b64(seed)==document['ED25519_PRIVATE_KEY'],'noncanonical recovery seed')
|
||||||
|
public=b64(Ed25519PrivateKey.from_private_bytes(seed).public_key().public_bytes(serialization.Encoding.Raw,serialization.PublicFormat.Raw))
|
||||||
|
except Exception:raise Refused('recovery key correspondence unconfirmed') from None
|
||||||
|
require(hmac.compare_digest(public,document['ED25519_PUBLIC_KEY']) and hmac.compare_digest(public,receipt['public_key']) and
|
||||||
|
receipt.get('public_fingerprint')==digest(public.encode()),'recovery public correspondence unconfirmed')
|
||||||
|
store.receipt.update(status='custody-reconciled-pending-independent-public-admission',kv_version=1,public_key=public,public_fingerprint=digest(public.encode()),key_custody_verified=True)
|
||||||
|
store._save()
|
||||||
|
return dict(store.receipt)
|
||||||
|
|
||||||
|
|
||||||
|
def execute(contract,directory,*,io=None,original_receipt=None):
|
||||||
|
remaining=validate_admission(contract);checked_sources(contract)
|
||||||
|
require(isinstance(directory,Path) and directory.absolute()==directory.resolve(),'exact private receipt directory required')
|
||||||
|
Journal._private(directory.stat(),directory=True)
|
||||||
|
plan=parsed((ROOT/PLAN).read_bytes())
|
||||||
|
require(str(directory)==plan['receipts']['directory'],'independently selected native receipt custody required')
|
||||||
|
if original_receipt is not None:require(Path(original_receipt).parent==directory,'original receipt custody mismatch')
|
||||||
|
# One native owner window; lock is custody concurrency only, not cluster CAS.
|
||||||
|
lock=os.open(directory/'.signer-window.lock',os.O_RDWR|os.O_CREAT|os.O_NOFOLLOW,0o600)
|
||||||
|
try:
|
||||||
|
Journal._private(os.fstat(lock));fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB)
|
||||||
|
with _absolute_budget(remaining):
|
||||||
|
io=NativeIO() if io is None else io
|
||||||
|
attended(io,contract);fence(io,contract)
|
||||||
|
if contract['action']=='recover':
|
||||||
|
require(original_receipt is not None,'independent original receipt required')
|
||||||
|
return reconcile(io,contract,directory,original_receipt)
|
||||||
|
require(original_receipt is None,'bootstrap cannot import recovery/private values')
|
||||||
|
writer=NativeWriter(io,contract,directory)
|
||||||
|
status,value=io.http('GET',META)
|
||||||
|
require(_policy_absent(status,value),'bootstrap exact metadata absence required')
|
||||||
|
writer.receipt.update(status='write-intent-unknown',phase='metadata-setup');writer._save()
|
||||||
|
try:
|
||||||
|
validate_admission(contract);checked_sources(contract);fence(io,contract)
|
||||||
|
status,value=io.http('POST',META,{'cas_required':True,'max_versions':2,'delete_version_after':'0s'})
|
||||||
|
require(status in {200,204},'metadata write unconfirmed')
|
||||||
|
exact_metadata(*io.http('GET',META),created=False)
|
||||||
|
writer.receipt['phase']='seed-cas'
|
||||||
|
writer.write()
|
||||||
|
exact_metadata(*io.http('GET',META),created=True)
|
||||||
|
writer.final_ready=True
|
||||||
|
writer.receipt.update(status='custody-verified-pending-independent-public-admission',exact_readback=True)
|
||||||
|
writer._save()
|
||||||
|
return dict(writer.receipt)
|
||||||
|
except BaseException:
|
||||||
|
writer.receipt.update(status='write-intent-unknown',exact_readback=False);writer._save()
|
||||||
|
raise Refused('native custody outcome unconfirmed') from None
|
||||||
|
finally:os.close(lock)
|
||||||
|
|
||||||
|
|
||||||
|
class SilentParser(argparse.ArgumentParser):
|
||||||
|
def error(self,message):raise Refused('invalid custody command arguments')
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
parser=SilentParser(description=__doc__,add_help=False)
|
||||||
|
parser.add_argument('--admission',required=True);parser.add_argument('--admission-sha256',required=True)
|
||||||
|
parser.add_argument('--receipt-directory',required=True);parser.add_argument('--original-receipt')
|
||||||
|
# argparse output is captured by warden; all execution exceptions are silent.
|
||||||
|
try:
|
||||||
|
require(not sys.flags.optimize,'optimized custody interpreter refused')
|
||||||
|
args=parser.parse_args(argv);raw=private_read(args.admission)
|
||||||
|
require(is_digest(args.admission_sha256) and digest(raw)==args.admission_sha256,'independently reviewed admission digest required')
|
||||||
|
execute(parsed(raw),Path(args.receipt_directory),original_receipt=args.original_receipt)
|
||||||
|
return 0
|
||||||
|
except BaseException:return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__=='__main__':sys.exit(main())
|
||||||
318
tests/test_flex_auth_signer_native.py
Normal file
318
tests/test_flex_auth_signer_native.py
Normal file
|
|
@ -0,0 +1,318 @@
|
||||||
|
"""Actual disposable KV effects through native production state machine.
|
||||||
|
|
||||||
|
Kubernetes, attended identity and owner dispositions are explicitly synthetic
|
||||||
|
fixture inputs; no production authority/identity/custody claim follows.
|
||||||
|
"""
|
||||||
|
from copy import deepcopy
|
||||||
|
from datetime import datetime,timedelta,timezone
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
|
import pytest
|
||||||
|
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'scripts'))
|
||||||
|
from test_task_execution_native_fixture import bao_dev
|
||||||
|
import flex_auth_signer_native as m
|
||||||
|
from task_execution_journal import Refused
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def native(bao_dev,tmp_path,monkeypatch):
|
||||||
|
origin,token,call,process=bao_dev
|
||||||
|
assert call('POST','sys/mounts/platform',{'type':'kv','options':{'version':'2'}})[0] in {200,204}
|
||||||
|
source=tmp_path/'source';source.mkdir()
|
||||||
|
for name in [*m.SOURCE_FILES,m.CCR,m.PLAN,'openbao/policies/workload-kv-read-flex-auth-envelope-signing.hcl']:
|
||||||
|
p=source/name;p.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(m.ROOT/name,p)
|
||||||
|
ccr=yaml.safe_load((source/m.CCR).read_text());ccr['status']='approved';ccr['openbao']['auth']['bound_claims_confirmed']=True
|
||||||
|
(source/m.CCR).write_text(yaml.safe_dump(ccr,sort_keys=False))
|
||||||
|
plan=json.loads((source/m.PLAN).read_text());plan['bootstrap']['kid']='synthetic-native-kid';plan['receipts']['directory']=str(tmp_path/'private');(source/m.PLAN).write_text(json.dumps(plan))
|
||||||
|
monkeypatch.setattr(m,'ROOT',source)
|
||||||
|
directory=tmp_path/'private';directory.mkdir(mode=0o700)
|
||||||
|
image='forgejo.coulomb.social/coulomb/flex-auth@sha256:'+'1'*64
|
||||||
|
dep={'metadata':{'uid':'dep','generation':1},'spec':{'replicas':0,'template':{'spec':{'serviceAccountName':m.NAME,
|
||||||
|
'containers':[{'image':image,'args':['serve','--caller-auth-mode','enforce','--caller-audience','flex-auth','--caller-binding','secrets-engine=system:serviceaccount:secrets-engine:secrets-engine','--envelope-signing-key-file','/var/run/secrets/decision-envelope-signing/seed','--envelope-signing-kid','synthetic-native-kid'],
|
||||||
|
'volumeMounts':[{'name':'flex-auth-envelope-signing','mountPath':'/var/run/secrets/decision-envelope-signing','readOnly':True}]}],
|
||||||
|
'volumes':[{'name':'flex-auth-envelope-signing','secret':{'secretName':m.SECRET,'defaultMode':288,'items':[{'key':'seed','path':'seed'}]}}]}}},'status':{'observedGeneration':1,'replicas':0}}
|
||||||
|
class IO:
|
||||||
|
def __init__(self):
|
||||||
|
self.calls=[];self.dep=deepcopy(dep);self.pods=[];self.hpas=[];self.rs=[];self.cluster=m.CLUSTER
|
||||||
|
self.entity='11111111-1111-1111-1111-111111111111';self.backend=m.BAO_CLUSTER;self.root=False;self.role_drift=False;self.lost=False;self.denied=False;self.pod_on_recheck=False;self.fences=0
|
||||||
|
def bao(self,args,payload=None,allowed=(0,)):
|
||||||
|
self.calls.append(('bao',tuple(args)))
|
||||||
|
if args[0]=='status':return 0,{'cluster_id':self.backend,'sealed':False}
|
||||||
|
if args[:2]==['token','lookup']:return 0,{'data':{'policies':['root'] if self.root else ['platform-admin'],'path':'auth/netkingdom/login/x','entity_id':self.entity,'id':token}}
|
||||||
|
if args[0]=='audit':return 0,{'file/':{}}
|
||||||
|
if args[-1].startswith('sys/policies/acl/'):
|
||||||
|
return 0,{'data':{'policy':(source/'openbao/policies/workload-kv-read-flex-auth-envelope-signing.hcl').read_text()}}
|
||||||
|
if args[-1].startswith('auth/kubernetes/role/'):
|
||||||
|
return 0,{'data':{'bound_service_account_names':['foreign'] if self.role_drift else ['flex-auth-envelope-signing-eso'],'bound_service_account_namespaces':['flex-auth'],'bound_service_account_namespace_selector':'','audience':'openbao','token_policies':['workload-kv-read-flex-auth-envelope-signing'],'token_ttl':900,'token_max_ttl':900,'token_explicit_max_ttl':900,'token_no_default_policy':True,'token_period':0,'token_num_uses':0,'token_bound_cidrs':[]}}
|
||||||
|
raise AssertionError('unexpected fixed API')
|
||||||
|
def kube(self,resource,name=None):
|
||||||
|
self.calls.append(('kube',resource,name))
|
||||||
|
if resource=='namespace':return {'metadata':{'uid':self.cluster}}
|
||||||
|
if resource=='deployment':
|
||||||
|
self.fences+=1
|
||||||
|
if self.pod_on_recheck and self.fences>=3:self.pods=[{'spec':{'serviceAccountName':m.NAME}}]
|
||||||
|
return deepcopy(self.dep)
|
||||||
|
return {'items':deepcopy({'pods':self.pods,'hpa':self.hpas,'replicasets':self.rs}[resource])}
|
||||||
|
def http(self,method,path,body=None):
|
||||||
|
self.calls.append(('http',method,path))
|
||||||
|
if self.denied and path in {m.PATH,m.PATH+'?version=1'} and method=='GET':return 403,{'errors':['secret backend detail']}
|
||||||
|
result=call(method,path,body)
|
||||||
|
if self.lost and method=='POST' and path==m.PATH:raise OSError('secret response lost')
|
||||||
|
return result
|
||||||
|
io=IO()
|
||||||
|
c={'schema':'railiance-platform.signer-native-admission.v1','action':'bootstrap','operation_id':'synthetic-native-operation','kid':'synthetic-native-kid',
|
||||||
|
'expires_at':(datetime.now(timezone.utc)+timedelta(seconds=100)).isoformat(),'founder_entity_id':io.entity,
|
||||||
|
'ccr_sha256':m.digest((source/m.CCR).read_bytes()),'source_bundle':{n:m.digest((source/n).read_bytes()) for n in m.SOURCE_FILES},
|
||||||
|
'bindings':{k:'sha256:'+'2'*64 for k in m.BINDINGS},'producer_image':image,'caller_binding':'secrets-engine=system:serviceaccount:secrets-engine:secrets-engine',
|
||||||
|
'deployment_projection_sha256':m.digest(m.encoded(m.deployment_projection(dep))),
|
||||||
|
'authority':'founder-attended-exact-source-scope; no standing grant',
|
||||||
|
'routing':{'lane_id':'openbao-platform-admin-login','verdict':'founder_required','founder_act':'oidc_login','receipt_sha256':'sha256:'+'3'*64},
|
||||||
|
'owner_reviews':{},'exclusive_window_sha256':'sha256:'+'4'*64,'consumer_trust_sha256':'sha256:'+'5'*64,'original_receipt_sha256':None,'original_execution':None}
|
||||||
|
c['bindings'].update(proposal_sha256=m.digest((source/m.PLAN).read_bytes()),policy_sha256=m.LIFECYCLE,image_sha256='sha256:'+'1'*64,trust_sha256=c['consumer_trust_sha256'],deployment_sha256=c['deployment_projection_sha256'],executed_revision_sha256=m.digest(m.encoded(c['source_bundle'])))
|
||||||
|
def approve(value):
|
||||||
|
if value['action']=='recover' and value['original_execution'] is None:
|
||||||
|
old=json.loads((directory/(c['operation_id']+'.json')).read_bytes())
|
||||||
|
value['original_execution']={key:deepcopy(old[key]) for key in ['bindings','writer_sha256','source_ccr_sha256']}
|
||||||
|
scope=m.digest(m.encoded({k:v for k,v in value.items() if k!='owner_reviews'}))
|
||||||
|
value['owner_reviews']={role:{'approved_admission_sha256':scope,'approval_ref':'synthetic-fixture-'+role} for role in m.REVIEWS}
|
||||||
|
return value
|
||||||
|
approve(c)
|
||||||
|
return c,io,directory,call,approve,source
|
||||||
|
|
||||||
|
|
||||||
|
def test_actual_native_bootstrap_exact_readback_and_no_retry(native):
|
||||||
|
c,io,directory,call,approve,source=native
|
||||||
|
result=m.execute(c,directory,io=io)
|
||||||
|
assert result['status']=='custody-verified-pending-independent-public-admission' and result['exact_readback']
|
||||||
|
assert call('GET',m.META)[1]['data']['current_version']==1
|
||||||
|
assert [(x[1],x[2]) for x in io.calls if x[0]=='http' and x[1]=='POST']==[('POST',m.META),('POST',m.PATH)]
|
||||||
|
private=call('GET',m.PATH)[1]['data']['data']['ED25519_PRIVATE_KEY']
|
||||||
|
raw=(directory/(c['operation_id']+'.json')).read_bytes();assert private.encode() not in raw
|
||||||
|
before=len(io.calls)
|
||||||
|
with pytest.raises(FileExistsError):m.execute(c,directory,io=io)
|
||||||
|
assert not any(x[0]=='http' and x[1]=='POST' for x in io.calls[before:])
|
||||||
|
|
||||||
|
|
||||||
|
def test_actual_process_loss_unknown_fresh_recovery_read_only(native):
|
||||||
|
c,io,directory,call,approve,source=native
|
||||||
|
child=os.fork()
|
||||||
|
if child==0:
|
||||||
|
original_http=io.http
|
||||||
|
def die_after_native_write(method,path,body=None):
|
||||||
|
result=original_http(method,path,body)
|
||||||
|
if method=='POST' and path==m.PATH:os._exit(73)
|
||||||
|
return result
|
||||||
|
io.http=die_after_native_write
|
||||||
|
try:m.execute(c,directory,io=io)
|
||||||
|
except BaseException:os._exit(74)
|
||||||
|
os._exit(75)
|
||||||
|
_,status=os.waitpid(child,0)
|
||||||
|
assert os.waitstatus_to_exitcode(status)==73
|
||||||
|
old=directory/(c['operation_id']+'.json');original=old.read_bytes();assert json.loads(original)['status']=='write-intent-unknown'
|
||||||
|
recovery=deepcopy(c);recovery.update(action='recover',operation_id='synthetic-independent-recovery',original_receipt_sha256=m.digest(original));approve(recovery)
|
||||||
|
io.lost=False;io.calls.clear()
|
||||||
|
result=m.execute(recovery,directory,io=io,original_receipt=old)
|
||||||
|
assert result['status']=='custody-reconciled-pending-independent-public-admission'
|
||||||
|
assert result['no_write_or_remint'] and old.read_bytes()==original
|
||||||
|
assert not any(x[0]=='http' and x[1]=='POST' for x in io.calls)
|
||||||
|
assert call('GET',m.META)[1]['data']['current_version']==1
|
||||||
|
assert (directory/(recovery['operation_id']+'.json')).stat().st_mode&0o777==0o600
|
||||||
|
io.calls.clear()
|
||||||
|
with pytest.raises(FileExistsError):m.execute(recovery,directory,io=io,original_receipt=old)
|
||||||
|
assert not any(x[0]=='http' for x in io.calls)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('case',['missing-image','missing-trust','mismatched-source','bad-review','bad-route','expired','wrong-entity','wrong-backend','wrong-cluster','root','pod','terminating-pod','hpa','replicas','image-drift','role-drift','pod-after-metadata'])
|
||||||
|
def test_refusals_no_seed_effect(native,case):
|
||||||
|
c,io,directory,call,approve,source=native
|
||||||
|
if case=='missing-image':c['producer_image']=''
|
||||||
|
elif case=='missing-trust':c['consumer_trust_sha256']=''
|
||||||
|
elif case=='mismatched-source':(source/'scripts/flex_auth_signer_custody.py').write_text('changed source')
|
||||||
|
elif case=='bad-review':c['owner_reviews']['founder']['approved_admission_sha256']='sha256:'+'f'*64
|
||||||
|
elif case=='bad-route':c['routing']['lane_id']='openbao-structure-survey';approve(c)
|
||||||
|
elif case=='expired':c['expires_at']=(datetime.now(timezone.utc)-timedelta(seconds=1)).isoformat();approve(c)
|
||||||
|
elif case=='wrong-entity':io.entity='other'
|
||||||
|
elif case=='wrong-backend':io.backend='other'
|
||||||
|
elif case=='wrong-cluster':io.cluster='other'
|
||||||
|
elif case=='root':io.root=True
|
||||||
|
elif case=='pod':io.pods=[{'spec':{'serviceAccountName':m.NAME}}]
|
||||||
|
elif case=='terminating-pod':io.pods=[{'metadata':{'deletionTimestamp':'now'},'spec':{'volumes':[{'secret':{'secretName':m.SECRET}}]}}]
|
||||||
|
elif case=='hpa':io.hpas=[{'spec':{'scaleTargetRef':{'name':m.NAME}}}]
|
||||||
|
elif case=='replicas':io.dep['spec']['replicas']=1
|
||||||
|
elif case=='image-drift':io.dep['spec']['template']['spec']['containers'][0]['image']='unreviewed'
|
||||||
|
elif case=='role-drift':io.role_drift=True
|
||||||
|
elif case=='pod-after-metadata':io.pod_on_recheck=True
|
||||||
|
with pytest.raises(Refused):m.execute(c,directory,io=io)
|
||||||
|
assert not any(x[0]=='http' and x[1]=='POST' and x[2]==m.PATH for x in io.calls)
|
||||||
|
assert call('GET',m.PATH)[0]==404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('case',['damaged-receipt','typed-damaged-receipt','changed-version','read-denied','no-fresh-review'])
|
||||||
|
def test_recovery_refuses_without_new_effect(native,case):
|
||||||
|
c,io,directory,call,approve,source=native;io.lost=True
|
||||||
|
with pytest.raises(Refused):m.execute(c,directory,io=io)
|
||||||
|
old=directory/(c['operation_id']+'.json');recovery=deepcopy(c);recovery.update(action='recover',operation_id='recovery',original_receipt_sha256=m.digest(old.read_bytes()));approve(recovery)
|
||||||
|
if case=='damaged-receipt':old.write_text('{}')
|
||||||
|
elif case=='typed-damaged-receipt':
|
||||||
|
damaged=json.loads(old.read_text());damaged['expected_version']=False;old.write_text(json.dumps(damaged));recovery['original_receipt_sha256']=m.digest(old.read_bytes());approve(recovery)
|
||||||
|
elif case=='changed-version':
|
||||||
|
doc=call('GET',m.PATH)[1]['data']['data'];assert call('POST',m.PATH,{'options':{'cas':1},'data':doc})[0]==200
|
||||||
|
elif case=='read-denied':io.denied=True
|
||||||
|
elif case=='no-fresh-review':recovery['owner_reviews']['founder']['approved_admission_sha256']='sha256:'+'f'*64
|
||||||
|
io.calls.clear();io.lost=False
|
||||||
|
with pytest.raises(Refused):m.execute(recovery,directory,io=io,original_receipt=old)
|
||||||
|
assert not any(x[0]=='http' and x[1]=='POST' for x in io.calls)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('bad_args',[False,True])
|
||||||
|
def test_main_missing_approval_silent_and_no_io(tmp_path,capsys,bad_args):
|
||||||
|
if bad_args:
|
||||||
|
assert m.main(['--unrecognized'])==1
|
||||||
|
output=capsys.readouterr();assert output.out==output.err==''
|
||||||
|
return
|
||||||
|
assert m.main(['--admission',str(tmp_path/'missing'),'--admission-sha256','sha256:'+'0'*64,'--receipt-directory',str(tmp_path)])==1
|
||||||
|
output=capsys.readouterr();assert output.out==output.err==''
|
||||||
|
|
||||||
|
|
||||||
|
def test_native_command_byte_and_elapsed_bounds_no_output(capsys):
|
||||||
|
io=m.NativeIO()
|
||||||
|
with pytest.raises(Refused):io.captured([sys.executable,'-c',"import sys;sys.stdout.write('x'*1048577)"])
|
||||||
|
import time
|
||||||
|
started=time.monotonic()
|
||||||
|
with pytest.raises(Refused):io.captured([sys.executable,'-c',"import time;time.sleep(10)"])
|
||||||
|
assert time.monotonic()-started<3
|
||||||
|
output=capsys.readouterr();assert output.out==output.err==''
|
||||||
|
|
||||||
|
|
||||||
|
def test_exclusive_native_window_refuses_second_writer(native):
|
||||||
|
import fcntl
|
||||||
|
c,io,directory,call,approve,source=native
|
||||||
|
fd=os.open(directory/'.signer-window.lock',os.O_RDWR|os.O_CREAT,0o600)
|
||||||
|
try:
|
||||||
|
fcntl.flock(fd,fcntl.LOCK_EX|fcntl.LOCK_NB)
|
||||||
|
with pytest.raises(BlockingIOError):m.execute(c,directory,io=io)
|
||||||
|
assert io.calls==[]
|
||||||
|
finally:os.close(fd)
|
||||||
|
|
||||||
|
|
||||||
|
def test_native_fixed_http_transport_against_disposable_backend(bao_dev,monkeypatch,capsys):
|
||||||
|
origin,token,call,process=bao_dev
|
||||||
|
assert call('POST','sys/mounts/platform',{'type':'kv','options':{'version':'2'}})[0] in {200,204}
|
||||||
|
factory=m.http.client.HTTPConnection;port=int(origin.rsplit(':',1)[1]);destinations=[]
|
||||||
|
def fixture_socket(host,requested_port,*args,**kwargs):
|
||||||
|
destinations.append((host,requested_port))
|
||||||
|
return factory(host,port if requested_port==18200 else requested_port,*args,**kwargs)
|
||||||
|
monkeypatch.setattr(m.http.client,'HTTPConnection',fixture_socket)
|
||||||
|
io=m.NativeIO();io._token=token
|
||||||
|
assert io.http('GET',m.META)==(404,{'errors':[]})
|
||||||
|
assert io.http('POST',m.META,{'cas_required':True,'max_versions':2,'delete_version_after':'0s'})[0] in {200,204}
|
||||||
|
m.exact_metadata(*io.http('GET',m.META),created=False)
|
||||||
|
assert all(host=='127.0.0.1' and selected_port==18200 for host,selected_port in destinations)
|
||||||
|
with pytest.raises(Refused):io.http('GET','other/path')
|
||||||
|
output=capsys.readouterr();assert output.out==output.err==''
|
||||||
|
|
||||||
|
|
||||||
|
def metadata_phase_crash(native,*,after):
|
||||||
|
c,io,directory,call,approve,source=native
|
||||||
|
child=os.fork()
|
||||||
|
if child==0:
|
||||||
|
original=io.http
|
||||||
|
def die(method,path,body=None):
|
||||||
|
if method=='POST' and path==m.META:
|
||||||
|
if after:original(method,path,body)
|
||||||
|
os._exit(73)
|
||||||
|
return original(method,path,body)
|
||||||
|
io.http=die
|
||||||
|
try:m.execute(c,directory,io=io)
|
||||||
|
except BaseException:os._exit(74)
|
||||||
|
os._exit(75)
|
||||||
|
_,status=os.waitpid(child,0);assert os.waitstatus_to_exitcode(status)==73
|
||||||
|
old=directory/(c['operation_id']+'.json');raw=old.read_bytes();receipt=json.loads(raw)
|
||||||
|
assert receipt['phase']=='metadata-setup' and receipt['status']=='write-intent-unknown'
|
||||||
|
assert receipt['public_key'] is None and receipt['kv_version'] is None
|
||||||
|
recovery=deepcopy(c);recovery.update(action='recover',operation_id='metadata-recovery',original_receipt_sha256=m.digest(raw));approve(recovery)
|
||||||
|
return old,raw,recovery
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('after',[False,True])
|
||||||
|
def test_real_metadata_process_loss_read_only_phase_recovery(native,after):
|
||||||
|
c,io,directory,call,approve,source=native
|
||||||
|
old,raw,recovery=metadata_phase_crash(native,after=after)
|
||||||
|
io.calls.clear();result=m.execute(recovery,directory,io=io,original_receipt=old)
|
||||||
|
assert result['status']==('metadata-only-confirmed' if after else 'current-custody-absent')
|
||||||
|
assert result['public_key'] is result['public_fingerprint'] is result['kv_version'] is None
|
||||||
|
assert result['key_custody_verified'] is False and result['historical_effect']=='unresolved'
|
||||||
|
assert 'fresh independently reviewed exact action required' in result['next_action']
|
||||||
|
assert old.read_bytes()==raw and result['original_unknown_preserved']
|
||||||
|
assert not any(x[0]=='http' and x[1]=='POST' for x in io.calls)
|
||||||
|
assert call('GET',m.PATH+'?version=1')==(404,{'errors':[]})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('case',['metadata-denied','metadata-misleading404','metadata-misleading200','metadata-malformed','metadata-timeout','data-denied','unexpected-version'])
|
||||||
|
def test_metadata_recovery_ambiguous_or_denied_never_classifies_or_writes(native,case):
|
||||||
|
c,io,directory,call,approve,source=native
|
||||||
|
old,raw,recovery=metadata_phase_crash(native,after=True)
|
||||||
|
if case=='unexpected-version':
|
||||||
|
assert call('POST',m.PATH,{'options':{'cas':0},'data':{'ED25519_PRIVATE_KEY':'synthetic-unadmitted','ED25519_PUBLIC_KEY':'synthetic-unadmitted','kid':c['kid']}})[0]==200
|
||||||
|
original=io.http
|
||||||
|
def faulty(method,path,body=None):
|
||||||
|
if path==m.META:
|
||||||
|
if case=='metadata-denied':return 403,{'errors':['denied']}
|
||||||
|
if case=='metadata-misleading404':return 404,{'errors':['permission denied']}
|
||||||
|
if case=='metadata-misleading200':
|
||||||
|
result=original(method,path,body);result[1]['errors']=['denied'];return result
|
||||||
|
if case=='metadata-malformed':return 200,{'data':{'current_version':False}}
|
||||||
|
if case=='metadata-timeout':raise OSError('synthetic private backend detail')
|
||||||
|
if path==m.PATH+'?version=1' and case=='data-denied':return 404,{'errors':['denied']}
|
||||||
|
return original(method,path,body)
|
||||||
|
io.http=faulty;io.calls.clear()
|
||||||
|
with pytest.raises((Refused,OSError)):m.execute(recovery,directory,io=io,original_receipt=old)
|
||||||
|
evidence=json.loads((directory/(recovery['operation_id']+'.json')).read_bytes())
|
||||||
|
assert evidence['status']=='recovery-read-intent-unconfirmed' and evidence['key_custody_verified'] is False
|
||||||
|
assert old.read_bytes()==raw
|
||||||
|
assert not any(x[0]=='http' and x[1]=='POST' for x in io.calls)
|
||||||
|
if case=='unexpected-version':assert not any(x[0]=='http' and x[2]==m.PATH+'?version=1' for x in io.calls)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stale_current_primitive_lineage_refuses_before_native_io(native):
|
||||||
|
c,io,directory,call,approve,source=native
|
||||||
|
plan=json.loads((source/m.PLAN).read_bytes());plan['bootstrap']['source_primitive_sha256']='sha256:'+'8'*64
|
||||||
|
(source/m.PLAN).write_text(json.dumps(plan));c['bindings']['proposal_sha256']=m.digest((source/m.PLAN).read_bytes());approve(c)
|
||||||
|
with pytest.raises(Refused,match='primitive lineage'):m.execute(c,directory,io=io)
|
||||||
|
assert io.calls==[]
|
||||||
|
|
||||||
|
|
||||||
|
def test_recovery_explicitly_binds_historical_execution_and_current_source(native):
|
||||||
|
c,io,directory,call,approve,source=native
|
||||||
|
old,raw,recovery=metadata_phase_crash(native,after=True)
|
||||||
|
# Model a separately retained older executed bundle/proposal. It must be
|
||||||
|
# explicitly selected in fresh recovery review, never treated as current.
|
||||||
|
original=json.loads(raw);original['bindings']['executed_revision_sha256']='sha256:'+'6'*64
|
||||||
|
original['bindings']['proposal_sha256']='sha256:'+'7'*64
|
||||||
|
old.write_text(json.dumps(original));recovery['original_receipt_sha256']=m.digest(old.read_bytes())
|
||||||
|
recovery['original_execution']={key:deepcopy(original[key]) for key in ['bindings','writer_sha256','source_ccr_sha256']};approve(recovery)
|
||||||
|
result=m.execute(recovery,directory,io=io,original_receipt=old)
|
||||||
|
assert result['status']=='metadata-only-confirmed'
|
||||||
|
assert result['original_execution']['bindings']['executed_revision_sha256']!=recovery['bindings']['executed_revision_sha256']
|
||||||
|
assert json.loads(old.read_text())==original
|
||||||
|
|
||||||
|
|
||||||
|
def test_generated_but_unstored_seed_phase_recovers_without_public_admission(native):
|
||||||
|
c,io,directory,call,approve,source=native
|
||||||
|
io.pod_on_recheck=True
|
||||||
|
with pytest.raises(Refused):m.execute(c,directory,io=io)
|
||||||
|
old=directory/(c['operation_id']+'.json');raw=old.read_bytes();receipt=json.loads(raw)
|
||||||
|
assert receipt['phase']=='seed-cas' and receipt['public_key'] is not None
|
||||||
|
assert call('GET',m.PATH+'?version=1')==(404,{'errors':[]})
|
||||||
|
io.pod_on_recheck=False;io.pods=[];io.calls.clear()
|
||||||
|
recovery=deepcopy(c);recovery.update(action='recover',operation_id='unstored-seed-recovery',original_receipt_sha256=m.digest(raw));approve(recovery)
|
||||||
|
result=m.execute(recovery,directory,io=io,original_receipt=old)
|
||||||
|
assert result['status']=='metadata-only-confirmed' and result['key_custody_verified'] is False
|
||||||
|
assert result['public_key'] is None and result['public_fingerprint'] is None
|
||||||
|
assert old.read_bytes()==raw
|
||||||
|
assert not any(x[0]=='http' and x[1]=='POST' for x in io.calls)
|
||||||
49
tests/test_flex_auth_signer_native_preparation.py
Normal file
49
tests/test_flex_auth_signer_native_preparation.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
"""Real CCR schema/applier contract, no credentials or backend invocation."""
|
||||||
|
from copy import deepcopy
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import yaml
|
||||||
|
ROOT=Path(__file__).resolve().parents[1]
|
||||||
|
spec=importlib.util.spec_from_file_location('signer_ccr',ROOT/'scripts/credential-change.py')
|
||||||
|
ccr_module=importlib.util.module_from_spec(spec);spec.loader.exec_module(ccr_module)
|
||||||
|
CCR=ROOT/'credential-change-requests/CCR-2026-0050-flex-auth-envelope-signing.yaml'
|
||||||
|
|
||||||
|
|
||||||
|
def test_real_schema_and_apply_refusal():
|
||||||
|
ccr,errors,warnings=ccr_module.validate_ccr(CCR)
|
||||||
|
assert errors==[]
|
||||||
|
assert warnings==['OIDC/Kubernetes bound claim is not confirmed; apply is blocked']
|
||||||
|
blockers=ccr_module.applier_readiness_blockers(ccr)
|
||||||
|
assert len(blockers)==2
|
||||||
|
assert any('status, got proposed' in x for x in blockers)
|
||||||
|
assert any('confirmed OpenBao auth binding' in x for x in blockers)
|
||||||
|
assert ccr['review']['comments']==[] and ccr['access_frontdoor']['resolvable'] is False
|
||||||
|
# Pure synthetic candidate only: ensure admitted-pattern scope has no latent
|
||||||
|
# schema/applier role/field failures once the real owners eventually approve.
|
||||||
|
admitted=deepcopy(ccr);admitted['status']='approved';admitted['openbao']['auth']['bound_claims_confirmed']=True
|
||||||
|
assert ccr_module.applier_readiness_blockers(admitted)==[]
|
||||||
|
|
||||||
|
|
||||||
|
def test_generated_read_scope_and_projection_join():
|
||||||
|
ccr,_,_=ccr_module.validate_ccr(CCR)
|
||||||
|
assert (ROOT/ccr['openbao']['policy_file']).read_text()==ccr_module.generated_policy_hcl(ccr)
|
||||||
|
hcl=ccr_module.generated_policy_hcl(ccr)
|
||||||
|
assert 'metadata/' not in hcl and 'list' not in hcl and 'create' not in hcl
|
||||||
|
assert ccr['openbao']['fields']==['ED25519_PRIVATE_KEY']
|
||||||
|
docs=list(yaml.safe_load_all((ROOT/'docs/credential-lane-designs/flex-auth-envelope-signing-native-projection.yaml.proposed').read_text()))
|
||||||
|
store=next(x for x in docs if x['kind']=='SecretStore')
|
||||||
|
assert store['spec']['provider']['vault']['auth']['kubernetes']['role']==ccr['openbao']['auth']['role']
|
||||||
|
external=next(x for x in docs if x['kind']=='ExternalSecret')
|
||||||
|
assert external['spec']['data']==[{'secretKey':'seed','remoteRef':{'key':'workloads/flex-auth/envelope-signing','property':'ED25519_PRIVATE_KEY'}}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_native_scope_separates_read_grant_and_unadmitted_execution():
|
||||||
|
plan=json.loads((ROOT/'docs/credential-lane-designs/flex-auth-envelope-signing-native-plan.json').read_text())
|
||||||
|
assert plan['authority_granted'] is False and plan['status']=='prepared-unapproved'
|
||||||
|
assert plan['bootstrap']['expected_version']==0
|
||||||
|
assert all(x['disposition'].startswith('pending ') for x in plan['reviews_required'])
|
||||||
|
assert 'implementation/independent tests still required' in plan['protected_adapter']['state']
|
||||||
|
assert plan['receipts']['maximum_bytes']==65536
|
||||||
|
assert all('platform/' in x['path'] for x in plan['bootstrap']['allowed_native_methods'])
|
||||||
|
assert 'zero flex-auth PVCs' in plan['requested_scope']['new_resources']
|
||||||
|
|
@ -1344,3 +1344,34 @@ admission; RPF-WP-0035-T12 remains progress.
|
||||||
R3 source metadata correction aligns proposal IDs across JSON/docs/Secret
|
R3 source metadata correction aligns proposal IDs across JSON/docs/Secret
|
||||||
annotation and documents existing base64url-ASCII public fingerprint hashing.
|
annotation and documents existing base64url-ASCII public fingerprint hashing.
|
||||||
Frozen R2 source/evidence is preserved; no writer behavior or admission changes.
|
Frozen R2 source/evidence is preserved; no writer behavior or admission changes.
|
||||||
|
|
||||||
|
2026-10-16 T12 native signer preparation: proposed CCR-2026-0050 and generated
|
||||||
|
read-only HCL pass real schema checks; current proposed status/unconfirmed claims
|
||||||
|
keep apply refused. Native candidate uses existing external-secrets-* role
|
||||||
|
convention with dedicated SA unchanged and projected private field only. Exact
|
||||||
|
attended CAS0/metadata/fence/receipt/forward-recovery action plan is separate from
|
||||||
|
read-grant authority; production adapter implementation/independent tests and
|
||||||
|
founder plus distinct owner reviews remain necessary. Access observed zero PVCs;
|
||||||
|
new outbox storage/retention/runtime bounds remain separate Access/cluster scope.
|
||||||
|
No live credentials, grants, roles, key or deployment changed. T12 stays progress.
|
||||||
|
|
||||||
|
2026-10-16 T12 native signer adapter source: silent fixed-scope attended child
|
||||||
|
checks exact independent admission/source/CCR/image/caller/trust/identity inputs,
|
||||||
|
actual backend/reader role/policy, signer fence and private exclusive receipt
|
||||||
|
custody before fixed metadata/CAS0 effects. Separate fresh read-only recovery
|
||||||
|
reconciles real process loss after native write without remint/retry/new effects.
|
||||||
|
Disposable tests exercise actual KV while owner reviews/identity/fence facts are
|
||||||
|
explicitly synthetic. Current warden plan dispatch selects structure-survey, not
|
||||||
|
OIDC admin login; mismatch remains a live refusal. Canonical proposed CCR, native
|
||||||
|
owner/founder approval and Access/cluster/outbox/PVC gates stay open; no live
|
||||||
|
key, credential read, grant, role, storage or deployment action performed.
|
||||||
|
|
||||||
|
2026-10-16 T12 native signer R2 source correction preserves R1 failures: exact
|
||||||
|
phase-aware read-only recovery classifies current metadata-only/absent states,
|
||||||
|
never admits a key or retry and preserves original uncertainty. Fresh recovery
|
||||||
|
explicitly binds historical execution plus current source; current primitive
|
||||||
|
plan/source/receipt join is enforced and old pin labeled historical. Real fork
|
||||||
|
metadata-loss and denied/ambiguous tests added.120second expiry is unchanged;
|
||||||
|
external reviews and unbounded OIDC require a pre-reviewed exact scheduled
|
||||||
|
window, otherwise refuse/new explicit revision. No native or owner approval
|
||||||
|
inferred; no live action.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue