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-listen-address=127.0.0.1:'+str(admission.port) not in process.args):
|
||||
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)
|
||||
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
|
||||
|
|
@ -66,7 +70,7 @@ class SignerWriter:
|
|||
raise Refused('exact signer operation contract required')
|
||||
if directory.absolute()!=directory.resolve() or directory.is_symlink():raise Refused('indirect receipt custody refused')
|
||||
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.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(),
|
||||
|
|
|
|||
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
|
||||
annotation and documents existing base64url-ASCII public fingerprint hashing.
|
||||
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