docs: preserve reviewed NetKingdom session artifacts
This commit is contained in:
parent
e19bcd1f3d
commit
b77ad96bd5
41 changed files with 4523 additions and 0 deletions
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"schema": "access-engine.service-http-join-clarification.v1",
|
||||
"prior_design": "service-http-join-proposal-r1.json",
|
||||
"superseded_item": "B3 text permitting server-refreshed Approval claim replacement inside evaluated binding.context",
|
||||
"reason": "Strict Secrets R2 requires exact original submitted request.context; a fresh Approval observation timestamp would fail correspondence even if underlying act is unchanged. That refusal must remain.",
|
||||
"B1": "Retain full original context/submitted digest unchanged and signed DENY; no provider fetch or claim replacement.",
|
||||
"B2_B3_preferred_review_direction": "Fetch authenticated fresh Approval fact out of band, verify exact original carried claim act/native/pdp/human-control/status/consumed/validity correspondence under Approval-owned freshness contract, keep original context/digests untouched. Bind authoritative fresh receipt/source/time in separately reviewed producer-owned signed provenance/diagnostics. Do not rewrite claim, compare native to PDP digest, invent authenticated booleans or weaken context equality.",
|
||||
"implementation_gate": "Approval owner must independently review exact typed fact interface, freshness correspondence, origin/channel admission and signed evidence shape before B2; B3 additionally requires full source join and exact pilot admission. No B2/B3 implementation authorized now."
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
{
|
||||
"schema": "access-engine.service-http-join-source-proposal.v1",
|
||||
"work_item": "FLEX-WP-0024-T06 with SECRETS-WP-0013/RPF-WP-0035-T12 coordination",
|
||||
"state": "source design for coordinator/owner review before implementation; no policy code or false-gate change in current patch",
|
||||
"problem": "Current /v1/check authenticates protected-system representation only and sends caller-supplied CheckRequest directly into Engine.Check. ServicePilotInputs.Prepare authenticates exact subject mapping but has no HTTP caller. Current source service policy correctly returns DENY. Signing authenticates Access output, not the provenance of caller-supplied Approval facts.",
|
||||
"reuse": {
|
||||
"caller": "Existing callerauth KubernetesTokenReviewer and ServicePilotInputs: actual TokenReview principal/audience/expiry, fixed service mapping and canonical detached request; no second human initiator auth for service pilot.",
|
||||
"approval": "Existing Approval Engine GET /v1/approvals/{id}/claim fact contract. Approval valid_now plus declared binding.human_control true carries owner-verified distinct human approver requirement; do not duplicate KeyCape human token verification in Access.",
|
||||
"request_digest": "Existing api.NewDecisionBindingFor, api.ApprovalBindingDigest and registry normalization remain authoritative; no new native Approval digest algorithm.",
|
||||
"consumer_effect": "Existing Secrets/Platform strict handoff and original-consumption confirmation/journal remain authoritative; Access never consumes approval or invokes backend effect.",
|
||||
"transport": "Owner-admitted exact Approval origin and existing authenticated channel/TLS mechanisms. No caller-selected URL/key/source, no same-channel public trust bootstrap and no unsigned identity flag."
|
||||
},
|
||||
"minimal_slices": [
|
||||
{
|
||||
"id": "B1",
|
||||
"purpose": "Wire actual exact service caller on HTTP while retaining signed DENY",
|
||||
"expected_files": [
|
||||
"cmd/access-engine/main.go",
|
||||
"cmd/access-engine/main_test.go",
|
||||
"internal/taskexecution/service_input.go",
|
||||
"internal/taskexecution/service_input_test.go"
|
||||
],
|
||||
"change": "For exact execute-ccr pilot requests on an owner-configured service-task pin, invoke ServicePilotInputs.Prepare using installed TokenReviewer instead of treating generic system authorization as subject authentication. Reuse one review result, not two parallel auth claims. Carry returned immutable Caller via callerauth.WithRecord and detached submitted request. Keep lifecycle path unchanged and pilot runtime_admission false. Missing service adapter/config refuses, never falls through to generic Check.",
|
||||
"checks": [
|
||||
"Actual disposable HTTP with controlled TokenReview endpoint verifies correct caller produces signed DENY",
|
||||
"Missing/foreign/expired token, wrong subject/type/tenant, public binding spoof/extra members, human/delegation flags, unknown transport expiry and operation deadline after token expiry refuse before evaluation",
|
||||
"Warn/disabled mode cannot instantiate task path; explicit retained flex-auth audience; source migration alias behavior tested deliberately",
|
||||
"Task BatchCheck/ListAllowed/CLI generic entrypoints cannot bypass dedicated service-input path"
|
||||
],
|
||||
"gate": "No eligibility/allow or backend effect; this first slice alone never completes integration."
|
||||
},
|
||||
{
|
||||
"id": "B2",
|
||||
"purpose": "Add a thin typed Approval fact reader with owner golden vectors, still DENY",
|
||||
"expected_files": [
|
||||
"internal/taskexecution/approval_fact.go",
|
||||
"internal/taskexecution/approval_fact_test.go",
|
||||
"docs/service-task-execution-contract.md"
|
||||
],
|
||||
"change": "Introduce server-installed ApprovalFactSource interface whose implementation reads only the fixed published claim endpoint using an admitted authenticated origin and least-scope read identity. No Go SDK exists in Approval owner source today; agree minimal Go transport/typed fact-validation contract with Approval owner before copying logic. Keep response bounds, deadlines, no redirects/proxies, exact id, schema/issuer/status/validity/freshness/human_control and fail-closed malformed/unavailable behavior. Never invoke Python SDK as a credential-carrying subprocess or create a new runtime.",
|
||||
"checks": [
|
||||
"Run real disposable Approval API with synthetic identity over owner-admitted local channel; compare owner-produced claim vectors",
|
||||
"Refuse foreign origin/issuer/id, stale/future/consumed/non-human-control claim, oversized/duplicate/malformed JSON, redirects, body drift and unavailable provider",
|
||||
"Owner contract confirms binding.human_control plus valid_now guarantees verified human approver count; requester service is never relabeled as human approver"
|
||||
],
|
||||
"gate": "No new Approval authority, schema, signature algorithm or policy grant; hard DENY remains."
|
||||
},
|
||||
{
|
||||
"id": "B3",
|
||||
"purpose": "Join server-owned facts into one existing PDP evaluation under reviewed eligibility successor",
|
||||
"expected_files": [
|
||||
"internal/decision/engine.go",
|
||||
"internal/taskexecution/service_input.go",
|
||||
"cmd/access-engine/main.go",
|
||||
"examples/secrets-engine-service-task-execution/policy_package.md"
|
||||
],
|
||||
"change": "Use an unforgeable-by-HTTP typed service/fact input to the existing decision engine. Reject task action via generic Check/Batch/List/CLI paths; specialized entry validates immutable source evidence, normalizes with existing registry, compares served approval act fields and approval-free PDP digest to normalized request, then uses the single existing policy evaluator. Preserve exact submitted request digest separately from evaluated server-refreshed Approval fact. Retain caller/provider provenance and policy/registry/source versions in signed record. Remove the structural false gate only in an independently reviewed complete successor; never expose a runtime enable boolean or a request context verified flag.",
|
||||
"checks": [
|
||||
"Preapproval may reuse a signed denied /v1/check binding for exact approval-free digest; it grants no effect. Owner normalization/registry drift between issue and use must refuse or require fresh approval",
|
||||
"Source-fetched claim may refresh its freshness object; submitted_request_digest still binds unchanged original request while evaluated request_digest binds owner-fetched facts; compare approval-free pdp_digest without rewriting historical claims",
|
||||
"Exact action/actor/principal/purpose/resource fields and human_control must match; consumed state refused before decision; all task lifetime bounds no later than caller/approval/operation expiry",
|
||||
"Separate source policy retains ordinary lifecycle behavior; single-package-per-process deployment choice must be explicit and cannot quietly replace existing lifecycle pin",
|
||||
"Consumer obtains exact decision then performs original single-use CAS, Platform independently confirms original triplet and executes journal-bound six-call metadata operation; Access performs no consume/effect"
|
||||
],
|
||||
"gate": "Blocked until Approval source/channel, exact descriptor ownership and full eligibility implementation are independently reviewed; future native deployment/policy admission remains separate."
|
||||
}
|
||||
],
|
||||
"nonchanges_now": [
|
||||
"No implementation of B1/B2/B3 in this source patch",
|
||||
"No edits to current human/task policy or literal runtime_admission false",
|
||||
"No new broker, runtime, namespace, grants or credentials",
|
||||
"No universal human initiating identity requirement for admitted service pilot",
|
||||
"No approval consume from Access and no consumer policy evaluation"
|
||||
],
|
||||
"approval_contract_source_evidence": [
|
||||
"approval-engine/docs/approval-claim.md",
|
||||
"approval-engine/approval_engine/store.py claim/_evaluate",
|
||||
"approval-engine/approval_engine/pep.py ApprovalHTTPClient.claim",
|
||||
"access-engine/docs/service-task-execution-contract.md",
|
||||
"access-engine/internal/taskexecution/service_input.go",
|
||||
"access-engine/cmd/access-engine/main.go newServeMuxWithCallerAuth"
|
||||
],
|
||||
"review_questions_for_owners": [
|
||||
"Approval owner: minimal Go claim-validation vector contract and authenticated-origin admission, including exact source assertion of verified human-control count",
|
||||
"Access owner: exact existing pilot source pin/package selection and generic-entrypoint guard before provider wiring",
|
||||
"Secrets/Platform owners: enriched/approval-free digest preparation and exact immutable descriptor/custody source correspondences; no request relabeling or claim rewrite"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
diff --git a/tools/verify_native_signer_receipt.py b/tools/verify_native_signer_receipt.py
|
||||
--- a/tools/verify_native_signer_receipt.py
|
||||
+++ b/tools/verify_native_signer_receipt.py
|
||||
@@ -25,10 +25,22 @@
|
||||
target = Path(path)
|
||||
if not target.is_absolute():
|
||||
raise ValueError('absolute input path required')
|
||||
- data = target.read_bytes()
|
||||
+ with target.open("rb") as stream:
|
||||
+ data = stream.read(524289)
|
||||
if len(data) > 524288:
|
||||
raise ValueError('receipt input too large')
|
||||
return data
|
||||
+
|
||||
+def same_json_types(left, right) -> bool:
|
||||
+ """JSON value correspondence only; never signature canonicalization."""
|
||||
+ if type(left) is not type(right):
|
||||
+ return False
|
||||
+ if isinstance(left, dict):
|
||||
+ return left.keys() == right.keys() and all(same_json_types(value,right[key]) for key,value in left.items())
|
||||
+ if isinstance(left, list):
|
||||
+ return len(left) == len(right) and all(same_json_types(a,b) for a,b in zip(left,right))
|
||||
+ return left == right
|
||||
+
|
||||
|
||||
def stamp(value: str) -> datetime:
|
||||
parsed = datetime.fromisoformat(value.replace('Z','+00:00'))
|
||||
@@ -46,20 +58,25 @@
|
||||
executable = Path(config['verifier'])
|
||||
if not executable.is_absolute() or not re.fullmatch('[a-f0-9]{64}',config['verifier_sha256']) or hashlib.sha256(executable.read_bytes()).hexdigest() != config['verifier_sha256']:
|
||||
raise ValueError('installed verifier digest mismatch')
|
||||
+ trust_bytes = read(config['trust_file'])
|
||||
trust = load_trust(Path(config['trust_file']))
|
||||
+ if not same_json_types(trust,strict_json(trust_bytes)):
|
||||
+ raise ValueError('trust input changed during verification')
|
||||
if (trust['evaluator'],trust['mode'],trust['policy_package'],trust['policy_version']) != ('flex-auth/local','standalone','secrets-engine.catalog-lane.lifecycle','v2'):
|
||||
raise ValueError('signer-only lifecycle package required')
|
||||
checks = {}
|
||||
envelopes = {}
|
||||
inputs = {}
|
||||
+ requests = {}
|
||||
for label in ('allow','deny'):
|
||||
- body = read(config[label+'_envelope'])
|
||||
+ blobs = {suffix:read(config[label+'_'+suffix]) for suffix in ('request','envelope','record')}
|
||||
+ body = blobs['envelope']
|
||||
# Strict decode before helper; authenticated typed return alone is acted on.
|
||||
strict_json(body.decode('utf-8'))
|
||||
envelope = authenticate_response(body,trust=trust,verifier=executable)
|
||||
- request = strict_json(read(config[label+'_request']))
|
||||
- record = strict_json(read(config[label+'_record']))
|
||||
- if record != envelope:
|
||||
+ request = strict_json(blobs['request'])
|
||||
+ record = strict_json(blobs['record'])
|
||||
+ if not same_json_types(record,envelope):
|
||||
raise ValueError('captured original source record differs from authenticated response')
|
||||
provenance = envelope['provenance']['caller']
|
||||
if any(provenance.get(key) != value for key,value in caller.items()) or stamp(provenance['not_after']) <= current:
|
||||
@@ -70,8 +87,9 @@
|
||||
if envelope['effect'] != 'deny' or envelope['binding']['submitted_request_digest'] != request_digest(request):
|
||||
raise ValueError('deny receipt does not bind exact negative request')
|
||||
envelopes[label] = envelope
|
||||
+ requests[label] = request
|
||||
for suffix in ('request','envelope','record'):
|
||||
- inputs[label+'_'+suffix] = hashlib.sha256(read(config[label+'_'+suffix])).hexdigest()
|
||||
+ inputs[label+'_'+suffix] = hashlib.sha256(blobs[suffix]).hexdigest()
|
||||
checks[label+'_authenticated_bound_and_record_matched'] = True
|
||||
# These are local refusals against the captured real producer envelope. They
|
||||
# do not claim a rejected caller was exercised on the native HTTP endpoint.
|
||||
@@ -89,13 +107,13 @@
|
||||
raise ValueError('negative authentication case accepted')
|
||||
try:
|
||||
expiry=stamp(envelopes['allow']['lifetime']['expires_at'])
|
||||
- request=strict_json(read(config['allow_request']))
|
||||
+ request=requests['allow']
|
||||
validate_decision_envelope(envelopes['allow'],request,accepted_policy_packages={trust['policy_package']},accepted_policy_versions={trust['policy_version']},now=expiry)
|
||||
except DecisionError:
|
||||
checks['expiry_boundary_refused'] = True
|
||||
else:
|
||||
raise ValueError('expired allow accepted')
|
||||
- return {'schema':'access-engine.signer-receipt-result.v1','checks':checks,'input_sha256':inputs,'verifier_sha256':config['verifier_sha256'],'trust_file_sha256':hashlib.sha256(read(config['trust_file'])).hexdigest(),'boundary':'offline captured signed receipt checks only; no native caller-denial, record durability, receiver custody, trust admission, rotation or protected effect inferred'}
|
||||
+ return {'schema':'access-engine.signer-receipt-result.v1','checks':checks,'input_sha256':inputs,'verifier_sha256':config['verifier_sha256'],'trust_file_sha256':hashlib.sha256(trust_bytes).hexdigest(),'boundary':'offline captured signed receipt checks only; no native caller-denial, record durability, receiver custody, trust admission, rotation or protected effect inferred'}
|
||||
|
||||
def main() -> int:
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
diff --git a/tests/test_native_signer_receipt.py b/tests/test_native_signer_receipt.py
|
||||
--- a/tests/test_native_signer_receipt.py
|
||||
+++ b/tests/test_native_signer_receipt.py
|
||||
@@ -9,6 +9,7 @@
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
+from unittest import mock
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
@unittest.skipUnless(os.environ.get('SECRETS_ENGINE_SOURCE_ROOT'), 'requires reviewed Secrets consumer source/bundle for integration checks')
|
||||
@@ -30,6 +31,7 @@
|
||||
env['lifetime']['not_before']=(cls.now-timedelta(seconds=1)).isoformat();env['lifetime']['expires_at']=(cls.now+timedelta(minutes=15)).isoformat()
|
||||
def sign(envelope):
|
||||
return json.loads(subprocess.check_output([str(cls.signer)],input=json.dumps(envelope).encode()))
|
||||
+ env['diagnostics']['target']={'phase':{'uses':1}}
|
||||
cls.allow=sign(env)
|
||||
req=json.loads((ROOT/'examples/secrets-engine/check_request_allow_rotate.json').read_text())
|
||||
(cls.base/'allow-request.json').write_text(json.dumps(req))
|
||||
@@ -53,6 +55,36 @@
|
||||
def test_changed_source_record_refuses(self):
|
||||
record=copy.deepcopy(self.allow);record['effect']='deny';(self.base/'allow-record.json').write_text(json.dumps(record))
|
||||
with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now)
|
||||
+ def test_nested_record_number_type_change_refuses(self):
|
||||
+ for replacement in (True,1.0):
|
||||
+ with self.subTest(replacement_type=type(replacement).__name__):
|
||||
+ record=copy.deepcopy(self.allow)
|
||||
+ record['diagnostics']['target']['phase']['uses']=replacement
|
||||
+ (self.base/'allow-record.json').write_text(json.dumps(record))
|
||||
+ with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now)
|
||||
+
|
||||
+ def test_receipt_hashes_use_the_once_read_verified_bytes(self):
|
||||
+ actual_read=MODULE.read
|
||||
+ seen={}
|
||||
+ original={str(self.base/f'{label}-{suffix}.json'):actual_read(str(self.base/f'{label}-{suffix}.json')) for label in ('allow','deny') for suffix in ('request','envelope','record')}
|
||||
+ def once(path):
|
||||
+ if path in original:
|
||||
+ seen[path]=seen.get(path,0)+1
|
||||
+ self.assertEqual(seen[path],1,'receipt was reopened after verification')
|
||||
+ return actual_read(path)
|
||||
+ with mock.patch.object(MODULE,'read',side_effect=once):
|
||||
+ result=MODULE.verify(self.config,now=self.now)
|
||||
+ self.assertEqual(len(seen),6)
|
||||
+ for label in ('allow','deny'):
|
||||
+ for suffix in ('request','envelope','record'):
|
||||
+ self.assertEqual(result['input_sha256'][label+'_'+suffix],hashlib.sha256(original[str(self.base/f'{label}-{suffix}.json')]).hexdigest())
|
||||
+
|
||||
+ def test_oversized_file_read_is_bounded(self):
|
||||
+ stream=mock.MagicMock();stream.__enter__.return_value=stream;stream.read.return_value=b'x'*524289
|
||||
+ with mock.patch.object(Path,'open',return_value=stream):
|
||||
+ with self.assertRaises(ValueError):MODULE.read('/absolute/oversized.json')
|
||||
+ stream.read.assert_called_once_with(524289)
|
||||
+
|
||||
def test_unadmitted_helper_digest_refuses(self):
|
||||
self.config['verifier_sha256']='0'*64
|
||||
with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now)
|
||||
|
|
@ -0,0 +1,392 @@
|
|||
diff --git a/tenancy.yaml b/tenancy.yaml
|
||||
--- a/tenancy.yaml
|
||||
+++ b/tenancy.yaml
|
||||
@@ -11,8 +11,16 @@
|
||||
workload_identity:
|
||||
name: flex-auth
|
||||
kind: platform-service
|
||||
- responsible_repo: flex-auth
|
||||
+ responsible_repo: access-engine
|
||||
identity_bindings:
|
||||
+ # Existing retained Secrets PDP pin; identity source, not key admission.
|
||||
+ - scheme: kubernetes-service-account
|
||||
+ authority: railiance01
|
||||
+ subject: system:serviceaccount:flex-auth:flex-auth-secrets-engine
|
||||
+ principal_type: service
|
||||
+ environment: prod
|
||||
+ evidence:
|
||||
+ - docs/native-signer-workload.json
|
||||
- scheme: kubernetes-service-account
|
||||
authority: railiance01
|
||||
subject: system:serviceaccount:flex-auth:flex-auth-ops-warden
|
||||
diff --git a/values/secrets-engine.yaml b/values/secrets-engine.yaml
|
||||
--- a/values/secrets-engine.yaml
|
||||
+++ b/values/secrets-engine.yaml
|
||||
@@ -13,6 +13,8 @@
|
||||
- /opt/flex-auth/examples/secrets-engine/policy_package.md
|
||||
callerAuth:
|
||||
mode: enforce
|
||||
+ # Retained runtime audience is explicit; signer preparation does not rename it.
|
||||
+ audience: flex-auth
|
||||
kubernetesURL: https://10.43.0.1
|
||||
binding: secrets-engine=system:serviceaccount:secrets-engine:secrets-engine
|
||||
consumer:
|
||||
diff --git a/charts/flex-auth/templates/deployment.yaml b/charts/flex-auth/templates/deployment.yaml
|
||||
--- a/charts/flex-auth/templates/deployment.yaml
|
||||
+++ b/charts/flex-auth/templates/deployment.yaml
|
||||
@@ -83,6 +83,8 @@
|
||||
{{- if not (and .Values.readback .Values.readback.enabled) }}
|
||||
{{- $binding = required "callerAuth.binding is required when callerAuth.mode is warn or enforce" .Values.callerAuth.binding }}
|
||||
{{- end }}
|
||||
+ - --caller-audience
|
||||
+ - {{ required "callerAuth.audience is required" .Values.callerAuth.audience | quote }}
|
||||
- --caller-auth-mode
|
||||
- {{ include "flex-auth.callerAuth.mode" . | quote }}
|
||||
- --caller-kubernetes-url
|
||||
diff --git a/.forgejo/workflows/image.yaml b/.forgejo/workflows/image.yaml
|
||||
--- a/.forgejo/workflows/image.yaml
|
||||
+++ b/.forgejo/workflows/image.yaml
|
||||
@@ -51,7 +51,7 @@
|
||||
SHORT="${REF:0:7}"
|
||||
mkdir -p buildctx "${HOME}/bin"
|
||||
wget -qO /tmp/repo.tar.gz \
|
||||
- "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${SHORT}.tar.gz"
|
||||
+ "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${REF}.tar.gz"
|
||||
tar xzf /tmp/repo.tar.gz -C buildctx --strip-components=1
|
||||
wget -qO- https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz \
|
||||
| tar xz --strip-components=1 -C "${HOME}/bin" docker/docker
|
||||
diff --git a/tools/verify_native_signer_receipt.py b/tools/verify_native_signer_receipt.py
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/tools/verify_native_signer_receipt.py
|
||||
@@ -0,0 +1,117 @@
|
||||
+#!/usr/bin/env python3
|
||||
+"""Verify captured native decision receipts with the installed Secrets verifier.
|
||||
+
|
||||
+Offline only: no token input, network request, key generation, build or effect.
|
||||
+Use an independently reviewed admission file; success never admits that file.
|
||||
+"""
|
||||
+from __future__ import annotations
|
||||
+import argparse
|
||||
+import copy
|
||||
+from datetime import datetime, timezone
|
||||
+import hashlib
|
||||
+import json
|
||||
+from pathlib import Path
|
||||
+import re
|
||||
+
|
||||
+# These are the consumer's existing verifier/binding functions, not another PDP
|
||||
+# or signature/canonicalization implementation. Install the reviewed bundle first.
|
||||
+from secrets_engine.authorization import request_digest, validate_decision_envelope
|
||||
+from secrets_engine.decision_signature import authenticate_response, load_trust, strict_json
|
||||
+from secrets_engine.errors import DecisionError
|
||||
+
|
||||
+FIELDS = {'schema','trust_file','verifier','verifier_sha256','allow_request','allow_envelope','allow_record','deny_request','deny_envelope','deny_record','expected_caller'}
|
||||
+
|
||||
+def read(path: str) -> bytes:
|
||||
+ target = Path(path)
|
||||
+ if not target.is_absolute():
|
||||
+ raise ValueError('absolute input path required')
|
||||
+ data = target.read_bytes()
|
||||
+ if len(data) > 524288:
|
||||
+ raise ValueError('receipt input too large')
|
||||
+ return data
|
||||
+
|
||||
+def stamp(value: str) -> datetime:
|
||||
+ parsed = datetime.fromisoformat(value.replace('Z','+00:00'))
|
||||
+ if parsed.tzinfo is None:
|
||||
+ raise ValueError('timezone required')
|
||||
+ return parsed.astimezone(timezone.utc)
|
||||
+
|
||||
+def verify(config: dict, *, now: datetime | None = None) -> dict:
|
||||
+ if not isinstance(config,dict) or set(config) != FIELDS or config['schema'] != 'access-engine.signer-receipt-check.v1':
|
||||
+ raise ValueError('unsupported admission fields')
|
||||
+ current = now or datetime.now(timezone.utc)
|
||||
+ caller = config['expected_caller']
|
||||
+ if caller != {'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine','audience':'flex-auth'}:
|
||||
+ raise ValueError('retained native caller admission required')
|
||||
+ executable = Path(config['verifier'])
|
||||
+ if not executable.is_absolute() or not re.fullmatch('[a-f0-9]{64}',config['verifier_sha256']) or hashlib.sha256(executable.read_bytes()).hexdigest() != config['verifier_sha256']:
|
||||
+ raise ValueError('installed verifier digest mismatch')
|
||||
+ trust = load_trust(Path(config['trust_file']))
|
||||
+ if (trust['evaluator'],trust['mode'],trust['policy_package'],trust['policy_version']) != ('flex-auth/local','standalone','secrets-engine.catalog-lane.lifecycle','v2'):
|
||||
+ raise ValueError('signer-only lifecycle package required')
|
||||
+ checks = {}
|
||||
+ envelopes = {}
|
||||
+ inputs = {}
|
||||
+ for label in ('allow','deny'):
|
||||
+ body = read(config[label+'_envelope'])
|
||||
+ # Strict decode before helper; authenticated typed return alone is acted on.
|
||||
+ strict_json(body.decode('utf-8'))
|
||||
+ envelope = authenticate_response(body,trust=trust,verifier=executable)
|
||||
+ request = strict_json(read(config[label+'_request']))
|
||||
+ record = strict_json(read(config[label+'_record']))
|
||||
+ if record != envelope:
|
||||
+ raise ValueError('captured original source record differs from authenticated response')
|
||||
+ provenance = envelope['provenance']['caller']
|
||||
+ if any(provenance.get(key) != value for key,value in caller.items()) or stamp(provenance['not_after']) <= current:
|
||||
+ raise ValueError('native caller provenance missing or expired')
|
||||
+ if label == 'allow':
|
||||
+ validate_decision_envelope(envelope,request,accepted_policy_packages={trust['policy_package']},accepted_policy_versions={trust['policy_version']},now=current)
|
||||
+ else:
|
||||
+ if envelope['effect'] != 'deny' or envelope['binding']['submitted_request_digest'] != request_digest(request):
|
||||
+ raise ValueError('deny receipt does not bind exact negative request')
|
||||
+ envelopes[label] = envelope
|
||||
+ for suffix in ('request','envelope','record'):
|
||||
+ inputs[label+'_'+suffix] = hashlib.sha256(read(config[label+'_'+suffix])).hexdigest()
|
||||
+ checks[label+'_authenticated_bound_and_record_matched'] = True
|
||||
+ # These are local refusals against the captured real producer envelope. They
|
||||
+ # do not claim a rejected caller was exercised on the native HTTP endpoint.
|
||||
+ for label in ('altered','unsigned','unknown_kid','wrong_policy_pin'):
|
||||
+ bad = copy.deepcopy(envelopes['allow']); pins = copy.deepcopy(trust)
|
||||
+ if label == 'altered': bad['binding']['action'] = 'forged-action'
|
||||
+ elif label == 'unsigned': bad['signature'] = {'mode':'unsigned'}
|
||||
+ elif label == 'unknown_kid': bad['signature']['kid'] = 'unadmitted-key'
|
||||
+ else: pins['policy_package_digest'] = 'sha256:'+'0'*64
|
||||
+ try:
|
||||
+ authenticate_response(json.dumps(bad).encode(),trust=pins,verifier=executable)
|
||||
+ except DecisionError:
|
||||
+ checks[label+'_refused'] = True
|
||||
+ else:
|
||||
+ raise ValueError('negative authentication case accepted')
|
||||
+ try:
|
||||
+ expiry=stamp(envelopes['allow']['lifetime']['expires_at'])
|
||||
+ request=strict_json(read(config['allow_request']))
|
||||
+ validate_decision_envelope(envelopes['allow'],request,accepted_policy_packages={trust['policy_package']},accepted_policy_versions={trust['policy_version']},now=expiry)
|
||||
+ except DecisionError:
|
||||
+ checks['expiry_boundary_refused'] = True
|
||||
+ else:
|
||||
+ raise ValueError('expired allow accepted')
|
||||
+ return {'schema':'access-engine.signer-receipt-result.v1','checks':checks,'input_sha256':inputs,'verifier_sha256':config['verifier_sha256'],'trust_file_sha256':hashlib.sha256(read(config['trust_file'])).hexdigest(),'boundary':'offline captured signed receipt checks only; no native caller-denial, record durability, receiver custody, trust admission, rotation or protected effect inferred'}
|
||||
+
|
||||
+def main() -> int:
|
||||
+ parser=argparse.ArgumentParser(description=__doc__)
|
||||
+ parser.add_argument('--admission',type=Path,required=True)
|
||||
+ parser.add_argument('--output',type=Path,required=True)
|
||||
+ args=parser.parse_args()
|
||||
+ try:
|
||||
+ config=strict_json(read(str(args.admission)))
|
||||
+ result=verify(config)
|
||||
+ args.output.write_text(json.dumps(result,indent=2)+'\n')
|
||||
+ except (OSError,ValueError,TypeError,KeyError,RecursionError,DecisionError):
|
||||
+ # Never dump source objects, unexpected key material or verifier stderr.
|
||||
+ print('signer receipt verification refused')
|
||||
+ return 1
|
||||
+ print('captured signer receipt checks passed')
|
||||
+ return 0
|
||||
+
|
||||
+if __name__ == '__main__':
|
||||
+ raise SystemExit(main())
|
||||
diff --git a/tests/test_native_signer_receipt.py b/tests/test_native_signer_receipt.py
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/tests/test_native_signer_receipt.py
|
||||
@@ -0,0 +1,68 @@
|
||||
+"""Offline tool regression using the producer's existing synthetic signer only."""
|
||||
+import copy
|
||||
+from datetime import datetime,timedelta,timezone
|
||||
+import hashlib
|
||||
+import importlib.util
|
||||
+import json
|
||||
+import os
|
||||
+from pathlib import Path
|
||||
+import subprocess
|
||||
+import tempfile
|
||||
+import unittest
|
||||
+
|
||||
+ROOT=Path(__file__).resolve().parents[1]
|
||||
+@unittest.skipUnless(os.environ.get('SECRETS_ENGINE_SOURCE_ROOT'), 'requires reviewed Secrets consumer source/bundle for integration checks')
|
||||
+class NativeSignerReceiptTests(unittest.TestCase):
|
||||
+ @classmethod
|
||||
+ def setUpClass(cls):
|
||||
+ global MODULE
|
||||
+ spec=importlib.util.spec_from_file_location('signer_receipt',ROOT/'tools/verify_native_signer_receipt.py')
|
||||
+ MODULE=importlib.util.module_from_spec(spec);spec.loader.exec_module(MODULE)
|
||||
+ consumer=Path(os.environ['SECRETS_ENGINE_SOURCE_ROOT'])
|
||||
+ cls.scratch=tempfile.TemporaryDirectory();cls.base=Path(cls.scratch.name)
|
||||
+ env={**os.environ,'GOFLAGS':'-buildvcs=false'}
|
||||
+ cls.verifier=cls.base/'verify';cls.signer=cls.base/'synthetic-sign'
|
||||
+ for binary,package in [(cls.verifier,'.'),(cls.signer,'./testsupport/signer')]:
|
||||
+ subprocess.run(['go','build','-p','2','-o',str(binary),package],cwd=consumer/'verifier',env=env,check=True)
|
||||
+ cls.now=datetime.now(timezone.utc)
|
||||
+ env=json.loads((consumer/'tests/fixtures/pdp-signature/decision_rotate_signed.json').read_text())
|
||||
+ env['provenance']['caller']={'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine','audience':'flex-auth','not_after':(cls.now+timedelta(minutes=20)).isoformat()}
|
||||
+ env['lifetime']['not_before']=(cls.now-timedelta(seconds=1)).isoformat();env['lifetime']['expires_at']=(cls.now+timedelta(minutes=15)).isoformat()
|
||||
+ def sign(envelope):
|
||||
+ return json.loads(subprocess.check_output([str(cls.signer)],input=json.dumps(envelope).encode()))
|
||||
+ cls.allow=sign(env)
|
||||
+ req=json.loads((ROOT/'examples/secrets-engine/check_request_allow_rotate.json').read_text())
|
||||
+ (cls.base/'allow-request.json').write_text(json.dumps(req))
|
||||
+ denied=copy.deepcopy(req);denied['action']='execute-ccr'
|
||||
+ cls.deny=copy.deepcopy(env);cls.deny['effect']='deny';cls.deny['binding']['action']='execute-ccr';cls.deny['binding']['submitted_request_digest']=MODULE.request_digest(denied);cls.deny=sign(cls.deny)
|
||||
+ (cls.base/'deny-request.json').write_text(json.dumps(denied))
|
||||
+ provenance=cls.allow['provenance'];cls.trust={key:provenance[key] for key in ('evaluator','mode','policy_package','policy_version','policy_package_digest')}
|
||||
+ cls.trust.update(schema='secrets-engine.pdp-trust.v1',keys={'testdata-ed25519':'IVL40Zt5HSRFMkLhXy6rbLfP-ntqXtMAl5YOBpiB2xI'})
|
||||
+ @classmethod
|
||||
+ def tearDownClass(cls):cls.scratch.cleanup()
|
||||
+ def setUp(self):
|
||||
+ for label,envelope in [('allow',self.allow),('deny',self.deny)]:
|
||||
+ for kind in ('envelope','record'):(self.base/f'{label}-{kind}.json').write_text(json.dumps(envelope))
|
||||
+ (self.base/'trust.json').write_text(json.dumps(self.trust))
|
||||
+ self.config={'schema':'access-engine.signer-receipt-check.v1','trust_file':str(self.base/'trust.json'),'verifier':str(self.verifier),'verifier_sha256':hashlib.sha256(self.verifier.read_bytes()).hexdigest(),'expected_caller':{'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine','audience':'flex-auth'}}
|
||||
+ for label in ('allow','deny'):
|
||||
+ for kind in ('request','envelope','record'):self.config[f'{label}_{kind}']=str(self.base/f'{label}-{kind}.json')
|
||||
+ def test_genuine_signed_receipt_and_refusal_matrix(self):
|
||||
+ result=MODULE.verify(self.config,now=self.now)
|
||||
+ self.assertTrue(all(result['checks'].values()));self.assertEqual(len(result['checks']),7)
|
||||
+ def test_changed_source_record_refuses(self):
|
||||
+ record=copy.deepcopy(self.allow);record['effect']='deny';(self.base/'allow-record.json').write_text(json.dumps(record))
|
||||
+ with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now)
|
||||
+ def test_unadmitted_helper_digest_refuses(self):
|
||||
+ self.config['verifier_sha256']='0'*64
|
||||
+ with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now)
|
||||
+ def test_disabled_caller_admission_refuses(self):
|
||||
+ self.config['expected_caller']['mode']='disabled'
|
||||
+ with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now)
|
||||
+ def test_expired_captured_allow_refuses(self):
|
||||
+ with self.assertRaises(MODULE.DecisionError):MODULE.verify(self.config,now=self.now+timedelta(minutes=16))
|
||||
+ def test_wrong_independent_policy_pin_refuses(self):
|
||||
+ wrong=copy.deepcopy(self.trust);wrong['policy_package_digest']='sha256:'+'0'*64;(self.base/'trust.json').write_text(json.dumps(wrong))
|
||||
+ with self.assertRaises(MODULE.DecisionError):MODULE.verify(self.config,now=self.now)
|
||||
+
|
||||
+if __name__=='__main__':unittest.main()
|
||||
diff --git a/docs/native-signer-workload.json b/docs/native-signer-workload.json
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/docs/native-signer-workload.json
|
||||
@@ -0,0 +1,68 @@
|
||||
+{
|
||||
+ "schema": "access-engine.signer-workload-reference.v1",
|
||||
+ "owner_repo": "access-engine",
|
||||
+ "existing_workload": "flex-auth",
|
||||
+ "authoritative_owner_declaration": "tenancy.yaml#/workload_identity",
|
||||
+ "exact_signer_identity_binding": "system:serviceaccount:flex-auth:flex-auth-secrets-engine",
|
||||
+ "declaration_scope": "Existing retained Secrets lifecycle PDP pin; declaration of identity source only. Not a new managed workload, credential grant, zone promotion or live admission.",
|
||||
+ "observed_runtime": {
|
||||
+ "cluster": "railiance01",
|
||||
+ "node_internal_ip": "92.205.62.239",
|
||||
+ "namespace": "flex-auth",
|
||||
+ "release": "flex-auth-secrets-engine",
|
||||
+ "deployment": "flex-auth-secrets-engine",
|
||||
+ "reviewer_service_account": "flex-auth-secrets-engine",
|
||||
+ "source_evidence": "the-custodian/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/live-metadata-r1.json",
|
||||
+ "evidence_sha256": "bafce991e02083ac30c8d6ce15f037dc22087bbb5dc8185c472149b46bca47c2"
|
||||
+ },
|
||||
+ "identity_roles": {
|
||||
+ "signer_workload": "system:serviceaccount:flex-auth:flex-auth-secrets-engine",
|
||||
+ "requesting_consumer": "system:serviceaccount:secrets-engine:secrets-engine",
|
||||
+ "retained_caller_audience": "flex-auth",
|
||||
+ "rule": "Signer custody binds the PDP workload, not the consumer or a human approver. Existing TokenReview reviewer projection does not grant signing-key custody."
|
||||
+ },
|
||||
+ "custody_reference": {
|
||||
+ "catalog_id": "flex-auth-decision-envelope-signing-key",
|
||||
+ "owner_repo": "railiance-platform",
|
||||
+ "current_state": "draft/nonresolvable",
|
||||
+ "proposed_source_path": "platform/workloads/flex-auth/envelope-signing",
|
||||
+ "proposed_private_field": "ED25519_PRIVATE_KEY",
|
||||
+ "required_encoding": "unpadded base64url 32-byte Ed25519 seed; mount0440 allowed, group-write and world permissions refused",
|
||||
+ "public_fields": [
|
||||
+ "ED25519_PUBLIC_KEY",
|
||||
+ "kid"
|
||||
+ ],
|
||||
+ "proposed_catalog_workload_reference": {
|
||||
+ "name": "flex-auth",
|
||||
+ "declaration_ref": "access-engine/tenancy.yaml#/workload_identity",
|
||||
+ "exact_binding": "system:serviceaccount:flex-auth:flex-auth-secrets-engine",
|
||||
+ "resolution": "unknown until owner custody and managed-package admission; do not mark resolvable merely from source binding"
|
||||
+ },
|
||||
+ "key_created_or_admitted": false
|
||||
+ },
|
||||
+ "managed_package_limit": {
|
||||
+ "rapp_declaration_ref": null,
|
||||
+ "status": "Existing managed-deployable migration debt remains unknown under security-zones_v0.1 \u00a76; no matching authoritative rapp declaration was established. Existing source tenancy declaration supplies owner identity binding but cannot substitute for managed-package admission.",
|
||||
+ "no_fiction": "Do not invent a rapp tuple or describe this Helm-managed Deployment as an independent operational unit to bypass the rule."
|
||||
+ },
|
||||
+ "unchanged_limits": [
|
||||
+ "tenancy and zones grades are unchanged",
|
||||
+ "service-pilot literal false gate unchanged",
|
||||
+ "human variant unchanged",
|
||||
+ "legacy twelve lifecycle actions remain the selected policy package",
|
||||
+ "no signer Secret name, kid, public fingerprint, custody receipt or rollout permission is supplied"
|
||||
+ ],
|
||||
+ "consumer_admission": "Secrets owner independently admits public keys/pins and exact installed verifier; this file never bootstraps trust from PDP discovery.",
|
||||
+ "native_tool": {
|
||||
+ "path": "tools/verify_native_signer_receipt.py",
|
||||
+ "requires": "Installed reviewed Secrets Engine package/helper and independent public admission; source request, authenticated response and original source record for one allow and one deny.",
|
||||
+ "operation": "Offline captured receipt checks only, no network/token input/key build or protected effect. Current clock validation; no CLI backdating option.",
|
||||
+ "result_limits": "Does not prove record durability, real rejected HTTP caller, audit receiver custody, rotation, or authorization to run effect."
|
||||
+ },
|
||||
+ "ci": {
|
||||
+ "trigger": "Existing ordinary push to main when .forgejo/workflows/image.yaml, Containerfile, cmd/**, internal/**, pkg/**, examples/**, go.mod/go.sum or tools/publish-runtime-images.sh changes; workflow_dispatch also exists. This patch changes the workflow, so an authorized push would trigger it.",
|
||||
+ "scope": "Existing container-build runner and already admitted forgejo.coulomb.social/coulomb/flex-auth package only in legacy mode; paired access-engine package publication requires separate existing dispatch authority and is excluded.",
|
||||
+ "provenance_preparation": "Download archive/${REF}.tar.gz using full validated GITHUB_SHA; existing publisher labels org.opencontainers.image.revision and reads back registry manifest digest.",
|
||||
+ "not_invoked": "No push, workflow dispatch, Docker build, image publication or paid resource allocation in this source preparation."
|
||||
+ }
|
||||
+}
|
||||
diff --git a/examples/secrets-engine/signer-receipt-admission.template.json b/examples/secrets-engine/signer-receipt-admission.template.json
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/examples/secrets-engine/signer-receipt-admission.template.json
|
||||
@@ -0,0 +1,17 @@
|
||||
+{
|
||||
+ "schema": "access-engine.signer-receipt-check.v1",
|
||||
+ "trust_file": "/absolute/owner-admitted/public-trust.json",
|
||||
+ "verifier": "/absolute/installed/secrets-engine-pdp-verify",
|
||||
+ "verifier_sha256": "REPLACE_WITH_INDEPENDENTLY_REVIEWED_INSTALLED_HELPER_SHA256",
|
||||
+ "allow_request": "/absolute/sanitized-native-receipts/allow-request.json",
|
||||
+ "allow_envelope": "/absolute/sanitized-native-receipts/allow-envelope.json",
|
||||
+ "allow_record": "/absolute/sanitized-native-receipts/allow-source-record.json",
|
||||
+ "deny_request": "/absolute/sanitized-native-receipts/deny-request.json",
|
||||
+ "deny_envelope": "/absolute/sanitized-native-receipts/deny-envelope.json",
|
||||
+ "deny_record": "/absolute/sanitized-native-receipts/deny-source-record.json",
|
||||
+ "expected_caller": {
|
||||
+ "mode": "enforce",
|
||||
+ "principal": "system:serviceaccount:secrets-engine:secrets-engine",
|
||||
+ "audience": "flex-auth"
|
||||
+ }
|
||||
+}
|
||||
diff --git a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
--- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
+++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
@@ -184,7 +184,7 @@
|
||||
|
||||
```task
|
||||
id: FLEX-WP-0024-T06
|
||||
-status: wait
|
||||
+status: progress
|
||||
priority: high
|
||||
```
|
||||
|
||||
@@ -197,3 +197,32 @@
|
||||
source review. This task owns the producer-side native gate; SECRETS-WP-0014 owns
|
||||
consumer admission/enforcement. The old fixture completion never established this
|
||||
gate, so the workplan is reopened rather than claiming it already passed.
|
||||
+
|
||||
+
|
||||
+2026-10-16 owner native preparation: T06 is `progress` because Access still owns
|
||||
+signer workload declaration, artifact/provenance and source/provider-join preparation;
|
||||
+no unqualified cross-owner wait is claimed. Read-only actual release metadata and
|
||||
+bounded proposal are in the Custodian
|
||||
+`docs/changes/netkingdom-target-2026-10-16/access-native-preparation/`.
|
||||
+The real Platform custody route `flex-auth-decision-envelope-signing-key` exists but
|
||||
+is draft/nonresolvable; no production kid/public fingerprint, Secret projection or
|
||||
+custody receipt is admitted. Coordinate that named commitment through CUST-WP-0080
|
||||
+instead of inventing a Platform task/approval. SECRETS-WP-0014-T03 owns installed
|
||||
+consumer native verification and must not form a circular blocking dependency.
|
||||
+
|
||||
+The first proposed native trial only signs the unchanged lifecycle package on the
|
||||
+existing retained release; `execute-ccr` remains denied. Current service-pilot
|
||||
+`runtime_admission := false` remains mandatory: the source ServicePilotInputs is
|
||||
+not yet wired into HTTP, and public service_actor_binding never authenticates the
|
||||
+caller. Later activation needs a complete reviewed provider join, exact policy and
|
||||
+operation admission, artifact digests, owner execution/rollback proposal and native
|
||||
+receipts. Preparation/local synthetic success grants none of those permissions.
|
||||
+
|
||||
+
|
||||
+2026-10-16 source-only native preparation adds the existing Secrets PDP ServiceAccount
|
||||
+to `tenancy.yaml` with evidence in `docs/native-signer-workload.json`, makes the
|
||||
+retained `flex-auth` audience explicit, pins CI archive fetch to the full source
|
||||
+revision, and adds offline native signer receipt verification using the existing
|
||||
+Secrets verifier. No key, trust, image publication or live activation is admitted.
|
||||
+The managed workload's missing rapp declaration remains explicit migration debt;
|
||||
+this source binding does not fabricate a package declaration or raise its grade.
|
||||
|
|
@ -0,0 +1,442 @@
|
|||
diff --git a/tenancy.yaml b/tenancy.yaml
|
||||
--- a/tenancy.yaml
|
||||
+++ b/tenancy.yaml
|
||||
@@ -11,8 +11,16 @@
|
||||
workload_identity:
|
||||
name: flex-auth
|
||||
kind: platform-service
|
||||
- responsible_repo: flex-auth
|
||||
+ responsible_repo: access-engine
|
||||
identity_bindings:
|
||||
+ # Existing retained Secrets PDP pin; identity source, not key admission.
|
||||
+ - scheme: kubernetes-service-account
|
||||
+ authority: railiance01
|
||||
+ subject: system:serviceaccount:flex-auth:flex-auth-secrets-engine
|
||||
+ principal_type: service
|
||||
+ environment: prod
|
||||
+ evidence:
|
||||
+ - docs/native-signer-workload.json
|
||||
- scheme: kubernetes-service-account
|
||||
authority: railiance01
|
||||
subject: system:serviceaccount:flex-auth:flex-auth-ops-warden
|
||||
diff --git a/values/secrets-engine.yaml b/values/secrets-engine.yaml
|
||||
--- a/values/secrets-engine.yaml
|
||||
+++ b/values/secrets-engine.yaml
|
||||
@@ -13,6 +13,8 @@
|
||||
- /opt/flex-auth/examples/secrets-engine/policy_package.md
|
||||
callerAuth:
|
||||
mode: enforce
|
||||
+ # Retained runtime audience is explicit; signer preparation does not rename it.
|
||||
+ audience: flex-auth
|
||||
kubernetesURL: https://10.43.0.1
|
||||
binding: secrets-engine=system:serviceaccount:secrets-engine:secrets-engine
|
||||
consumer:
|
||||
diff --git a/charts/flex-auth/templates/deployment.yaml b/charts/flex-auth/templates/deployment.yaml
|
||||
--- a/charts/flex-auth/templates/deployment.yaml
|
||||
+++ b/charts/flex-auth/templates/deployment.yaml
|
||||
@@ -83,6 +83,8 @@
|
||||
{{- if not (and .Values.readback .Values.readback.enabled) }}
|
||||
{{- $binding = required "callerAuth.binding is required when callerAuth.mode is warn or enforce" .Values.callerAuth.binding }}
|
||||
{{- end }}
|
||||
+ - --caller-audience
|
||||
+ - {{ required "callerAuth.audience is required" .Values.callerAuth.audience | quote }}
|
||||
- --caller-auth-mode
|
||||
- {{ include "flex-auth.callerAuth.mode" . | quote }}
|
||||
- --caller-kubernetes-url
|
||||
diff --git a/.forgejo/workflows/image.yaml b/.forgejo/workflows/image.yaml
|
||||
--- a/.forgejo/workflows/image.yaml
|
||||
+++ b/.forgejo/workflows/image.yaml
|
||||
@@ -51,7 +51,7 @@
|
||||
SHORT="${REF:0:7}"
|
||||
mkdir -p buildctx "${HOME}/bin"
|
||||
wget -qO /tmp/repo.tar.gz \
|
||||
- "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${SHORT}.tar.gz"
|
||||
+ "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${REF}.tar.gz"
|
||||
tar xzf /tmp/repo.tar.gz -C buildctx --strip-components=1
|
||||
wget -qO- https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz \
|
||||
| tar xz --strip-components=1 -C "${HOME}/bin" docker/docker
|
||||
diff --git a/tools/verify_native_signer_receipt.py b/tools/verify_native_signer_receipt.py
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/tools/verify_native_signer_receipt.py
|
||||
@@ -0,0 +1,135 @@
|
||||
+#!/usr/bin/env python3
|
||||
+"""Verify captured native decision receipts with the installed Secrets verifier.
|
||||
+
|
||||
+Offline only: no token input, network request, key generation, build or effect.
|
||||
+Use an independently reviewed admission file; success never admits that file.
|
||||
+"""
|
||||
+from __future__ import annotations
|
||||
+import argparse
|
||||
+import copy
|
||||
+from datetime import datetime, timezone
|
||||
+import hashlib
|
||||
+import json
|
||||
+from pathlib import Path
|
||||
+import re
|
||||
+
|
||||
+# These are the consumer's existing verifier/binding functions, not another PDP
|
||||
+# or signature/canonicalization implementation. Install the reviewed bundle first.
|
||||
+from secrets_engine.authorization import request_digest, validate_decision_envelope
|
||||
+from secrets_engine.decision_signature import authenticate_response, load_trust, strict_json
|
||||
+from secrets_engine.errors import DecisionError
|
||||
+
|
||||
+FIELDS = {'schema','trust_file','verifier','verifier_sha256','allow_request','allow_envelope','allow_record','deny_request','deny_envelope','deny_record','expected_caller'}
|
||||
+
|
||||
+def read(path: str) -> bytes:
|
||||
+ target = Path(path)
|
||||
+ if not target.is_absolute():
|
||||
+ raise ValueError('absolute input path required')
|
||||
+ with target.open("rb") as stream:
|
||||
+ data = stream.read(524289)
|
||||
+ if len(data) > 524288:
|
||||
+ raise ValueError('receipt input too large')
|
||||
+ return data
|
||||
+
|
||||
+def same_json_types(left, right) -> bool:
|
||||
+ """JSON value correspondence only; never signature canonicalization."""
|
||||
+ if type(left) is not type(right):
|
||||
+ return False
|
||||
+ if isinstance(left, dict):
|
||||
+ return left.keys() == right.keys() and all(same_json_types(value,right[key]) for key,value in left.items())
|
||||
+ if isinstance(left, list):
|
||||
+ return len(left) == len(right) and all(same_json_types(a,b) for a,b in zip(left,right))
|
||||
+ return left == right
|
||||
+
|
||||
+
|
||||
+def stamp(value: str) -> datetime:
|
||||
+ parsed = datetime.fromisoformat(value.replace('Z','+00:00'))
|
||||
+ if parsed.tzinfo is None:
|
||||
+ raise ValueError('timezone required')
|
||||
+ return parsed.astimezone(timezone.utc)
|
||||
+
|
||||
+def verify(config: dict, *, now: datetime | None = None) -> dict:
|
||||
+ if not isinstance(config,dict) or set(config) != FIELDS or config['schema'] != 'access-engine.signer-receipt-check.v1':
|
||||
+ raise ValueError('unsupported admission fields')
|
||||
+ current = now or datetime.now(timezone.utc)
|
||||
+ caller = config['expected_caller']
|
||||
+ if caller != {'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine','audience':'flex-auth'}:
|
||||
+ raise ValueError('retained native caller admission required')
|
||||
+ executable = Path(config['verifier'])
|
||||
+ if not executable.is_absolute() or not re.fullmatch('[a-f0-9]{64}',config['verifier_sha256']) or hashlib.sha256(executable.read_bytes()).hexdigest() != config['verifier_sha256']:
|
||||
+ raise ValueError('installed verifier digest mismatch')
|
||||
+ trust_bytes = read(config['trust_file'])
|
||||
+ trust = load_trust(Path(config['trust_file']))
|
||||
+ if not same_json_types(trust,strict_json(trust_bytes)):
|
||||
+ raise ValueError('trust input changed during verification')
|
||||
+ if (trust['evaluator'],trust['mode'],trust['policy_package'],trust['policy_version']) != ('flex-auth/local','standalone','secrets-engine.catalog-lane.lifecycle','v2'):
|
||||
+ raise ValueError('signer-only lifecycle package required')
|
||||
+ checks = {}
|
||||
+ envelopes = {}
|
||||
+ inputs = {}
|
||||
+ requests = {}
|
||||
+ for label in ('allow','deny'):
|
||||
+ blobs = {suffix:read(config[label+'_'+suffix]) for suffix in ('request','envelope','record')}
|
||||
+ body = blobs['envelope']
|
||||
+ # Strict decode before helper; authenticated typed return alone is acted on.
|
||||
+ strict_json(body.decode('utf-8'))
|
||||
+ envelope = authenticate_response(body,trust=trust,verifier=executable)
|
||||
+ request = strict_json(blobs['request'])
|
||||
+ record = strict_json(blobs['record'])
|
||||
+ if not same_json_types(record,envelope):
|
||||
+ raise ValueError('captured original source record differs from authenticated response')
|
||||
+ provenance = envelope['provenance']['caller']
|
||||
+ if any(provenance.get(key) != value for key,value in caller.items()) or stamp(provenance['not_after']) <= current:
|
||||
+ raise ValueError('native caller provenance missing or expired')
|
||||
+ if label == 'allow':
|
||||
+ validate_decision_envelope(envelope,request,accepted_policy_packages={trust['policy_package']},accepted_policy_versions={trust['policy_version']},now=current)
|
||||
+ else:
|
||||
+ if envelope['effect'] != 'deny' or envelope['binding']['submitted_request_digest'] != request_digest(request):
|
||||
+ raise ValueError('deny receipt does not bind exact negative request')
|
||||
+ envelopes[label] = envelope
|
||||
+ requests[label] = request
|
||||
+ for suffix in ('request','envelope','record'):
|
||||
+ inputs[label+'_'+suffix] = hashlib.sha256(blobs[suffix]).hexdigest()
|
||||
+ checks[label+'_authenticated_bound_and_record_matched'] = True
|
||||
+ # These are local refusals against the captured real producer envelope. They
|
||||
+ # do not claim a rejected caller was exercised on the native HTTP endpoint.
|
||||
+ for label in ('altered','unsigned','unknown_kid','wrong_policy_pin'):
|
||||
+ bad = copy.deepcopy(envelopes['allow']); pins = copy.deepcopy(trust)
|
||||
+ if label == 'altered': bad['binding']['action'] = 'forged-action'
|
||||
+ elif label == 'unsigned': bad['signature'] = {'mode':'unsigned'}
|
||||
+ elif label == 'unknown_kid': bad['signature']['kid'] = 'unadmitted-key'
|
||||
+ else: pins['policy_package_digest'] = 'sha256:'+'0'*64
|
||||
+ try:
|
||||
+ authenticate_response(json.dumps(bad).encode(),trust=pins,verifier=executable)
|
||||
+ except DecisionError:
|
||||
+ checks[label+'_refused'] = True
|
||||
+ else:
|
||||
+ raise ValueError('negative authentication case accepted')
|
||||
+ try:
|
||||
+ expiry=stamp(envelopes['allow']['lifetime']['expires_at'])
|
||||
+ request=requests['allow']
|
||||
+ validate_decision_envelope(envelopes['allow'],request,accepted_policy_packages={trust['policy_package']},accepted_policy_versions={trust['policy_version']},now=expiry)
|
||||
+ except DecisionError:
|
||||
+ checks['expiry_boundary_refused'] = True
|
||||
+ else:
|
||||
+ raise ValueError('expired allow accepted')
|
||||
+ return {'schema':'access-engine.signer-receipt-result.v1','checks':checks,'input_sha256':inputs,'verifier_sha256':config['verifier_sha256'],'trust_file_sha256':hashlib.sha256(trust_bytes).hexdigest(),'boundary':'offline captured signed receipt checks only; no native caller-denial, record durability, receiver custody, trust admission, rotation or protected effect inferred'}
|
||||
+
|
||||
+def main() -> int:
|
||||
+ parser=argparse.ArgumentParser(description=__doc__)
|
||||
+ parser.add_argument('--admission',type=Path,required=True)
|
||||
+ parser.add_argument('--output',type=Path,required=True)
|
||||
+ args=parser.parse_args()
|
||||
+ try:
|
||||
+ config=strict_json(read(str(args.admission)))
|
||||
+ result=verify(config)
|
||||
+ args.output.write_text(json.dumps(result,indent=2)+'\n')
|
||||
+ except (OSError,ValueError,TypeError,KeyError,RecursionError,DecisionError):
|
||||
+ # Never dump source objects, unexpected key material or verifier stderr.
|
||||
+ print('signer receipt verification refused')
|
||||
+ return 1
|
||||
+ print('captured signer receipt checks passed')
|
||||
+ return 0
|
||||
+
|
||||
+if __name__ == '__main__':
|
||||
+ raise SystemExit(main())
|
||||
diff --git a/tests/test_native_signer_receipt.py b/tests/test_native_signer_receipt.py
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/tests/test_native_signer_receipt.py
|
||||
@@ -0,0 +1,100 @@
|
||||
+"""Offline tool regression using the producer's existing synthetic signer only."""
|
||||
+import copy
|
||||
+from datetime import datetime,timedelta,timezone
|
||||
+import hashlib
|
||||
+import importlib.util
|
||||
+import json
|
||||
+import os
|
||||
+from pathlib import Path
|
||||
+import subprocess
|
||||
+import tempfile
|
||||
+import unittest
|
||||
+from unittest import mock
|
||||
+
|
||||
+ROOT=Path(__file__).resolve().parents[1]
|
||||
+@unittest.skipUnless(os.environ.get('SECRETS_ENGINE_SOURCE_ROOT'), 'requires reviewed Secrets consumer source/bundle for integration checks')
|
||||
+class NativeSignerReceiptTests(unittest.TestCase):
|
||||
+ @classmethod
|
||||
+ def setUpClass(cls):
|
||||
+ global MODULE
|
||||
+ spec=importlib.util.spec_from_file_location('signer_receipt',ROOT/'tools/verify_native_signer_receipt.py')
|
||||
+ MODULE=importlib.util.module_from_spec(spec);spec.loader.exec_module(MODULE)
|
||||
+ consumer=Path(os.environ['SECRETS_ENGINE_SOURCE_ROOT'])
|
||||
+ cls.scratch=tempfile.TemporaryDirectory();cls.base=Path(cls.scratch.name)
|
||||
+ env={**os.environ,'GOFLAGS':'-buildvcs=false'}
|
||||
+ cls.verifier=cls.base/'verify';cls.signer=cls.base/'synthetic-sign'
|
||||
+ for binary,package in [(cls.verifier,'.'),(cls.signer,'./testsupport/signer')]:
|
||||
+ subprocess.run(['go','build','-p','2','-o',str(binary),package],cwd=consumer/'verifier',env=env,check=True)
|
||||
+ cls.now=datetime.now(timezone.utc)
|
||||
+ env=json.loads((consumer/'tests/fixtures/pdp-signature/decision_rotate_signed.json').read_text())
|
||||
+ env['provenance']['caller']={'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine','audience':'flex-auth','not_after':(cls.now+timedelta(minutes=20)).isoformat()}
|
||||
+ env['lifetime']['not_before']=(cls.now-timedelta(seconds=1)).isoformat();env['lifetime']['expires_at']=(cls.now+timedelta(minutes=15)).isoformat()
|
||||
+ def sign(envelope):
|
||||
+ return json.loads(subprocess.check_output([str(cls.signer)],input=json.dumps(envelope).encode()))
|
||||
+ env['diagnostics']['target']={'phase':{'uses':1}}
|
||||
+ cls.allow=sign(env)
|
||||
+ req=json.loads((ROOT/'examples/secrets-engine/check_request_allow_rotate.json').read_text())
|
||||
+ (cls.base/'allow-request.json').write_text(json.dumps(req))
|
||||
+ denied=copy.deepcopy(req);denied['action']='execute-ccr'
|
||||
+ cls.deny=copy.deepcopy(env);cls.deny['effect']='deny';cls.deny['binding']['action']='execute-ccr';cls.deny['binding']['submitted_request_digest']=MODULE.request_digest(denied);cls.deny=sign(cls.deny)
|
||||
+ (cls.base/'deny-request.json').write_text(json.dumps(denied))
|
||||
+ provenance=cls.allow['provenance'];cls.trust={key:provenance[key] for key in ('evaluator','mode','policy_package','policy_version','policy_package_digest')}
|
||||
+ cls.trust.update(schema='secrets-engine.pdp-trust.v1',keys={'testdata-ed25519':'IVL40Zt5HSRFMkLhXy6rbLfP-ntqXtMAl5YOBpiB2xI'})
|
||||
+ @classmethod
|
||||
+ def tearDownClass(cls):cls.scratch.cleanup()
|
||||
+ def setUp(self):
|
||||
+ for label,envelope in [('allow',self.allow),('deny',self.deny)]:
|
||||
+ for kind in ('envelope','record'):(self.base/f'{label}-{kind}.json').write_text(json.dumps(envelope))
|
||||
+ (self.base/'trust.json').write_text(json.dumps(self.trust))
|
||||
+ self.config={'schema':'access-engine.signer-receipt-check.v1','trust_file':str(self.base/'trust.json'),'verifier':str(self.verifier),'verifier_sha256':hashlib.sha256(self.verifier.read_bytes()).hexdigest(),'expected_caller':{'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine','audience':'flex-auth'}}
|
||||
+ for label in ('allow','deny'):
|
||||
+ for kind in ('request','envelope','record'):self.config[f'{label}_{kind}']=str(self.base/f'{label}-{kind}.json')
|
||||
+ def test_genuine_signed_receipt_and_refusal_matrix(self):
|
||||
+ result=MODULE.verify(self.config,now=self.now)
|
||||
+ self.assertTrue(all(result['checks'].values()));self.assertEqual(len(result['checks']),7)
|
||||
+ def test_changed_source_record_refuses(self):
|
||||
+ record=copy.deepcopy(self.allow);record['effect']='deny';(self.base/'allow-record.json').write_text(json.dumps(record))
|
||||
+ with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now)
|
||||
+ def test_nested_record_number_type_change_refuses(self):
|
||||
+ for replacement in (True,1.0):
|
||||
+ with self.subTest(replacement_type=type(replacement).__name__):
|
||||
+ record=copy.deepcopy(self.allow)
|
||||
+ record['diagnostics']['target']['phase']['uses']=replacement
|
||||
+ (self.base/'allow-record.json').write_text(json.dumps(record))
|
||||
+ with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now)
|
||||
+
|
||||
+ def test_receipt_hashes_use_the_once_read_verified_bytes(self):
|
||||
+ actual_read=MODULE.read
|
||||
+ seen={}
|
||||
+ original={str(self.base/f'{label}-{suffix}.json'):actual_read(str(self.base/f'{label}-{suffix}.json')) for label in ('allow','deny') for suffix in ('request','envelope','record')}
|
||||
+ def once(path):
|
||||
+ if path in original:
|
||||
+ seen[path]=seen.get(path,0)+1
|
||||
+ self.assertEqual(seen[path],1,'receipt was reopened after verification')
|
||||
+ return actual_read(path)
|
||||
+ with mock.patch.object(MODULE,'read',side_effect=once):
|
||||
+ result=MODULE.verify(self.config,now=self.now)
|
||||
+ self.assertEqual(len(seen),6)
|
||||
+ for label in ('allow','deny'):
|
||||
+ for suffix in ('request','envelope','record'):
|
||||
+ self.assertEqual(result['input_sha256'][label+'_'+suffix],hashlib.sha256(original[str(self.base/f'{label}-{suffix}.json')]).hexdigest())
|
||||
+
|
||||
+ def test_oversized_file_read_is_bounded(self):
|
||||
+ stream=mock.MagicMock();stream.__enter__.return_value=stream;stream.read.return_value=b'x'*524289
|
||||
+ with mock.patch.object(Path,'open',return_value=stream):
|
||||
+ with self.assertRaises(ValueError):MODULE.read('/absolute/oversized.json')
|
||||
+ stream.read.assert_called_once_with(524289)
|
||||
+
|
||||
+ def test_unadmitted_helper_digest_refuses(self):
|
||||
+ self.config['verifier_sha256']='0'*64
|
||||
+ with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now)
|
||||
+ def test_disabled_caller_admission_refuses(self):
|
||||
+ self.config['expected_caller']['mode']='disabled'
|
||||
+ with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now)
|
||||
+ def test_expired_captured_allow_refuses(self):
|
||||
+ with self.assertRaises(MODULE.DecisionError):MODULE.verify(self.config,now=self.now+timedelta(minutes=16))
|
||||
+ def test_wrong_independent_policy_pin_refuses(self):
|
||||
+ wrong=copy.deepcopy(self.trust);wrong['policy_package_digest']='sha256:'+'0'*64;(self.base/'trust.json').write_text(json.dumps(wrong))
|
||||
+ with self.assertRaises(MODULE.DecisionError):MODULE.verify(self.config,now=self.now)
|
||||
+
|
||||
+if __name__=='__main__':unittest.main()
|
||||
diff --git a/docs/native-signer-workload.json b/docs/native-signer-workload.json
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/docs/native-signer-workload.json
|
||||
@@ -0,0 +1,68 @@
|
||||
+{
|
||||
+ "schema": "access-engine.signer-workload-reference.v1",
|
||||
+ "owner_repo": "access-engine",
|
||||
+ "existing_workload": "flex-auth",
|
||||
+ "authoritative_owner_declaration": "tenancy.yaml#/workload_identity",
|
||||
+ "exact_signer_identity_binding": "system:serviceaccount:flex-auth:flex-auth-secrets-engine",
|
||||
+ "declaration_scope": "Existing retained Secrets lifecycle PDP pin; declaration of identity source only. Not a new managed workload, credential grant, zone promotion or live admission.",
|
||||
+ "observed_runtime": {
|
||||
+ "cluster": "railiance01",
|
||||
+ "node_internal_ip": "92.205.62.239",
|
||||
+ "namespace": "flex-auth",
|
||||
+ "release": "flex-auth-secrets-engine",
|
||||
+ "deployment": "flex-auth-secrets-engine",
|
||||
+ "reviewer_service_account": "flex-auth-secrets-engine",
|
||||
+ "source_evidence": "the-custodian/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/live-metadata-r1.json",
|
||||
+ "evidence_sha256": "bafce991e02083ac30c8d6ce15f037dc22087bbb5dc8185c472149b46bca47c2"
|
||||
+ },
|
||||
+ "identity_roles": {
|
||||
+ "signer_workload": "system:serviceaccount:flex-auth:flex-auth-secrets-engine",
|
||||
+ "requesting_consumer": "system:serviceaccount:secrets-engine:secrets-engine",
|
||||
+ "retained_caller_audience": "flex-auth",
|
||||
+ "rule": "Signer custody binds the PDP workload, not the consumer or a human approver. Existing TokenReview reviewer projection does not grant signing-key custody."
|
||||
+ },
|
||||
+ "custody_reference": {
|
||||
+ "catalog_id": "flex-auth-decision-envelope-signing-key",
|
||||
+ "owner_repo": "railiance-platform",
|
||||
+ "current_state": "draft/nonresolvable",
|
||||
+ "proposed_source_path": "platform/workloads/flex-auth/envelope-signing",
|
||||
+ "proposed_private_field": "ED25519_PRIVATE_KEY",
|
||||
+ "required_encoding": "unpadded base64url 32-byte Ed25519 seed; mount0440 allowed, group-write and world permissions refused",
|
||||
+ "public_fields": [
|
||||
+ "ED25519_PUBLIC_KEY",
|
||||
+ "kid"
|
||||
+ ],
|
||||
+ "proposed_catalog_workload_reference": {
|
||||
+ "name": "flex-auth",
|
||||
+ "declaration_ref": "access-engine/tenancy.yaml#/workload_identity",
|
||||
+ "exact_binding": "system:serviceaccount:flex-auth:flex-auth-secrets-engine",
|
||||
+ "resolution": "unknown until owner custody and managed-package admission; do not mark resolvable merely from source binding"
|
||||
+ },
|
||||
+ "key_created_or_admitted": false
|
||||
+ },
|
||||
+ "managed_package_limit": {
|
||||
+ "rapp_declaration_ref": null,
|
||||
+ "status": "Existing managed-deployable migration debt remains unknown under security-zones_v0.1 \u00a76; no matching authoritative rapp declaration was established. Existing source tenancy declaration supplies owner identity binding but cannot substitute for managed-package admission.",
|
||||
+ "no_fiction": "Do not invent a rapp tuple or describe this Helm-managed Deployment as an independent operational unit to bypass the rule."
|
||||
+ },
|
||||
+ "unchanged_limits": [
|
||||
+ "tenancy and zones grades are unchanged",
|
||||
+ "service-pilot literal false gate unchanged",
|
||||
+ "human variant unchanged",
|
||||
+ "legacy twelve lifecycle actions remain the selected policy package",
|
||||
+ "no signer Secret name, kid, public fingerprint, custody receipt or rollout permission is supplied"
|
||||
+ ],
|
||||
+ "consumer_admission": "Secrets owner independently admits public keys/pins and exact installed verifier; this file never bootstraps trust from PDP discovery.",
|
||||
+ "native_tool": {
|
||||
+ "path": "tools/verify_native_signer_receipt.py",
|
||||
+ "requires": "Installed reviewed Secrets Engine package/helper and independent public admission; source request, authenticated response and original source record for one allow and one deny.",
|
||||
+ "operation": "Offline captured receipt checks only, no network/token input/key build or protected effect. Current clock validation; no CLI backdating option.",
|
||||
+ "result_limits": "Does not prove record durability, real rejected HTTP caller, audit receiver custody, rotation, or authorization to run effect."
|
||||
+ },
|
||||
+ "ci": {
|
||||
+ "trigger": "Existing ordinary push to main when .forgejo/workflows/image.yaml, Containerfile, cmd/**, internal/**, pkg/**, examples/**, go.mod/go.sum or tools/publish-runtime-images.sh changes; workflow_dispatch also exists. This patch changes the workflow, so an authorized push would trigger it.",
|
||||
+ "scope": "Existing container-build runner and already admitted forgejo.coulomb.social/coulomb/flex-auth package only in legacy mode; paired access-engine package publication requires separate existing dispatch authority and is excluded.",
|
||||
+ "provenance_preparation": "Download archive/${REF}.tar.gz using full validated GITHUB_SHA; existing publisher labels org.opencontainers.image.revision and reads back registry manifest digest.",
|
||||
+ "not_invoked": "No push, workflow dispatch, Docker build, image publication or paid resource allocation in this source preparation."
|
||||
+ }
|
||||
+}
|
||||
diff --git a/examples/secrets-engine/signer-receipt-admission.template.json b/examples/secrets-engine/signer-receipt-admission.template.json
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/examples/secrets-engine/signer-receipt-admission.template.json
|
||||
@@ -0,0 +1,17 @@
|
||||
+{
|
||||
+ "schema": "access-engine.signer-receipt-check.v1",
|
||||
+ "trust_file": "/absolute/owner-admitted/public-trust.json",
|
||||
+ "verifier": "/absolute/installed/secrets-engine-pdp-verify",
|
||||
+ "verifier_sha256": "REPLACE_WITH_INDEPENDENTLY_REVIEWED_INSTALLED_HELPER_SHA256",
|
||||
+ "allow_request": "/absolute/sanitized-native-receipts/allow-request.json",
|
||||
+ "allow_envelope": "/absolute/sanitized-native-receipts/allow-envelope.json",
|
||||
+ "allow_record": "/absolute/sanitized-native-receipts/allow-source-record.json",
|
||||
+ "deny_request": "/absolute/sanitized-native-receipts/deny-request.json",
|
||||
+ "deny_envelope": "/absolute/sanitized-native-receipts/deny-envelope.json",
|
||||
+ "deny_record": "/absolute/sanitized-native-receipts/deny-source-record.json",
|
||||
+ "expected_caller": {
|
||||
+ "mode": "enforce",
|
||||
+ "principal": "system:serviceaccount:secrets-engine:secrets-engine",
|
||||
+ "audience": "flex-auth"
|
||||
+ }
|
||||
+}
|
||||
diff --git a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
--- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
+++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
@@ -184,7 +184,7 @@
|
||||
|
||||
```task
|
||||
id: FLEX-WP-0024-T06
|
||||
-status: wait
|
||||
+status: progress
|
||||
priority: high
|
||||
```
|
||||
|
||||
@@ -197,3 +197,32 @@
|
||||
source review. This task owns the producer-side native gate; SECRETS-WP-0014 owns
|
||||
consumer admission/enforcement. The old fixture completion never established this
|
||||
gate, so the workplan is reopened rather than claiming it already passed.
|
||||
+
|
||||
+
|
||||
+2026-10-16 owner native preparation: T06 is `progress` because Access still owns
|
||||
+signer workload declaration, artifact/provenance and source/provider-join preparation;
|
||||
+no unqualified cross-owner wait is claimed. Read-only actual release metadata and
|
||||
+bounded proposal are in the Custodian
|
||||
+`docs/changes/netkingdom-target-2026-10-16/access-native-preparation/`.
|
||||
+The real Platform custody route `flex-auth-decision-envelope-signing-key` exists but
|
||||
+is draft/nonresolvable; no production kid/public fingerprint, Secret projection or
|
||||
+custody receipt is admitted. Coordinate that named commitment through CUST-WP-0080
|
||||
+instead of inventing a Platform task/approval. SECRETS-WP-0014-T03 owns installed
|
||||
+consumer native verification and must not form a circular blocking dependency.
|
||||
+
|
||||
+The first proposed native trial only signs the unchanged lifecycle package on the
|
||||
+existing retained release; `execute-ccr` remains denied. Current service-pilot
|
||||
+`runtime_admission := false` remains mandatory: the source ServicePilotInputs is
|
||||
+not yet wired into HTTP, and public service_actor_binding never authenticates the
|
||||
+caller. Later activation needs a complete reviewed provider join, exact policy and
|
||||
+operation admission, artifact digests, owner execution/rollback proposal and native
|
||||
+receipts. Preparation/local synthetic success grants none of those permissions.
|
||||
+
|
||||
+
|
||||
+2026-10-16 source-only native preparation adds the existing Secrets PDP ServiceAccount
|
||||
+to `tenancy.yaml` with evidence in `docs/native-signer-workload.json`, makes the
|
||||
+retained `flex-auth` audience explicit, pins CI archive fetch to the full source
|
||||
+revision, and adds offline native signer receipt verification using the existing
|
||||
+Secrets verifier. No key, trust, image publication or live activation is admitted.
|
||||
+The managed workload's missing rapp declaration remains explicit migration debt;
|
||||
+this source binding does not fabricate a package declaration or raise its grade.
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Create only the reviewed local package; no remote or runtime operations."""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tarfile
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
DEST = Path('/home/worsch/rapp-flex-auth-secrets-engine')
|
||||
ARCHIVE = HERE.parent / 'extraction/rapp-flex-auth-secrets-engine.tar'
|
||||
EXPECTED_ARCHIVE = '766279e86749bde4e25aafcb3989134e9879564667dd46a55be29bc995f66d8d'
|
||||
EXPECTED_TREE = 'd4fbad71bdc8ef870603526ccd1f53195d0322af'
|
||||
|
||||
|
||||
def git(*args):
|
||||
return subprocess.check_output(['git', '-C', str(DEST), *args], text=True).strip()
|
||||
|
||||
|
||||
def main():
|
||||
assert not DEST.exists() and not DEST.is_symlink(), 'Destination already exists; inspect before retry'
|
||||
assert hashlib.sha256(ARCHIVE.read_bytes()).hexdigest() == EXPECTED_ARCHIVE
|
||||
manifest = json.loads((HERE.parent / 'extraction/manifest.json').read_text())['files_sha256']
|
||||
files = {}
|
||||
with tarfile.open(ARCHIVE) as archive:
|
||||
for member in archive.getmembers():
|
||||
assert member.isfile() and member.name.startswith(DEST.name + '/')
|
||||
rel = Path(member.name).relative_to(DEST.name)
|
||||
assert '..' not in rel.parts and str(rel) not in files
|
||||
data = archive.extractfile(member).read()
|
||||
assert hashlib.sha256(data).hexdigest() == manifest[str(rel)]
|
||||
files[str(rel)] = (data, member.mode & 0o777)
|
||||
assert set(files) == set(manifest)
|
||||
DEST.mkdir()
|
||||
for rel, (data, mode) in files.items():
|
||||
target = DEST / rel
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(data)
|
||||
target.chmod(mode)
|
||||
git('init', '--initial-branch=main')
|
||||
git('add', '--', *sorted(files))
|
||||
assert git('write-tree') == EXPECTED_TREE
|
||||
git('commit', '-m', 'Declare retained Secrets lifecycle PDP package')
|
||||
assert git('rev-parse', 'HEAD^{tree}') == EXPECTED_TREE
|
||||
assert not git('status', '--porcelain')
|
||||
assert not git('remote')
|
||||
receipt = {
|
||||
'schema': 'custodian.local-canonical-package.v1',
|
||||
'destination': str(DEST), 'commit': git('rev-parse', 'HEAD'),
|
||||
'tree': EXPECTED_TREE, 'archive_sha256': 'sha256:' + EXPECTED_ARCHIVE,
|
||||
'files_sha256': {k: 'sha256:' + v for k, v in manifest.items()},
|
||||
'clean': True, 'remote_configured': False, 'published': False,
|
||||
'native_accepted': False,
|
||||
'scope': 'Reviewed source package locally committed; resolver validation recorded separately. No placement, custody, trust or runtime admission.'
|
||||
}
|
||||
(HERE / 'canonical-creation.json').write_text(json.dumps(receipt, indent=2) + '\n')
|
||||
print(json.dumps({'commit': receipt['commit'], 'tree': EXPECTED_TREE, 'destination': str(DEST)}))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"schema": "repo-manager.local-package-admission-preparation.v1",
|
||||
"authority": "disposable proof only; canonical package remains absent",
|
||||
"tar_sha256": "766279e86749bde4e25aafcb3989134e9879564667dd46a55be29bc995f66d8d",
|
||||
"file_hashes_verified": {
|
||||
".repo-classification.yaml": "99a5c80dda99a732563dbb64b8ce8b8663d9d619b919f9fed96cb7a63598fbb4",
|
||||
"AGENTS.md": "a02928b531ce61765bfb09bb1abea548ddcb589ecef7c762a1851d8e7745c7c7",
|
||||
"CLAUDE.md": "70f13f7bdd8c420fe2820ac879e464e7333c4547a5b0adf1b5e5a7a14113fa00",
|
||||
"INTENT.md": "a40ff791a4070bcdc59b7f889412d2558c5cd943308890bc0bf0690ea78c3f67",
|
||||
"README.md": "d8f096b22d4e67ce6eb2a2e201720fbbf3bc54059fec98ddf3f6ec4229087361",
|
||||
"SCOPE.md": "6167744e2b3f4b82a38f4950463068966fe595c7d62d3674813a0e8ed66789de",
|
||||
"declarations/rapp.yaml": "0b722431bc5162dd86f72b15f12424e149aca8f2a8790adf2c57e8511ea73b4c",
|
||||
"docs/operations.md": "690c2140d86c68edcc09d9c9acda10f0a4f14723c4236d02dd9f832e74651d6e",
|
||||
"source-pin.json": "d28e8ce2f83ebb8dd8bfd2cd03d8308405d189a77d05f7294439ba1da31e1818",
|
||||
"tests/test_render.py": "5e7cfe42249eda989813275b5cd307e7a2b0be9696443e566d30578b720c8c6a",
|
||||
"tools/render.py": "93ca85ddd8036952ae98e31ac1f36ebfb1ac9ad0a2f73b1e90dcf067f4ae544c",
|
||||
"workplans/README.md": "29f2194b1cf0981172cd28928f74224c72778c1771413006a92e4b4ed8214511",
|
||||
"workplans/archived/.gitkeep": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"disposable_root": "/tmp/access-package-admission-3ombu49c",
|
||||
"commit": "b6fc079c8df2a04073746f0a5f9f3963a9097c8c",
|
||||
"tree": "d4fbad71bdc8ef870603526ccd1f53195d0322af",
|
||||
"family_focused": {
|
||||
"argv": [
|
||||
"python3",
|
||||
"/home/worsch/railiance-master/tools/validate-family-declarations.py",
|
||||
"--root",
|
||||
"/tmp/access-package-admission-3ombu49c",
|
||||
"--repo",
|
||||
"/home/worsch/rail-kubernetes"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout": "2 declaration(s) ok\n",
|
||||
"stderr": ""
|
||||
},
|
||||
"resolution": {
|
||||
"ok": true,
|
||||
"contract": "helixforge.workload-reference/v1",
|
||||
"status": "resolved",
|
||||
"reference": {
|
||||
"rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
"name": "flex-auth-secrets-engine",
|
||||
"deployable": "flex-auth-secrets-engine"
|
||||
},
|
||||
"workload": {
|
||||
"rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
"name": "flex-auth-secrets-engine",
|
||||
"declaration_repo": "rapp-flex-auth-secrets-engine",
|
||||
"declaration_path": "rapp-flex-auth-secrets-engine/declarations/rapp.yaml",
|
||||
"ownership_repo": "access-engine",
|
||||
"readiness_state": "draft",
|
||||
"data_classification": "restricted",
|
||||
"criticality": "high",
|
||||
"deployables": [
|
||||
"flex-auth-secrets-engine"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
"name": "flex-auth-secrets-engine"
|
||||
}
|
||||
},
|
||||
"index_errors": []
|
||||
},
|
||||
"canonical_target_claims_before_preparation": [],
|
||||
"canonical_baseline_index_errors": [
|
||||
{
|
||||
"code": "declaration_missing",
|
||||
"path": "/home/worsch/rapp-forgejo/declarations/rapp.yaml",
|
||||
"message": "rapp repository has no authoritative declarations/rapp.yaml"
|
||||
}
|
||||
],
|
||||
"canonical_baseline_declaration_count": 13,
|
||||
"publication": "none; remote not configured; central onboarding not attempted"
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
# Repo Manager package admission preparation
|
||||
|
||||
2026-10-16. Delegated Repo Manager source review. This records executable
|
||||
preparation and the next owner action; it is not another owner's assent or
|
||||
permission to execute canonical writes. Continue existing `FLEX-WP-0024-T06`,
|
||||
coordinated by `CUST-WP-0080`; no duplicate backlog or new workplan is needed.
|
||||
|
||||
The selected tar is accepted by Access and the Railiance Master source review.
|
||||
SHA256: `766279e86749bde4e25aafcb3989134e9879564667dd46a55be29bc995f66d8d`.
|
||||
The declaration SHA256 is
|
||||
`0b722431bc5162dd86f72b15f12424e149aca8f2a8790adf2c57e8511ea73b4c`.
|
||||
Keep these 13 files unchanged; `extraction/manifest.json` is their hash manifest.
|
||||
Do not regenerate the reviewed package with `rmgr rapp init`, `wrap`, or
|
||||
`place`: those scaffold/change source and placement and would invalidate the
|
||||
reviewed bytes. Existing source tar plus its manifest are the source artifacts;
|
||||
this preparation does not create a competing package authority.
|
||||
|
||||
## What can be admitted locally
|
||||
|
||||
Yes: a committed canonical declaration accepted by the family validator can be
|
||||
resolved from the local checkout before remote publication. The deployment guide
|
||||
§3.5 defines rapp existence by committed declaration and family acceptance;
|
||||
there is no `register-rapp`. `repo_manager.workloads` reads local files and
|
||||
requires neither remote nor registry. It does not itself enforce commit state;
|
||||
therefore commit/tree receipts are a separate necessary evidence check. A
|
||||
disposable commit in `/tmp` proves the mechanism, not canonical existence.
|
||||
|
||||
No: central repository onboarding is not a local-only admission path.
|
||||
`commands/repo_onboard.py::_source_is_reachable` requires committed classification,
|
||||
upstream configuration and zero commits ahead before even `--dry-run` reads the
|
||||
hub. Central `managed_repos` is a projection, not an alternative declaration.
|
||||
Keep the actual authoritative target identity unknown until the canonical owner
|
||||
declaration exists and the consumer's resolver can access it. A central/private
|
||||
consumer must have admitted read access; local proof does not supply that access.
|
||||
|
||||
## Reproduce safe preparation
|
||||
|
||||
```bash
|
||||
/home/worsch/repo-manager/.venv/bin/python docs/changes/netkingdom-target-2026-10-16/access-package-identity/admission/prepare_local.py
|
||||
```
|
||||
|
||||
This verifies the exact archive and all 13 file hashes, creates a new disposable
|
||||
Git repo under `/tmp`, commits only those bytes, validates the package together
|
||||
with rail-kubernetes, validates owner classification, and resolves the exact
|
||||
tuple plus deployable. It records the current canonical target claims and known
|
||||
baseline index errors in `local-proof.json`. No remote, hub write or credential
|
||||
path is invoked. The first attempt failed because repo-manager's Python lacks
|
||||
`jsonschema`; the corrected script invokes system Python for the family validator.
|
||||
An intermediate focused run warned that Access was absent from its disposable
|
||||
root; a read-only Access symlink removes that warning in the final proof.
|
||||
|
||||
Final proof commit: `b6fc079c8df2a04073746f0a5f9f3963a9097c8c` (disposable).
|
||||
Exact source Git tree: `d4fbad71bdc8ef870603526ccd1f53195d0322af`.
|
||||
Focused family validation: 2 declarations, zero errors/warnings. Exact resolver:
|
||||
resolved, draft, restricted/high, owner access-engine. Canonical baseline: 13
|
||||
declarations, no target claim; pre-existing rapp-forgejo missing declaration.
|
||||
None of these assertions establishes custody, placement, zone membership,
|
||||
live source-to-runtime correspondence, signer trust, or native rollout approval.
|
||||
|
||||
## Next concrete action after coordinator review
|
||||
|
||||
Coordinator reviews this plan and selects the canonical local destination.
|
||||
`/home/worsch/rapp-flex-auth-secrets-engine` is currently absent and is the normal
|
||||
fleet sibling candidate, not an already authorized placement decision. The
|
||||
commands below use `PACKAGE_ROOT` supplied by that review; this is repository
|
||||
filesystem placement only. Existing declaration keeps `bound_reefs: []`, draft
|
||||
readiness and private exposure.
|
||||
|
||||
```bash
|
||||
set -eu
|
||||
: "${PACKAGE_ROOT:?Set coordinator-reviewed absolute canonical package path}"
|
||||
test ! -e "$PACKAGE_ROOT"
|
||||
test "$(basename "$PACKAGE_ROOT")" = rapp-flex-auth-secrets-engine
|
||||
ARCHIVE=/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/access-package-identity/extraction/rapp-flex-auth-secrets-engine.tar
|
||||
printf '%s %s\n' 766279e86749bde4e25aafcb3989134e9879564667dd46a55be29bc995f66d8d "$ARCHIVE" | sha256sum --check
|
||||
mkdir "$PACKAGE_ROOT"
|
||||
tar -xf "$ARCHIVE" --strip-components=1 -C "$PACKAGE_ROOT"
|
||||
git -C "$PACKAGE_ROOT" init --initial-branch=main
|
||||
git -C "$PACKAGE_ROOT" add .
|
||||
test "$(git -C "$PACKAGE_ROOT" write-tree)" = d4fbad71bdc8ef870603526ccd1f53195d0322af
|
||||
git -C "$PACKAGE_ROOT" commit -m 'Declare retained Secrets lifecycle PDP package'
|
||||
python3 /home/worsch/railiance-master/tools/validate-family-declarations.py --root /home/worsch --repo "$PACKAGE_ROOT"
|
||||
rmgr workload resolve --root "$PACKAGE_ROOT" --rapp-id rapp-flex-auth-secrets-engine --name flex-auth-secrets-engine --deployable flex-auth-secrets-engine
|
||||
rmgr workload index --root /home/worsch
|
||||
git -C "$PACKAGE_ROOT" rev-parse HEAD HEAD^{tree}
|
||||
```
|
||||
|
||||
The global validator/index have existing unrelated findings, so preserve the
|
||||
nonzero result and compare it with baseline; do not declare global success or
|
||||
erase it with `|| true`. Run the following resolver/index commands separately
|
||||
after recording that finding. Stop on changed package hashes, family semantics,
|
||||
duplicate target claims, or unexpected findings. Confirm clean tracked source,
|
||||
exact declaration hash, commit, tree and resolver root in the admission receipt.
|
||||
Do not substitute the `/tmp` proof commit for that receipt. Any shell signing
|
||||
or required credential step stays in its existing owner path, never bypassed.
|
||||
|
||||
Rollback before publication: leave the unconsumed canonical candidate unadmitted
|
||||
and record why. Removing an unregistered new checkout requires the normal
|
||||
destructive-action review; no Helm, PVC, image or runtime changes are involved.
|
||||
Cost: local disk/CPU only, no spend. Risk: mistaken authority promotion or wrong
|
||||
destination; abort owner is the Custodian coordinator with Access owner review.
|
||||
|
||||
## Private publication and registration, separately gated
|
||||
|
||||
Repo Manager owns classification/onboarding, not Forgejo hosting. No inspected
|
||||
Repo Manager command creates a remote hosted repository. Do not invent a
|
||||
`repo create` or use another workload's token. Before any private creation or
|
||||
push, record the approved namespace, exact URL, private visibility, allowed
|
||||
writer, source commit and existing Forge owner procedure. Use `warden route`
|
||||
before any credential request. No credential request/read is necessary for this
|
||||
packet. Public publication requires its explicit gate and is outside this plan;
|
||||
workload private exposure and Git repository visibility are separate properties.
|
||||
|
||||
After authorized private Forge creation through the existing Forge owner path:
|
||||
|
||||
```bash
|
||||
: "${APPROVED_ORIGIN:?Exact approved private Forge URL required}"
|
||||
: "${AUTHORITATIVE_HUB:?Verified authoritative State Hub URL required}"
|
||||
git -C "$PACKAGE_ROOT" remote add origin "$APPROVED_ORIGIN"
|
||||
git -C "$PACKAGE_ROOT" push --set-upstream origin main
|
||||
git -C "$PACKAGE_ROOT" fetch origin main
|
||||
test "$(git -C "$PACKAGE_ROOT" rev-parse HEAD)" = "$(git -C "$PACKAGE_ROOT" rev-parse origin/main)"
|
||||
rmgr repo-onboard --path "$PACKAGE_ROOT" --api-base "$AUTHORITATIVE_HUB" --dry-run
|
||||
rmgr repo-onboard --path "$PACKAGE_ROOT" --api-base "$AUTHORITATIVE_HUB"
|
||||
rmgr sync --path "$PACKAGE_ROOT" --push
|
||||
```
|
||||
|
||||
Verify remote private metadata and source commit through the Forge owner path,
|
||||
then hub readback. `rmgr repo-onboard` is the normal repository registration
|
||||
surface; `rmgr register put` manages internal registers and is not this operation.
|
||||
Publishing to a remote is unnecessary for the bounded local canonical resolution
|
||||
receipt, but mandatory before central onboarding. Neither step permits Platform
|
||||
custody/catalog edits or Access native signer activation. Existing owner gates
|
||||
remain under FLEX-WP-0024-T06 and Platform/Secrets acceptance records.
|
||||
|
||||
Sources read: Repo Manager INTENT, AGENTS, ADR-004, deployment guide §§3.5–5,
|
||||
workload resolver, onboarding implementation and CLI; Railiance Master validator;
|
||||
the target architecture and exact extraction/family-review packet. Inspected
|
||||
Repo Manager HEAD: `97ea2489257de08911cc5da1e1b84d8e15780dc3`.
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Reproduce source admission checks in a disposable local Git repository.
|
||||
|
||||
No canonical checkout, remote, registry, credentials, or runtime are mutated.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
PACKET = HERE.parent
|
||||
PACKAGE = "rapp-flex-auth-secrets-engine"
|
||||
EXPECTED = "766279e86749bde4e25aafcb3989134e9879564667dd46a55be29bc995f66d8d"
|
||||
PYTHON = "/home/worsch/repo-manager/.venv/bin/python"
|
||||
|
||||
|
||||
def run(argv, cwd=None):
|
||||
p = subprocess.run(argv, cwd=cwd, text=True, capture_output=True)
|
||||
return {"argv": argv, "returncode": p.returncode,
|
||||
"stdout": p.stdout, "stderr": p.stderr}
|
||||
|
||||
|
||||
def checked(argv, cwd=None):
|
||||
result = run(argv, cwd)
|
||||
if result["returncode"]:
|
||||
raise RuntimeError(json.dumps(result))
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
archive = PACKET / "extraction" / (PACKAGE + ".tar")
|
||||
digest = hashlib.sha256(archive.read_bytes()).hexdigest()
|
||||
assert digest == EXPECTED, "archive drift"
|
||||
expected_files = json.loads((PACKET / "extraction/manifest.json").read_text())["files_sha256"]
|
||||
root = Path(tempfile.mkdtemp(prefix="access-package-admission-", dir="/tmp"))
|
||||
repo = root / PACKAGE
|
||||
(root / "access-engine").symlink_to("/home/worsch/access-engine", target_is_directory=True)
|
||||
with tarfile.open(archive) as tar:
|
||||
members = tar.getmembers()
|
||||
assert all(m.isfile() and m.name.startswith(PACKAGE + "/") and
|
||||
".." not in Path(m.name).parts for m in members)
|
||||
assert {str(Path(m.name).relative_to(PACKAGE)) for m in members} == set(expected_files)
|
||||
for member in members:
|
||||
rel = Path(member.name).relative_to(PACKAGE)
|
||||
data = tar.extractfile(member).read()
|
||||
assert hashlib.sha256(data).hexdigest() == expected_files[str(rel)], str(rel)
|
||||
target = repo / rel
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(data)
|
||||
checked(["git", "init", "--initial-branch=main", str(repo)])
|
||||
checked(["git", "add", "."], repo)
|
||||
checked(["git", "-c", "user.name=Codex source admission proof", "-c",
|
||||
"user.email=codex-source-proof@localhost", "-c", "commit.gpgsign=false",
|
||||
"commit", "-m", "Disposable proof of reviewed source package; no canonical admission"], repo)
|
||||
focused = checked(["python3", "/home/worsch/railiance-master/tools/validate-family-declarations.py",
|
||||
"--root", str(root), "--repo", "/home/worsch/rail-kubernetes"])
|
||||
resolve = checked(["/home/worsch/repo-manager/.venv/bin/rmgr", "workload", "resolve",
|
||||
"--root", str(root), "--rapp-id", PACKAGE, "--name", "flex-auth-secrets-engine",
|
||||
"--deployable", "flex-auth-secrets-engine"])
|
||||
# Global comparison is read-only, including known unrelated baseline errors.
|
||||
import sys
|
||||
sys.path.insert(0, "/home/worsch/repo-manager/src")
|
||||
from repo_manager.workloads import index_workloads
|
||||
from repo_manager.commands.repo_onboard import _load_classification
|
||||
_load_classification(repo)
|
||||
baseline = index_workloads(Path("/home/worsch"))
|
||||
proof = json.loads(resolve["stdout"])
|
||||
claims = [r for r in baseline["workloads"] if "flex-auth-secrets-engine" in r["deployables"]]
|
||||
assert not claims, "canonical fleet already claims target; re-review before proceeding"
|
||||
assert proof["status"] == "resolved", proof
|
||||
report = {"schema": "repo-manager.local-package-admission-preparation.v1",
|
||||
"authority": "disposable proof only; canonical package remains absent",
|
||||
"tar_sha256": digest, "file_hashes_verified": expected_files,
|
||||
"disposable_root": str(root), "commit": checked(["git", "rev-parse", "HEAD"], repo)["stdout"].strip(),
|
||||
"tree": checked(["git", "rev-parse", "HEAD^{tree}"], repo)["stdout"].strip(),
|
||||
"family_focused": focused, "resolution": proof,
|
||||
"canonical_target_claims_before_preparation": claims,
|
||||
"canonical_baseline_index_errors": baseline["errors"],
|
||||
"canonical_baseline_declaration_count": baseline["declaration_count"],
|
||||
"publication": "none; remote not configured; central onboarding not attempted"}
|
||||
(HERE / "local-proof.json").write_text(json.dumps(report, indent=2) + "\n")
|
||||
print(json.dumps({"commit": report["commit"], "tree": report["tree"], "path": str(repo)}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
# Access retained PDP package identity: Repo Manager review
|
||||
|
||||
16 October 2026. Source-only, delegated Repo Manager boundary review. This is a
|
||||
proposal for owner integration, not owner admission or a new governing exception.
|
||||
|
||||
The retained `flex-auth/flex-auth-secrets-engine` Helm Deployment is a managed
|
||||
running workload awaiting rapp extraction. Its package identity remains
|
||||
`unknown`. Continuing its existing ownership-repo operation is supported;
|
||||
using an unmanaged/not-applicable identity to satisfy workload-based custody or
|
||||
zone admission is not supported.
|
||||
|
||||
## Exact governing basis
|
||||
|
||||
- `repo-manager/docs/adr-004-authoritative-workload-declarations.md`, decisions
|
||||
1–6 and consequences: one authoritative `rapp-*/declarations/rapp.yaml`
|
||||
surface; pre-rapp managed runtimes are migration debt; only genuine
|
||||
non-workload subjects may be not-applicable.
|
||||
- `railiance-master/docs/adr/ADR-0007-rapp-declaration-contract.md`, accepted-2,
|
||||
decisions 2, 3 and 5: exactly one package per deployable; independently
|
||||
rolling back units must not be grouped by repository name alone.
|
||||
- `repo-manager/docs/RailianceAppDeploymentGuide.md` §§3.5–4 explicitly lists
|
||||
`access-engine/railiance/app.toml` as the flex-auth Path B legacy example.
|
||||
Ownership-repo operation is allowed before extraction, but does not supply
|
||||
the missing package identity. A rapp requires a committed declaration and
|
||||
acceptance by `railiance-master/tools/validate-family-declarations.py`.
|
||||
- `repo-manager/docs/workload-reference-contract_v1.md`: the exact
|
||||
`(rapp_id, workload_identity.name)` pair and optional declared deployable
|
||||
resolve without name, path, repository, namespace or owner fallbacks.
|
||||
- `repo-manager/workplans/RMGR-WP-0010-authoritative-workload-references.md`
|
||||
is finished producer work. T05/interface
|
||||
`interfaces/rapp-workload-coverage-v1.yaml` published this division of
|
||||
authority; it does not authorize Repo Manager to author another owner's
|
||||
package or approve custody.
|
||||
|
||||
## Bounded source observations
|
||||
|
||||
Existing `access-engine/values/secrets-engine.yaml` names the release/workload
|
||||
`flex-auth-secrets-engine`, repository
|
||||
`forgejo.coulomb.social/coulomb/flex-auth`, and digest
|
||||
`sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd`.
|
||||
It selects the retained `examples/secrets-engine` lifecycle policy and registry,
|
||||
caller audience `flex-auth`, and consumer binding
|
||||
`secrets-engine=system:serviceaccount:secrets-engine:secrets-engine`.
|
||||
The signer workload source binding is separately
|
||||
`system:serviceaccount:flex-auth:flex-auth-secrets-engine` in
|
||||
`access-engine/docs/native-signer-workload.json`. This is useful owner identity
|
||||
evidence but explicitly not a managed-package declaration.
|
||||
|
||||
The 14 checked-out rapp directories produce 13 indexed declarations and one
|
||||
missing declaration (`rapp-forgejo`). None claims deployable
|
||||
`flex-auth-secrets-engine`. `rapp-secrets-engine` is a retired tombstone owned
|
||||
by `secrets-engine`, declaring only `secrets-engine-cli`, with no reef binding.
|
||||
Its `runtime_dependencies: [openbao, flex-auth, state-hub]` does not assign
|
||||
ownership of any of those dependencies. Reuse capability discovery and Access
|
||||
tenancy metadata likewise cannot assign package ownership.
|
||||
|
||||
Repo Manager indexing/resolution checks identity and composition; it is not the
|
||||
full normative family-schema or runtime-admission validator. In particular a
|
||||
resolved tuple is not proof of non-retired readiness, custody, a live deployment
|
||||
join, zone membership or signed-decision consumer admission.
|
||||
|
||||
## Minimal owner implementation proposal
|
||||
|
||||
Keep producer native preparation in the existing Access task
|
||||
`FLEX-WP-0024-T06` (`7f785b52-bf62-5884-8d34-670d70bc9023`); its current text
|
||||
already owns exact packages and marks this debt. Add the following acceptance
|
||||
paragraph there when Access integrates this review:
|
||||
|
||||
> Before managed-workload custody admission, extract an owner-reviewed rapp
|
||||
> declaration for the existing retained `flex-auth-secrets-engine` release,
|
||||
> preserving its independent rollout/rollback boundary. Access selects and
|
||||
> records the canonical package ID and workload name, declares itself
|
||||
> `ownership_repo: access-engine`, and binds the exact deployable. Validate
|
||||
> against the normative Railiance Master schema and family checks; record the
|
||||
> declaration commit, exact resolver reference and unique-deployable result.
|
||||
> Package declaration/resolution alone grants no signer custody or rollout.
|
||||
|
||||
The smallest scope is a package for this independently retained lifecycle PDP
|
||||
release. A larger Access package is valid only if Access establishes shared
|
||||
rollout/rollback fate for every included deployment; `app.toml` already describes
|
||||
independent production pins. Canonical tuple selection is an Access/package
|
||||
owner decision. No proposed tuple in this packet should be consumed as authority.
|
||||
|
||||
Owner declaration checklist (the schema remains normative):
|
||||
|
||||
1. Create the selected `rapp-*` repository and its versioned
|
||||
`declarations/rapp.yaml`; declare `kind: managed-workload-package`,
|
||||
`repo_family: rapp`, matching `repo`/`rapp_id`, contract version and truthful
|
||||
package readiness. A draft declaration must not claim verified runtime state.
|
||||
2. Set `ownership_repo: access-engine`; select `workload_identity.name` and
|
||||
`package_type: helm-managed-platform-service`; declare the existing
|
||||
`flex-auth-secrets-engine` deployable in `composition.member_repos`.
|
||||
Access supplies classification, criticality and actual runtime dependencies.
|
||||
3. Carry the existing chart/values source through an exact revision or thin
|
||||
owner delegate. Record current image, release, namespace, ServiceAccount,
|
||||
policy/registry pin, caller audience and target scope as unchanged extraction
|
||||
preconditions. No new image, policy expansion or resource rename is needed
|
||||
merely to establish the declaration.
|
||||
4. Supply schema-compliant rollout, smoke and rollback contracts for the
|
||||
retained release. Record previous Helm revision and digest before any future
|
||||
change. Declare `rail-kubernetes` and only an actually reviewed reef binding;
|
||||
no public exposure or production admission is inferred.
|
||||
5. Commit and run family validation, then Repo Manager exact resolution with
|
||||
the selected tuple and `--deployable flex-auth-secrets-engine`. Check global
|
||||
duplicate deployable findings separately; the resolver can resolve a pair
|
||||
while unrelated/global index errors remain.
|
||||
6. Platform independently admits the package-to-ServiceAccount custody binding
|
||||
and catalog pointer for `flex-auth-decision-envelope-signing-key`; Access
|
||||
and Secrets retain their own image/trust/consumer admission gates. Retain the
|
||||
existing unknown state until those required gates have actual receipts.
|
||||
|
||||
## Reviews and remaining owner obligations
|
||||
|
||||
This review establishes the Repo Manager integration contract and rejects the
|
||||
unmanaged bypass; it does not claim Access package selection, Railiance Master
|
||||
semantic/schema review, Platform custody admission, Secrets consumer trust, or
|
||||
founder native-action assent. Access integrates the concrete paragraph in T06
|
||||
and chooses its package identity. Railiance Master owns any requested exception
|
||||
or changed family semantics. Platform owns the exact custody/catalog join.
|
||||
Secrets owns independent installed verification under SECRETS-WP-0014-T03.
|
||||
The Custodian coordinator should track these named returns under CUST-WP-0080
|
||||
without inventing a Platform work item or owner assent.
|
||||
|
||||
Verification: existing `repo-manager/tests/test_workloads.py` passed all five
|
||||
tests (0.11 seconds), including missing declaration, duplicate deployable,
|
||||
exact-pair-only resolution and no repository fallback. The checked-out index
|
||||
was read without mutation. No new implementation tests are necessary for this
|
||||
documentation-only proposal; a real extraction requires the schema/family and
|
||||
package contract checks above. No native reads, deploys, credentials, external
|
||||
messages, commits, pushes or canonical owner-file edits were performed.
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
diff --git a/docs/native-signer-workload.json b/docs/native-signer-workload.json
|
||||
--- a/docs/native-signer-workload.json
|
||||
+++ b/docs/native-signer-workload.json
|
||||
@@ -42,8 +42,15 @@
|
||||
},
|
||||
"managed_package_limit": {
|
||||
"rapp_declaration_ref": null,
|
||||
- "status": "Existing managed-deployable migration debt remains unknown under security-zones_v0.1 \u00a76; no matching authoritative rapp declaration was established. Existing source tenancy declaration supplies owner identity binding but cannot substitute for managed-package admission.",
|
||||
- "no_fiction": "Do not invent a rapp tuple or describe this Helm-managed Deployment as an independent operational unit to bypass the rule."
|
||||
+ "status": "Access selected a minimal package for the independently rolled-back retained Secrets lifecycle PDP under FLEX-WP-0024-T06. Draft source package and exact-source renderer validate locally. Managed-workload identity remains unknown until the reviewed authoritative package declaration is committed and available to the exact Repo Manager resolver; source preparation grants no custody, placement, native rollout or trust.",
|
||||
+ "no_fiction": "Do not invent a rapp tuple or describe this Helm-managed Deployment as an independent operational unit to bypass the rule.",
|
||||
+ "proposed_owner_selected_reference": {
|
||||
+ "rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
+ "name": "flex-auth-secrets-engine",
|
||||
+ "deployable": "flex-auth-secrets-engine",
|
||||
+ "declaration_ref": "rapp-flex-auth-secrets-engine/declarations/rapp.yaml",
|
||||
+ "state": "Owner-selected draft extraction; not yet committed/published or independently admitted. Current authoritative resolution stays unknown until reviewed declaration exists."
|
||||
+ }
|
||||
},
|
||||
"unchanged_limits": [
|
||||
"tenancy and zones grades are unchanged",
|
||||
diff --git a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
--- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
+++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
@@ -272,3 +272,19 @@
|
||||
Retained-registry HTTP and production serve proofs and negative joins pass.
|
||||
R3 preserves precise notbefore pending separate Approval-owner timestamp fidelity
|
||||
repair; no rounding waiver, copied registry algorithm or empty-registry workaround.
|
||||
+
|
||||
+2026-10-16 owner package extraction preparation: Access selects
|
||||
+`rapp-flex-auth-secrets-engine` / workload `flex-auth-secrets-engine` / deployable
|
||||
+`flex-auth-secrets-engine`, owned by `access-engine`, for the independently
|
||||
+rolled-back retained Secrets lifecycle PDP only. The draft thin package pins
|
||||
+current chart/values source and retained image separately; release, namespace,
|
||||
+ServiceAccount, policy, registry, audience and caller binding are preserved.
|
||||
+Tenant/user/canary deployments and retired `rapp-secrets-engine` are excluded.
|
||||
+Normative Railiance Master schema/family validation and exact Repo Manager
|
||||
+resolution pass locally; global target ownership is unique. Existing unrelated
|
||||
+fleet declaration findings remain visible. Draft readiness and empty reef
|
||||
+bindings claim no placement. Package owner review, authoritative declaration
|
||||
+commit/availability and exact source-to-runtime join precede managed-workload
|
||||
+custody admission. No unmanaged bypass, remote repo/publication, native change
|
||||
+or future signer image activation is authorized by this extraction. T06 remains
|
||||
+`progress`; no second work item duplicates this bounded dependency.
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
{
|
||||
"ok": false,
|
||||
"contract": "helixforge.workload-reference/v1",
|
||||
"root": "/tmp/access-rapp-index-3lxeg_uq",
|
||||
"declaration_count": 14,
|
||||
"workloads": [
|
||||
{
|
||||
"rapp_id": "rapp-canned-prompts",
|
||||
"name": "canned-prompts",
|
||||
"declaration_repo": "rapp-canned-prompts",
|
||||
"declaration_path": "rapp-canned-prompts/declarations/rapp.yaml",
|
||||
"ownership_repo": "canned-prompts",
|
||||
"readiness_state": "verified",
|
||||
"data_classification": "internal",
|
||||
"criticality": "medium",
|
||||
"deployables": [
|
||||
"canned-prompts"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-canned-prompts",
|
||||
"name": "canned-prompts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"rapp_id": "rapp-core-hub",
|
||||
"name": "core-hub",
|
||||
"declaration_repo": "rapp-core-hub",
|
||||
"declaration_path": "rapp-core-hub/declarations/rapp.yaml",
|
||||
"ownership_repo": "core-hub",
|
||||
"readiness_state": "verified",
|
||||
"data_classification": "confidential",
|
||||
"criticality": "high",
|
||||
"deployables": [
|
||||
"core-hub-api",
|
||||
"core-hub-migrate",
|
||||
"hub-core-candidate",
|
||||
"hub-core-candidate-migrate",
|
||||
"repository-classification-publisher"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-core-hub",
|
||||
"name": "core-hub"
|
||||
}
|
||||
},
|
||||
{
|
||||
"rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
"name": "flex-auth-secrets-engine",
|
||||
"declaration_repo": "rapp-flex-auth-secrets-engine",
|
||||
"declaration_path": "rapp-flex-auth-secrets-engine/declarations/rapp.yaml",
|
||||
"ownership_repo": "access-engine",
|
||||
"readiness_state": "draft",
|
||||
"data_classification": "restricted",
|
||||
"criticality": "high",
|
||||
"deployables": [
|
||||
"flex-auth-secrets-engine"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
"name": "flex-auth-secrets-engine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"rapp_id": "rapp-issue-core",
|
||||
"name": "issue-core",
|
||||
"declaration_repo": "rapp-issue-core",
|
||||
"declaration_path": "rapp-issue-core/declarations/rapp.yaml",
|
||||
"ownership_repo": "issue-core",
|
||||
"readiness_state": "verified",
|
||||
"data_classification": "confidential",
|
||||
"criticality": "high",
|
||||
"deployables": [
|
||||
"issue-core"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-issue-core",
|
||||
"name": "issue-core"
|
||||
}
|
||||
},
|
||||
{
|
||||
"rapp_id": "rapp-openbao",
|
||||
"name": "openbao",
|
||||
"declaration_repo": "rapp-openbao",
|
||||
"declaration_path": "rapp-openbao/declarations/rapp.yaml",
|
||||
"ownership_repo": "railiance-platform",
|
||||
"readiness_state": "verified",
|
||||
"data_classification": "restricted",
|
||||
"criticality": "critical",
|
||||
"deployables": [
|
||||
"openbao",
|
||||
"openbao-ui-gateway"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-openbao",
|
||||
"name": "openbao"
|
||||
}
|
||||
},
|
||||
{
|
||||
"rapp_id": "rapp-playwright",
|
||||
"name": "playwright",
|
||||
"declaration_repo": "rapp-playwright",
|
||||
"declaration_path": "rapp-playwright/declarations/rapp.yaml",
|
||||
"ownership_repo": "railiance-platform",
|
||||
"readiness_state": "draft",
|
||||
"data_classification": "internal",
|
||||
"criticality": "low",
|
||||
"deployables": [
|
||||
"playwright"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-playwright",
|
||||
"name": "playwright"
|
||||
}
|
||||
},
|
||||
{
|
||||
"rapp_id": "rapp-policy-nexus",
|
||||
"name": "policy-nexus",
|
||||
"declaration_repo": "rapp-policy-nexus",
|
||||
"declaration_path": "rapp-policy-nexus/declarations/rapp.yaml",
|
||||
"ownership_repo": "policy-nexus",
|
||||
"readiness_state": "verified",
|
||||
"data_classification": "public",
|
||||
"criticality": "high",
|
||||
"deployables": [
|
||||
"policy-nexus-static"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-policy-nexus",
|
||||
"name": "policy-nexus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"rapp_id": "rapp-postgres",
|
||||
"name": "postgres",
|
||||
"declaration_repo": "rapp-postgres",
|
||||
"declaration_path": "rapp-postgres/declarations/rapp.yaml",
|
||||
"ownership_repo": "railiance-platform",
|
||||
"readiness_state": "verified",
|
||||
"data_classification": "restricted",
|
||||
"criticality": "critical",
|
||||
"deployables": [
|
||||
"platform-pg"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-postgres",
|
||||
"name": "postgres"
|
||||
}
|
||||
},
|
||||
{
|
||||
"rapp_id": "rapp-qonto",
|
||||
"name": "qonto",
|
||||
"declaration_repo": "rapp-qonto",
|
||||
"declaration_path": "rapp-qonto/declarations/rapp.yaml",
|
||||
"ownership_repo": "qonto-assistant",
|
||||
"readiness_state": "verified",
|
||||
"data_classification": "restricted",
|
||||
"criticality": "critical",
|
||||
"deployables": [
|
||||
"rapp-qonto"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-qonto",
|
||||
"name": "qonto"
|
||||
}
|
||||
},
|
||||
{
|
||||
"rapp_id": "rapp-sbom-nexus",
|
||||
"name": "sbom-nexus",
|
||||
"declaration_repo": "rapp-sbom-nexus",
|
||||
"declaration_path": "rapp-sbom-nexus/declarations/rapp.yaml",
|
||||
"ownership_repo": "sbom-nexus",
|
||||
"readiness_state": "draft",
|
||||
"data_classification": "confidential",
|
||||
"criticality": "high",
|
||||
"deployables": [
|
||||
"sbom-nexus"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-sbom-nexus",
|
||||
"name": "sbom-nexus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"rapp_id": "rapp-secrets-engine",
|
||||
"name": "secrets-engine",
|
||||
"declaration_repo": "rapp-secrets-engine",
|
||||
"declaration_path": "rapp-secrets-engine/declarations/rapp.yaml",
|
||||
"ownership_repo": "secrets-engine",
|
||||
"readiness_state": "retired",
|
||||
"data_classification": "restricted",
|
||||
"criticality": "high",
|
||||
"deployables": [
|
||||
"secrets-engine-cli"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-secrets-engine",
|
||||
"name": "secrets-engine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"rapp_id": "rapp-telemetry",
|
||||
"name": "telemetry",
|
||||
"declaration_repo": "rapp-telemetry",
|
||||
"declaration_path": "rapp-telemetry/declarations/rapp.yaml",
|
||||
"ownership_repo": "railiance-telemetry",
|
||||
"readiness_state": "deployed",
|
||||
"data_classification": "confidential",
|
||||
"criticality": "high",
|
||||
"deployables": [
|
||||
"alertmanager-telemetry-alertmanager",
|
||||
"prometheus-telemetry-prometheus",
|
||||
"telemetry-ack",
|
||||
"telemetry-admission-create",
|
||||
"telemetry-admission-patch",
|
||||
"telemetry-grafana",
|
||||
"telemetry-kube-state-metrics",
|
||||
"telemetry-operator"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-telemetry",
|
||||
"name": "telemetry"
|
||||
}
|
||||
},
|
||||
{
|
||||
"rapp_id": "rapp-tenant-engine",
|
||||
"name": "tenant-engine",
|
||||
"declaration_repo": "rapp-tenant-engine",
|
||||
"declaration_path": "rapp-tenant-engine/declarations/rapp.yaml",
|
||||
"ownership_repo": "tenant-engine",
|
||||
"readiness_state": "verified",
|
||||
"data_classification": "confidential",
|
||||
"criticality": "high",
|
||||
"deployables": [
|
||||
"tenant-engine"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-tenant-engine",
|
||||
"name": "tenant-engine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"rapp_id": "rapp-user-engine",
|
||||
"name": "user-engine",
|
||||
"declaration_repo": "rapp-user-engine",
|
||||
"declaration_path": "rapp-user-engine/declarations/rapp.yaml",
|
||||
"ownership_repo": "user-engine",
|
||||
"readiness_state": "verified",
|
||||
"data_classification": "confidential",
|
||||
"criticality": "high",
|
||||
"deployables": [
|
||||
"user-engine",
|
||||
"user-engine-pg"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-user-engine",
|
||||
"name": "user-engine"
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": "declaration_missing",
|
||||
"path": "/tmp/access-rapp-index-3lxeg_uq/rapp-forgejo/declarations/rapp.yaml",
|
||||
"message": "rapp repository has no authoritative declarations/rapp.yaml"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"schema": "access-engine.retained-package-extraction.v1",
|
||||
"package": "rapp-flex-auth-secrets-engine",
|
||||
"package_source": "/tmp/access-rapp-extraction-20261016/rapp-flex-auth-secrets-engine",
|
||||
"tar_sha256": "766279e86749bde4e25aafcb3989134e9879564667dd46a55be29bc995f66d8d",
|
||||
"files_sha256": {
|
||||
".repo-classification.yaml": "99a5c80dda99a732563dbb64b8ce8b8663d9d619b919f9fed96cb7a63598fbb4",
|
||||
"AGENTS.md": "a02928b531ce61765bfb09bb1abea548ddcb589ecef7c762a1851d8e7745c7c7",
|
||||
"CLAUDE.md": "70f13f7bdd8c420fe2820ac879e464e7333c4547a5b0adf1b5e5a7a14113fa00",
|
||||
"INTENT.md": "a40ff791a4070bcdc59b7f889412d2558c5cd943308890bc0bf0690ea78c3f67",
|
||||
"README.md": "d8f096b22d4e67ce6eb2a2e201720fbbf3bc54059fec98ddf3f6ec4229087361",
|
||||
"SCOPE.md": "6167744e2b3f4b82a38f4950463068966fe595c7d62d3674813a0e8ed66789de",
|
||||
"declarations/rapp.yaml": "0b722431bc5162dd86f72b15f12424e149aca8f2a8790adf2c57e8511ea73b4c",
|
||||
"docs/operations.md": "690c2140d86c68edcc09d9c9acda10f0a4f14723c4236d02dd9f832e74651d6e",
|
||||
"source-pin.json": "d28e8ce2f83ebb8dd8bfd2cd03d8308405d189a77d05f7294439ba1da31e1818",
|
||||
"tests/test_render.py": "5e7cfe42249eda989813275b5cd307e7a2b0be9696443e566d30578b720c8c6a",
|
||||
"tools/render.py": "93ca85ddd8036952ae98e31ac1f36ebfb1ac9ad0a2f73b1e90dcf067f4ae544c",
|
||||
"workplans/README.md": "29f2194b1cf0981172cd28928f74224c72778c1771413006a92e4b4ed8214511",
|
||||
"workplans/archived/.gitkeep": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"access_owner_patch": {
|
||||
"base": "2b064eee510640224274ccdc7cefceb1ae13162e",
|
||||
"sha256": "f611c2a963adbf0a99504eca323490ddc3c85bca4a3cbb3abbae7b3a6dcef965"
|
||||
},
|
||||
"receipt_hashes": {
|
||||
"resolution.json": "b0dc341f7e2a279e3e6d9e1000135a6c9d771fb54b7ab88655ad1d2e8f42be2e",
|
||||
"family-focused.log": "c70d046d2ed4d087a7a94f7d25370ecc24ce6b1d972dc22004e82d04da727688",
|
||||
"owner-review.json": "e334d09487412e9848257d348e63fdd60f01dc07053d1b2c81b93b3783a7871e",
|
||||
"global-index.json": "c69c28e3c5f4423f1f52401add8294ae9899f46d62cbc22bf68aa61a9023b72a",
|
||||
"family-global.log": "070452e060746209bf617a88acc1814b196790b77bb6b5a6cc0dc3b5cdd773b2",
|
||||
"access-owner-reference.patch": "f611c2a963adbf0a99504eca323490ddc3c85bca4a3cbb3abbae7b3a6dcef965",
|
||||
"retained-render.yaml": "f979f7d05d5b0a54e3b8ac7c413aa4d401139f991b43412cdbde3d63249b0dd9",
|
||||
"render-tests.log": "22eab9efa0566d6609561b1fbfd08cef2611399240e944114cab929a811b979d"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"schema": "access-engine.retained-package-owner-selection.v1",
|
||||
"owner": "Access Engine",
|
||||
"work_item": "FLEX-WP-0024-T06",
|
||||
"selected_reference": {
|
||||
"rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
"name": "flex-auth-secrets-engine",
|
||||
"deployable": "flex-auth-secrets-engine"
|
||||
},
|
||||
"ownership_repo": "access-engine",
|
||||
"basis": "One existing independently rolled-back Secrets lifecycle PDP release; no shared rollback fate with other Access deployments. RMASTER-ADR-0007 accepted-2 and Repo Manager ADR-004 exact declaration contract.",
|
||||
"classification": "restricted",
|
||||
"criticality": "high",
|
||||
"classification_basis": "Authorization requests and internal decision records; future admitted signing/reviewer credentials are restricted. High criticality follows existing Access app owner classification; sole PDP failure blocks consumer write paths.",
|
||||
"readiness_state": "draft",
|
||||
"bound_reefs": [],
|
||||
"unchanged_runtime": {
|
||||
"release": "flex-auth-secrets-engine",
|
||||
"namespace": "flex-auth",
|
||||
"service_account": "flex-auth-secrets-engine",
|
||||
"policy": "secrets-engine.catalog-lane.lifecycle@2",
|
||||
"policy_artifact_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4",
|
||||
"image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd",
|
||||
"caller": "system:serviceaccount:secrets-engine:secrets-engine",
|
||||
"audience": "flex-auth"
|
||||
},
|
||||
"source_revision": "2b064eee510640224274ccdc7cefceb1ae13162e",
|
||||
"source_vs_image": "Exact committed chart/values source is separate from observed retained image build dd8dd517438b876fdadf27770e3f7e7f55ea69cf. This extraction never labels the current predecessor image as signed native candidate.",
|
||||
"validation": [
|
||||
"Normative family validator focused root: 2 declarations, 0 errors/warnings.",
|
||||
"Actual Repo Manager index/resolver: exact tuple/deployable resolves; wrong retired tuple/name/deployable remains unknown.",
|
||||
"Full checked-out family scan + draft package: 18 declarations, existing rapp-forgejo declaration_missing error and reef projection warning retained.",
|
||||
"Actual global resolver overlay: target deployable has exactly one owner; no new duplicate deployable/reference.",
|
||||
"Actual exact-archive Helm render passes; mutable revision, archive hash, image, SA, caller and audience pin mutations refuse with no manifest."
|
||||
],
|
||||
"limits": [
|
||||
"Authoritative managed identity remains unknown until reviewed declaration is committed and available in resolver inventory.",
|
||||
"Package owner/framework review, remote repo creation/publication, Fabric registration, placement, custody and native rollout are separate; no receipt fabricated.",
|
||||
"Empty reef bindings intentionally claim no new placement; historical native runtime observation does not promote source readiness.",
|
||||
"Package docs contain bounded future operator commands; no native command executed.",
|
||||
"Retained current image is unsigned predecessor; future signer/outbox activation requires reviewed hardening successor and exact CI immutable digest."
|
||||
],
|
||||
"immutable_candidate": {
|
||||
"candidate_digest": null,
|
||||
"reason": "Hardening successor canonical commit/publication not yet supplied. Anonymous existing Forgejo API actions/runs metadata route returned404; this does not establish absent CI. No login/token read performed.",
|
||||
"read_only_route": "https://forgejo.coulomb.social/api/v1/repos/coulomb/access-engine/actions/runs?limit=5",
|
||||
"required_input": "Exact published hardening revision plus existing authenticated CI run id/status and existing runner docker image RepoDigests/revision label receipt; no new credential required or publication invoked."
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
diff --git a/docs/native-signer-workload.json b/docs/native-signer-workload.json
|
||||
--- a/docs/native-signer-workload.json
|
||||
+++ b/docs/native-signer-workload.json
|
||||
@@ -42,8 +42,15 @@
|
||||
},
|
||||
"managed_package_limit": {
|
||||
"rapp_declaration_ref": null,
|
||||
- "status": "Existing managed-deployable migration debt remains unknown under security-zones_v0.1 \u00a76; no matching authoritative rapp declaration was established. Existing source tenancy declaration supplies owner identity binding but cannot substitute for managed-package admission.",
|
||||
- "no_fiction": "Do not invent a rapp tuple or describe this Helm-managed Deployment as an independent operational unit to bypass the rule."
|
||||
+ "status": "Access selected a minimal package for the independently rolled-back retained Secrets lifecycle PDP under FLEX-WP-0024-T06. Draft source package and exact-source renderer validate locally. Managed-workload identity remains unknown until the reviewed authoritative package declaration is committed and available to the exact Repo Manager resolver; source preparation grants no custody, placement, native rollout or trust.",
|
||||
+ "no_fiction": "Do not invent a rapp tuple or describe this Helm-managed Deployment as an independent operational unit to bypass the rule.",
|
||||
+ "proposed_owner_selected_reference": {
|
||||
+ "rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
+ "name": "flex-auth-secrets-engine",
|
||||
+ "deployable": "flex-auth-secrets-engine",
|
||||
+ "declaration_ref": "rapp-flex-auth-secrets-engine/declarations/rapp.yaml",
|
||||
+ "state": "Owner-selected draft extraction; not yet committed/published or independently admitted. Current authoritative resolution stays unknown until reviewed declaration exists."
|
||||
+ }
|
||||
},
|
||||
"unchanged_limits": [
|
||||
"tenancy and zones grades are unchanged",
|
||||
diff --git a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
--- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
+++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
@@ -283,3 +283,19 @@
|
||||
proofs are part of the owner package. No new service, live mount, deletion or
|
||||
custody grant; exact published successor, new PVC/PV retention/managed-package
|
||||
owner disposition and independent consumer admission remain prerequisites.
|
||||
+
|
||||
+2026-10-16 owner package extraction preparation: Access selects
|
||||
+`rapp-flex-auth-secrets-engine` / workload `flex-auth-secrets-engine` / deployable
|
||||
+`flex-auth-secrets-engine`, owned by `access-engine`, for the independently
|
||||
+rolled-back retained Secrets lifecycle PDP only. The draft thin package pins
|
||||
+current chart/values source and retained image separately; release, namespace,
|
||||
+ServiceAccount, policy, registry, audience and caller binding are preserved.
|
||||
+Tenant/user/canary deployments and retired `rapp-secrets-engine` are excluded.
|
||||
+Normative Railiance Master schema/family validation and exact Repo Manager
|
||||
+resolution pass locally; global target ownership is unique. Existing unrelated
|
||||
+fleet declaration findings remain visible. Draft readiness and empty reef
|
||||
+bindings claim no placement. Package owner review, authoritative declaration
|
||||
+commit/availability and exact source-to-runtime join precede managed-workload
|
||||
+custody admission. No unmanaged bypass, remote repo/publication, native change
|
||||
+or future signer image activation is authorized by this extraction. T06 remains
|
||||
+`progress`; no second work item duplicates this bounded dependency.
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
diff --git a/docs/native-signer-workload.json b/docs/native-signer-workload.json
|
||||
--- a/docs/native-signer-workload.json
|
||||
+++ b/docs/native-signer-workload.json
|
||||
@@ -33,17 +33,33 @@
|
||||
"kid"
|
||||
],
|
||||
"proposed_catalog_workload_reference": {
|
||||
- "name": "flex-auth",
|
||||
- "declaration_ref": "access-engine/tenancy.yaml#/workload_identity",
|
||||
+ "rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
+ "name": "flex-auth-secrets-engine",
|
||||
+ "deployable": "flex-auth-secrets-engine",
|
||||
+ "declaration_ref": "rapp-flex-auth-secrets-engine/declarations/rapp.yaml",
|
||||
+ "declaration_commit": "d52f811ed5ec0be27c9c367e71d476125b3228be",
|
||||
"exact_binding": "system:serviceaccount:flex-auth:flex-auth-secrets-engine",
|
||||
- "resolution": "unknown until owner custody and managed-package admission; do not mark resolvable merely from source binding"
|
||||
+ "resolution": "Local source tuple resolves draft. This is an owner-proposed catalog pointer only; actual Platform catalog/custody is unchanged and remains pending admission."
|
||||
},
|
||||
"key_created_or_admitted": false
|
||||
},
|
||||
"managed_package_limit": {
|
||||
- "rapp_declaration_ref": null,
|
||||
- "status": "Existing managed-deployable migration debt remains unknown under security-zones_v0.1 \u00a76; no matching authoritative rapp declaration was established. Existing source tenancy declaration supplies owner identity binding but cannot substitute for managed-package admission.",
|
||||
- "no_fiction": "Do not invent a rapp tuple or describe this Helm-managed Deployment as an independent operational unit to bypass the rule."
|
||||
+ "rapp_declaration_ref": "rapp-flex-auth-secrets-engine/declarations/rapp.yaml",
|
||||
+ "status": "Canonical local package declaration now exists at exact recorded commit/hash. Repo Manager resolves the exact tuple/deployable under /home/worsch with ownership_repo access-engine and readiness draft. Existing unrelated rapp-forgejo declaration_missing remains visible. This source resolution does not claim central registration, private source access/placement, reef admission, signer custody, native deployment or independent consumer trust.",
|
||||
+ "no_fiction": "Do not invent a rapp tuple or describe this Helm-managed Deployment as an independent operational unit to bypass the rule.",
|
||||
+ "owner_selected_reference": {
|
||||
+ "rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
+ "name": "flex-auth-secrets-engine",
|
||||
+ "deployable": "flex-auth-secrets-engine",
|
||||
+ "declaration_ref": "rapp-flex-auth-secrets-engine/declarations/rapp.yaml",
|
||||
+ "declaration_commit": "d52f811ed5ec0be27c9c367e71d476125b3228be",
|
||||
+ "declaration_tree": "d4fbad71bdc8ef870603526ccd1f53195d0322af",
|
||||
+ "declaration_sha256": "0b722431bc5162dd86f72b15f12424e149aca8f2a8790adf2c57e8511ea73b4c",
|
||||
+ "local_resolver_root": "/home/worsch",
|
||||
+ "local_resolution": "resolved",
|
||||
+ "readiness_state": "draft",
|
||||
+ "scope": "Committed canonical local declaration and exact local source resolution only; no central/private read-placement, reef binding, custody, live deployment or consumer trust admission."
|
||||
+ }
|
||||
},
|
||||
"unchanged_limits": [
|
||||
"tenancy and zones grades are unchanged",
|
||||
diff --git a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
--- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
+++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
@@ -283,3 +283,22 @@
|
||||
proofs are part of the owner package. No new service, live mount, deletion or
|
||||
custody grant; exact published successor, new PVC/PV retention/managed-package
|
||||
owner disposition and independent consumer admission remain prerequisites.
|
||||
+
|
||||
+2026-10-16 owner package extraction record: canonical local package
|
||||
+`rapp-flex-auth-secrets-engine` now exists at commit
|
||||
+`d52f811ed5ec0be27c9c367e71d476125b3228be`, tree
|
||||
+`d4fbad71bdc8ef870603526ccd1f53195d0322af`; authoritative declaration
|
||||
+`declarations/rapp.yaml` SHA256 `0b722431bc5162dd86f72b15f12424e149aca8f2a8790adf2c57e8511ea73b4c`.
|
||||
+Repo Manager exact local resolution under `/home/worsch` returns
|
||||
+`rapp-flex-auth-secrets-engine` / workload `flex-auth-secrets-engine` /
|
||||
+deployable `flex-auth-secrets-engine`, ownership `access-engine`, readiness
|
||||
+`draft`. The retained lifecycle PDP is the only rollback boundary claimed;
|
||||
+tenant/user/canary and retired `rapp-secrets-engine` remain excluded. Normative
|
||||
+focused family validation passes; existing unrelated global rapp-forgejo error
|
||||
+and reef projection warning remain visible. Package extraction keeps source
|
||||
+`2b064eee510640224274ccdc7cefceb1ae13162e` and current image
|
||||
+`sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd`
|
||||
+unchanged. Exact local source resolution is no central/private read-placement,
|
||||
+reef binding, custody, native rollout or consumer trust admission. Future signer
|
||||
+image/config adoption remains separate. T06 remains `progress`; no second work
|
||||
+item duplicates this bounded dependency.
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"ok": true,
|
||||
"contract": "helixforge.workload-reference/v1",
|
||||
"status": "resolved",
|
||||
"reference": {
|
||||
"rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
"name": "flex-auth-secrets-engine",
|
||||
"deployable": "flex-auth-secrets-engine"
|
||||
},
|
||||
"workload": {
|
||||
"rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
"name": "flex-auth-secrets-engine",
|
||||
"declaration_repo": "rapp-flex-auth-secrets-engine",
|
||||
"declaration_path": "rapp-flex-auth-secrets-engine/declarations/rapp.yaml",
|
||||
"ownership_repo": "access-engine",
|
||||
"readiness_state": "draft",
|
||||
"data_classification": "restricted",
|
||||
"criticality": "high",
|
||||
"deployables": [
|
||||
"flex-auth-secrets-engine"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
"name": "flex-auth-secrets-engine"
|
||||
}
|
||||
},
|
||||
"index_errors": [
|
||||
{
|
||||
"code": "declaration_missing",
|
||||
"path": "/home/worsch/rapp-forgejo/declarations/rapp.yaml",
|
||||
"message": "rapp repository has no authoritative declarations/rapp.yaml"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"focused_index": {
|
||||
"ok": true,
|
||||
"contract": "helixforge.workload-reference/v1",
|
||||
"root": "/tmp/access-rapp-extraction-20261016",
|
||||
"declaration_count": 1,
|
||||
"workloads": [
|
||||
{
|
||||
"rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
"name": "flex-auth-secrets-engine",
|
||||
"declaration_repo": "rapp-flex-auth-secrets-engine",
|
||||
"declaration_path": "rapp-flex-auth-secrets-engine/declarations/rapp.yaml",
|
||||
"ownership_repo": "access-engine",
|
||||
"readiness_state": "draft",
|
||||
"data_classification": "restricted",
|
||||
"criticality": "high",
|
||||
"deployables": [
|
||||
"flex-auth-secrets-engine"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
"name": "flex-auth-secrets-engine"
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": []
|
||||
},
|
||||
"exact_reference": {
|
||||
"ok": true,
|
||||
"contract": "helixforge.workload-reference/v1",
|
||||
"status": "resolved",
|
||||
"reference": {
|
||||
"rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
"name": "flex-auth-secrets-engine",
|
||||
"deployable": "flex-auth-secrets-engine"
|
||||
},
|
||||
"workload": {
|
||||
"rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
"name": "flex-auth-secrets-engine",
|
||||
"declaration_repo": "rapp-flex-auth-secrets-engine",
|
||||
"declaration_path": "rapp-flex-auth-secrets-engine/declarations/rapp.yaml",
|
||||
"ownership_repo": "access-engine",
|
||||
"readiness_state": "draft",
|
||||
"data_classification": "restricted",
|
||||
"criticality": "high",
|
||||
"deployables": [
|
||||
"flex-auth-secrets-engine"
|
||||
],
|
||||
"reference": {
|
||||
"rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
"name": "flex-auth-secrets-engine"
|
||||
}
|
||||
},
|
||||
"index_errors": []
|
||||
},
|
||||
"negative_references": [
|
||||
{
|
||||
"ok": true,
|
||||
"contract": "helixforge.workload-reference/v1",
|
||||
"status": "unknown",
|
||||
"reference": {
|
||||
"rapp_id": "rapp-secrets-engine",
|
||||
"name": "flex-auth-secrets-engine"
|
||||
},
|
||||
"reason": "not_found",
|
||||
"index_errors": []
|
||||
},
|
||||
{
|
||||
"ok": true,
|
||||
"contract": "helixforge.workload-reference/v1",
|
||||
"status": "unknown",
|
||||
"reference": {
|
||||
"rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
"name": "flex-auth"
|
||||
},
|
||||
"reason": "not_found",
|
||||
"index_errors": []
|
||||
},
|
||||
{
|
||||
"ok": true,
|
||||
"contract": "helixforge.workload-reference/v1",
|
||||
"status": "unknown",
|
||||
"reference": {
|
||||
"rapp_id": "rapp-flex-auth-secrets-engine",
|
||||
"name": "flex-auth-secrets-engine",
|
||||
"deployable": "flex-auth-user-engine"
|
||||
},
|
||||
"reason": "deployable_not_declared",
|
||||
"index_errors": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
---
|
||||
# Source: flex-auth/templates/networkpolicy.yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: flex-auth-secrets-engine
|
||||
labels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
app.kubernetes.io/instance: flex-auth-secrets-engine
|
||||
app.kubernetes.io/part-of: flex-auth
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
egress:
|
||||
- ports:
|
||||
- port: 443
|
||||
protocol: TCP
|
||||
- port: 6443
|
||||
protocol: TCP
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: secrets-engine
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: secrets-engine
|
||||
ports:
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
---
|
||||
# Source: flex-auth/templates/serviceaccount.yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: flex-auth-secrets-engine
|
||||
labels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
app.kubernetes.io/instance: flex-auth-secrets-engine
|
||||
app.kubernetes.io/part-of: flex-auth
|
||||
automountServiceAccountToken: false
|
||||
---
|
||||
# Source: flex-auth/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: flex-auth-secrets-engine-tokenreviewer
|
||||
labels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
app.kubernetes.io/instance: flex-auth-secrets-engine
|
||||
app.kubernetes.io/part-of: flex-auth
|
||||
rules:
|
||||
- apiGroups:
|
||||
- authentication.k8s.io
|
||||
resources:
|
||||
- tokenreviews
|
||||
verbs:
|
||||
- create
|
||||
---
|
||||
# Source: flex-auth/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: flex-auth-secrets-engine-tokenreviewer
|
||||
labels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
app.kubernetes.io/instance: flex-auth-secrets-engine
|
||||
app.kubernetes.io/part-of: flex-auth
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: flex-auth-secrets-engine-tokenreviewer
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: flex-auth-secrets-engine
|
||||
namespace: flex-auth
|
||||
---
|
||||
# Source: flex-auth/templates/service.yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: flex-auth-secrets-engine
|
||||
labels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
app.kubernetes.io/instance: flex-auth-secrets-engine
|
||||
app.kubernetes.io/part-of: flex-auth
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
---
|
||||
# Source: flex-auth/templates/deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: flex-auth-secrets-engine
|
||||
labels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
app.kubernetes.io/instance: flex-auth-secrets-engine
|
||||
app.kubernetes.io/part-of: flex-auth
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
app.kubernetes.io/instance: flex-auth-secrets-engine
|
||||
app.kubernetes.io/part-of: flex-auth
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
serviceAccountName: flex-auth-secrets-engine
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
fsGroup: 65532
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: flex-auth
|
||||
image: forgejo.coulomb.social/coulomb/flex-auth@sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- serve
|
||||
- --addr
|
||||
- 0.0.0.0:8080
|
||||
- --registry
|
||||
- /opt/flex-auth/examples/secrets-engine/registry_snapshot.json
|
||||
- --policy
|
||||
- /opt/flex-auth/examples/secrets-engine/policy_package.md
|
||||
- --caller-audience
|
||||
- "flex-auth"
|
||||
- --caller-auth-mode
|
||||
- "enforce"
|
||||
- --caller-kubernetes-url
|
||||
- "https://10.43.0.1"
|
||||
- --caller-binding
|
||||
- "secrets-engine=system:serviceaccount:secrets-engine:secrets-engine"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
periodSeconds: 20
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
resources:
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 192Mi
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 32Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: true
|
||||
volumeMounts:
|
||||
- mountPath: /var/run/secrets/flex-auth-reviewer
|
||||
name: flex-auth-reviewer
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: flex-auth-reviewer
|
||||
projected:
|
||||
defaultMode: 0440
|
||||
sources:
|
||||
- serviceAccountToken:
|
||||
expirationSeconds: 3600
|
||||
path: token
|
||||
- configMap:
|
||||
name: kube-root-ca.crt
|
||||
items:
|
||||
- key: ca.crt
|
||||
path: ca.crt
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
--- a/workplans/RMASTER-WP-0025-universal-rapp-workload-coverage.md
|
||||
+++ b/workplans/RMASTER-WP-0025-universal-rapp-workload-coverage.md
|
||||
@@ -122,3 +122,26 @@
|
||||
deployables have no exemption from the exactly-one-rapp invariant;
|
||||
non-workload subjects retain native identity; uncovered managed deployables
|
||||
remain visible migration debt rather than receiving inferred identity.
|
||||
+
|
||||
+## Supplemental source review — retained Access lifecycle PDP (2026-10-16)
|
||||
+
|
||||
+The delegated Railiance Master family-owner review accepts the draft extraction
|
||||
+`rapp-flex-auth-secrets-engine` at archive SHA256
|
||||
+`766279e86749bde4e25aafcb3989134e9879564667dd46a55be29bc995f66d8d`
|
||||
+and declaration SHA256
|
||||
+`0b722431bc5162dd86f72b15f12424e149aca8f2a8790adf2c57e8511ea73b4c`
|
||||
+for bounded source integration. The one independently rolled-back deployable
|
||||
+`flex-auth-secrets-engine` preserves `ownership_repo: access-engine`; no broader
|
||||
+Access grouping or new family semantics is introduced.
|
||||
+
|
||||
+Independent focused normative validation and render/refusal checks pass. Global
|
||||
+validation retains the pre-existing `rapp-forgejo` missing declaration and derived
|
||||
+reef warning; no new finding appears. Readiness remains draft with no reef
|
||||
+binding. Runtime workload identity remains unknown until an authoritative
|
||||
+committed declaration is present in resolver inventory. This is no native
|
||||
+admission, custody, signer trust, successor image, or rollout approval.
|
||||
+
|
||||
+Detailed exact-source review and independent logs are retained in Custodian
|
||||
+`docs/changes/netkingdom-target-2026-10-16/access-package-identity/family-review/`.
|
||||
+Implementation remains Access `FLEX-WP-0024-T06`. This supplemental review does
|
||||
+not reopen the completed framework contract tasks.
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"disposition.md": "6a1857e0954e1656cddfe9bab6fe7e67dc76ff223021a52adcf0801eb68c8ffc",
|
||||
"family-baseline.log": "968b198e16bbec72cd63d6a0cf3657154b66e3f2c4e460f85eeca6a7b8b15e07",
|
||||
"family-focused.log": "0e784f5213e342822f26df7340bdbacd6800b6536706353008d94de17ea48833",
|
||||
"family-global.log": "ca926c29a953b3ce9051e79fda287af189b19d62bb7eac57e612ae7ece80bde6",
|
||||
"family-self-test.log": "ef25dba2cf3db07e999caa614e2a8c30781783c2f61a106461485269618420ef",
|
||||
"master-existing-workrecord.patch": "a446da405464f46f31ba000ca72a093972ba28f4ff04dc45cd64b228e2e1033a",
|
||||
"render-tests.log": "db9a258918c359aa1f73390570f2b47d5c8b5cb9f95180be1dee0f285662824d",
|
||||
"retained-render.yaml": "f979f7d05d5b0a54e3b8ac7c413aa4d401139f991b43412cdbde3d63249b0dd9",
|
||||
"review.json": "5f6019ad8d4b5728717420e0c9e7dbd5e95b7e825517e0cadad4828d49992ba7"
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
---
|
||||
# Source: flex-auth/templates/networkpolicy.yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: flex-auth-secrets-engine
|
||||
labels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
app.kubernetes.io/instance: flex-auth-secrets-engine
|
||||
app.kubernetes.io/part-of: flex-auth
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
egress:
|
||||
- ports:
|
||||
- port: 443
|
||||
protocol: TCP
|
||||
- port: 6443
|
||||
protocol: TCP
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: secrets-engine
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: secrets-engine
|
||||
ports:
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
---
|
||||
# Source: flex-auth/templates/serviceaccount.yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: flex-auth-secrets-engine
|
||||
labels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
app.kubernetes.io/instance: flex-auth-secrets-engine
|
||||
app.kubernetes.io/part-of: flex-auth
|
||||
automountServiceAccountToken: false
|
||||
---
|
||||
# Source: flex-auth/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: flex-auth-secrets-engine-tokenreviewer
|
||||
labels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
app.kubernetes.io/instance: flex-auth-secrets-engine
|
||||
app.kubernetes.io/part-of: flex-auth
|
||||
rules:
|
||||
- apiGroups:
|
||||
- authentication.k8s.io
|
||||
resources:
|
||||
- tokenreviews
|
||||
verbs:
|
||||
- create
|
||||
---
|
||||
# Source: flex-auth/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: flex-auth-secrets-engine-tokenreviewer
|
||||
labels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
app.kubernetes.io/instance: flex-auth-secrets-engine
|
||||
app.kubernetes.io/part-of: flex-auth
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: flex-auth-secrets-engine-tokenreviewer
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: flex-auth-secrets-engine
|
||||
namespace: flex-auth
|
||||
---
|
||||
# Source: flex-auth/templates/service.yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: flex-auth-secrets-engine
|
||||
labels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
app.kubernetes.io/instance: flex-auth-secrets-engine
|
||||
app.kubernetes.io/part-of: flex-auth
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
---
|
||||
# Source: flex-auth/templates/deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: flex-auth-secrets-engine
|
||||
labels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
app.kubernetes.io/instance: flex-auth-secrets-engine
|
||||
app.kubernetes.io/part-of: flex-auth
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: flex-auth-secrets-engine
|
||||
app.kubernetes.io/instance: flex-auth-secrets-engine
|
||||
app.kubernetes.io/part-of: flex-auth
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
serviceAccountName: flex-auth-secrets-engine
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
fsGroup: 65532
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: flex-auth
|
||||
image: forgejo.coulomb.social/coulomb/flex-auth@sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- serve
|
||||
- --addr
|
||||
- 0.0.0.0:8080
|
||||
- --registry
|
||||
- /opt/flex-auth/examples/secrets-engine/registry_snapshot.json
|
||||
- --policy
|
||||
- /opt/flex-auth/examples/secrets-engine/policy_package.md
|
||||
- --caller-audience
|
||||
- "flex-auth"
|
||||
- --caller-auth-mode
|
||||
- "enforce"
|
||||
- --caller-kubernetes-url
|
||||
- "https://10.43.0.1"
|
||||
- --caller-binding
|
||||
- "secrets-engine=system:serviceaccount:secrets-engine:secrets-engine"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
periodSeconds: 20
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
resources:
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 192Mi
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 32Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: true
|
||||
volumeMounts:
|
||||
- mountPath: /var/run/secrets/flex-auth-reviewer
|
||||
name: flex-auth-reviewer
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: flex-auth-reviewer
|
||||
projected:
|
||||
defaultMode: 0440
|
||||
sources:
|
||||
- serviceAccountToken:
|
||||
expirationSeconds: 3600
|
||||
path: token
|
||||
- configMap:
|
||||
name: kube-root-ca.crt
|
||||
items:
|
||||
- key: ca.crt
|
||||
path: ca.crt
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
{
|
||||
"schema": "access-engine.local-serve-consumer-proof.v1",
|
||||
"results": {
|
||||
"signed_cli_http_r2_consumer_and_request_lifetime_validation": "pass",
|
||||
"returned_typed_authenticated_envelope_equals_recorded_envelope": "pass",
|
||||
"tampered_actual_signed_response_refusal": "pass",
|
||||
"unsigned_actual_cli_http_response_refusal": "pass",
|
||||
"group_writable_seed_cli_startup_refusal": "pass"
|
||||
},
|
||||
"independent_source_policy_pin": {
|
||||
"policy_package": "secrets-engine.catalog-lane.lifecycle",
|
||||
"policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4",
|
||||
"policy_version": "v2"
|
||||
},
|
||||
"synthetic_public_key": "O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik",
|
||||
"synthetic_key_only": true,
|
||||
"producer_binary_sha256": "d81bd409bc01670be7b912b7ed06621b843fe4d6d5b567449f6f32da2d2dc553",
|
||||
"consumer_verifier_sha256": "f2c2c75d07c3b21a9248afaf17f3cf050b53f44eea525fca4bb44d29333c92e4",
|
||||
"consumer_source_manifest_sha256": "de2f6912eaedb0a71d4b56b033684a75a6b9e3230cbdcbc709e2ea559aad333a",
|
||||
"signed_envelope": {
|
||||
"id": "decision:414734bb30381ff7",
|
||||
"contract_version": "flex-auth.decision-record.v1",
|
||||
"request_id": "check:secrets-engine-rotate",
|
||||
"effect": "allow",
|
||||
"reason": "catalog_lane_policy_matched",
|
||||
"matched_policy_version": "v2",
|
||||
"matched_rule": "catalog_lane_policy_matched",
|
||||
"resource": {
|
||||
"id": "lane:glas-primary",
|
||||
"type": "secret-catalog-lane",
|
||||
"system": "secrets-engine",
|
||||
"tenant": "tenant:platform",
|
||||
"attributes": {
|
||||
"auth_targets": [],
|
||||
"fields": [
|
||||
"password"
|
||||
],
|
||||
"policy_targets": [],
|
||||
"stage": "prod"
|
||||
}
|
||||
},
|
||||
"subject": {
|
||||
"id": "secrets-engine",
|
||||
"type": "service",
|
||||
"tenant": "tenant:platform",
|
||||
"attributes": {
|
||||
"description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.",
|
||||
"display_name": "secrets-engine service principal",
|
||||
"groups": [
|
||||
"group:secrets-engine-lane-operators"
|
||||
],
|
||||
"organization_relation": "ServiceProvider",
|
||||
"roles": [
|
||||
"Operator"
|
||||
]
|
||||
}
|
||||
},
|
||||
"binding": {
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "secrets-engine",
|
||||
"type": "service",
|
||||
"tenant": "tenant:platform",
|
||||
"attributes": {
|
||||
"description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.",
|
||||
"display_name": "secrets-engine service principal",
|
||||
"groups": [
|
||||
"group:secrets-engine-lane-operators"
|
||||
],
|
||||
"organization_relation": "ServiceProvider",
|
||||
"roles": [
|
||||
"Operator"
|
||||
]
|
||||
}
|
||||
},
|
||||
"action": "rotate",
|
||||
"resource": {
|
||||
"id": "lane:glas-primary",
|
||||
"type": "secret-catalog-lane",
|
||||
"system": "secrets-engine",
|
||||
"tenant": "tenant:platform",
|
||||
"attributes": {
|
||||
"auth_targets": [],
|
||||
"fields": [
|
||||
"password"
|
||||
],
|
||||
"policy_targets": [],
|
||||
"stage": "prod"
|
||||
}
|
||||
},
|
||||
"request_digest": "sha256:de67324f54187055307a833235f83ced9fcd3a20952a27b3d19493ed39734345",
|
||||
"submitted_request_digest": "sha256:41c8fc084e58c46554ccb6afe9943a99906e5986668c923811721f66d9b30a6a"
|
||||
},
|
||||
"lifetime": {
|
||||
"kind": "ttl",
|
||||
"ttl": "15m",
|
||||
"not_before": "2026-10-16T05:48:27Z",
|
||||
"expires_at": "2026-10-16T06:03:27Z"
|
||||
},
|
||||
"diagnostics": {
|
||||
"action": "rotate",
|
||||
"matched_relationship": "",
|
||||
"policy_package": "secrets-engine.catalog-lane.lifecycle",
|
||||
"policy_status": "ready",
|
||||
"registry_overrode": [],
|
||||
"registry_resource": false,
|
||||
"registry_subject": true
|
||||
},
|
||||
"provenance": {
|
||||
"evaluator": "flex-auth/local",
|
||||
"mode": "standalone",
|
||||
"policy_package": "secrets-engine.catalog-lane.lifecycle",
|
||||
"policy_version": "v2",
|
||||
"policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4",
|
||||
"registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb",
|
||||
"decision_time": "2026-10-16T05:48:27Z",
|
||||
"caller": {
|
||||
"mode": "disabled"
|
||||
}
|
||||
},
|
||||
"signature": {
|
||||
"mode": "signed",
|
||||
"alg": "ed25519",
|
||||
"kid": "synthetic-local-smoke",
|
||||
"value": "2OWuQk3UHXFQtloqnlNVKRdC6_6Ubx-aL3x9DuZcCyeSfDM6OD80q1c1oYEwEpF525GJKN3jYue8nK6T9JYaDw"
|
||||
},
|
||||
"caring": {
|
||||
"profile": "caring-0.4.0-rc2",
|
||||
"conformance_findings": [
|
||||
{
|
||||
"code": "CARING-DESCRIPTOR-MISSING",
|
||||
"severity": "warning",
|
||||
"message": "no CARING descriptor matched the request",
|
||||
"fields": [
|
||||
"caring_context"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"record_equals_response": true,
|
||||
"boundary": "disposable literal loopback serve; caller-auth disabled with synthetic token, no production credential or authorization; proves CLI signer wiring and exact R2 verification only"
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
{
|
||||
"schema": "access-engine.local-serve-consumer-proof.v1",
|
||||
"results": {
|
||||
"signed_cli_http_r2_consumer_and_request_lifetime_validation": "pass",
|
||||
"returned_typed_authenticated_envelope_equals_recorded_envelope": "pass",
|
||||
"tampered_actual_signed_response_refusal": "pass",
|
||||
"unsigned_actual_cli_http_response_refusal": "pass",
|
||||
"group_writable_seed_cli_startup_refusal": "pass"
|
||||
},
|
||||
"independent_source_policy_pin": {
|
||||
"policy_package": "secrets-engine.catalog-lane.lifecycle",
|
||||
"policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4",
|
||||
"policy_version": "v2"
|
||||
},
|
||||
"synthetic_public_key": "O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik",
|
||||
"synthetic_key_only": true,
|
||||
"producer_binary_sha256": "189291b54454a3eb7f10bf2091a0bfe955932ebe8de91fda24bea0210ca898e3",
|
||||
"consumer_verifier_sha256": "f2c2c75d07c3b21a9248afaf17f3cf050b53f44eea525fca4bb44d29333c92e4",
|
||||
"consumer_source_manifest_sha256": "de2f6912eaedb0a71d4b56b033684a75a6b9e3230cbdcbc709e2ea559aad333a",
|
||||
"signed_envelope": {
|
||||
"id": "decision:414734bb30381ff7",
|
||||
"contract_version": "flex-auth.decision-record.v1",
|
||||
"request_id": "check:secrets-engine-rotate",
|
||||
"effect": "allow",
|
||||
"reason": "catalog_lane_policy_matched",
|
||||
"matched_policy_version": "v2",
|
||||
"matched_rule": "catalog_lane_policy_matched",
|
||||
"resource": {
|
||||
"id": "lane:glas-primary",
|
||||
"type": "secret-catalog-lane",
|
||||
"system": "secrets-engine",
|
||||
"tenant": "tenant:platform",
|
||||
"attributes": {
|
||||
"auth_targets": [],
|
||||
"fields": [
|
||||
"password"
|
||||
],
|
||||
"policy_targets": [],
|
||||
"stage": "prod"
|
||||
}
|
||||
},
|
||||
"subject": {
|
||||
"id": "secrets-engine",
|
||||
"type": "service",
|
||||
"tenant": "tenant:platform",
|
||||
"attributes": {
|
||||
"description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.",
|
||||
"display_name": "secrets-engine service principal",
|
||||
"groups": [
|
||||
"group:secrets-engine-lane-operators"
|
||||
],
|
||||
"organization_relation": "ServiceProvider",
|
||||
"roles": [
|
||||
"Operator"
|
||||
]
|
||||
}
|
||||
},
|
||||
"binding": {
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "secrets-engine",
|
||||
"type": "service",
|
||||
"tenant": "tenant:platform",
|
||||
"attributes": {
|
||||
"description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.",
|
||||
"display_name": "secrets-engine service principal",
|
||||
"groups": [
|
||||
"group:secrets-engine-lane-operators"
|
||||
],
|
||||
"organization_relation": "ServiceProvider",
|
||||
"roles": [
|
||||
"Operator"
|
||||
]
|
||||
}
|
||||
},
|
||||
"action": "rotate",
|
||||
"resource": {
|
||||
"id": "lane:glas-primary",
|
||||
"type": "secret-catalog-lane",
|
||||
"system": "secrets-engine",
|
||||
"tenant": "tenant:platform",
|
||||
"attributes": {
|
||||
"auth_targets": [],
|
||||
"fields": [
|
||||
"password"
|
||||
],
|
||||
"policy_targets": [],
|
||||
"stage": "prod"
|
||||
}
|
||||
},
|
||||
"request_digest": "sha256:de67324f54187055307a833235f83ced9fcd3a20952a27b3d19493ed39734345",
|
||||
"submitted_request_digest": "sha256:41c8fc084e58c46554ccb6afe9943a99906e5986668c923811721f66d9b30a6a"
|
||||
},
|
||||
"lifetime": {
|
||||
"kind": "ttl",
|
||||
"ttl": "15m",
|
||||
"not_before": "2026-10-16T05:47:00Z",
|
||||
"expires_at": "2026-10-16T06:02:00Z"
|
||||
},
|
||||
"diagnostics": {
|
||||
"action": "rotate",
|
||||
"matched_relationship": "",
|
||||
"policy_package": "secrets-engine.catalog-lane.lifecycle",
|
||||
"policy_status": "ready",
|
||||
"registry_overrode": [],
|
||||
"registry_resource": false,
|
||||
"registry_subject": true
|
||||
},
|
||||
"provenance": {
|
||||
"evaluator": "flex-auth/local",
|
||||
"mode": "standalone",
|
||||
"policy_package": "secrets-engine.catalog-lane.lifecycle",
|
||||
"policy_version": "v2",
|
||||
"policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4",
|
||||
"registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb",
|
||||
"decision_time": "2026-10-16T05:47:00Z",
|
||||
"caller": {
|
||||
"mode": "disabled"
|
||||
}
|
||||
},
|
||||
"signature": {
|
||||
"mode": "signed",
|
||||
"alg": "ed25519",
|
||||
"kid": "synthetic-local-smoke",
|
||||
"value": "4LckmpmZO5pkV_BZ6pCElIUlmcvRkLW7Ddq1OepPxoQH_wOlkYDrBqI4IGn23M_wrYSwZhnavacM8fDkQzx9Bw"
|
||||
},
|
||||
"caring": {
|
||||
"profile": "caring-0.4.0-rc2",
|
||||
"conformance_findings": [
|
||||
{
|
||||
"code": "CARING-DESCRIPTOR-MISSING",
|
||||
"severity": "warning",
|
||||
"message": "no CARING descriptor matched the request",
|
||||
"fields": [
|
||||
"caring_context"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"record_equals_response": true,
|
||||
"boundary": "disposable literal loopback serve; caller-auth disabled with synthetic token, no production credential or authorization; proves CLI signer wiring and exact R2 verification only"
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"schema": "access-engine.owner-source-review.v2",
|
||||
"supersedes_source_patch": "producer-signing-r1.patch",
|
||||
"preserves_prior_evidence": true,
|
||||
"changes": "Reject group-writable seed files as well as all world permissions; 0440 projected Secret remains accepted. Targeted config unit test and actual disposable CLI evidence added. No cryptographic/schema or consumer source changes.",
|
||||
"producer_patch": "producer-signing-r2.patch",
|
||||
"producer_patch_sha256": "25e05b04f96cfe6f7d1abb11ab5d533b14a830c437136fc78aa381198ff8db37",
|
||||
"delta": "producer-signing-r1-to-r2.patch",
|
||||
"delta_sha256": "4b7857039390b72ef62e8b2499475c15eb0536ca3ef1a1adf78f797395532b82",
|
||||
"consumer_r2_patch_sha256": "98c4300c42fe93759a55297fe6629f1d4aeed249c3625f7fd37c8ab523a08ca9",
|
||||
"exact_source_review": "See owner-source-review.json; compatible verifier and rollout conditions remain unchanged.",
|
||||
"validation": {
|
||||
"targeted_unit": "go test -p 2 ./cmd/access-engine -run TestEnvelopeSigningStartupConfiguration -count=1 passes",
|
||||
"local_cli_http_exact_consumer": "local-serve-consumer-proof-r2.json: all five claims pass",
|
||||
"native_test_bounds": "Actual local serve and exact R2 consumer; test-only zero seed, independently derived public key and source policy digest; no production issuer/caller binding, trust admission, native platform deployment or actual credential custody inferred."
|
||||
},
|
||||
"proof_results": {
|
||||
"signed_cli_http_r2_consumer_and_request_lifetime_validation": "pass",
|
||||
"returned_typed_authenticated_envelope_equals_recorded_envelope": "pass",
|
||||
"tampered_actual_signed_response_refusal": "pass",
|
||||
"unsigned_actual_cli_http_response_refusal": "pass",
|
||||
"group_writable_seed_cli_startup_refusal": "pass"
|
||||
},
|
||||
"independent_policy_pin": {
|
||||
"policy_package": "secrets-engine.catalog-lane.lifecycle",
|
||||
"policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4",
|
||||
"policy_version": "v2"
|
||||
},
|
||||
"required_integration": "Apply full r2 to canonical producer then statehub fix-consistency. Existing FLEX-WP-0024 T06 stays waiting. Do not apply both r1 and full r2; delta is for an r1-integrated tree."
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
diff --git a/cmd/access-engine/envelope_signing.go b/cmd/access-engine/envelope_signing.go
|
||||
--- a/cmd/access-engine/envelope_signing.go
|
||||
+++ b/cmd/access-engine/envelope_signing.go
|
||||
@@ -26,8 +26,8 @@
|
||||
}
|
||||
defer file.Close()
|
||||
info, err := file.Stat()
|
||||
- if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0007 != 0 {
|
||||
- return fmt.Errorf("envelope signing key must be a regular file with no world permissions")
|
||||
+ if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0027 != 0 {
|
||||
+ return fmt.Errorf("envelope signing key must be a regular file without group-write or world permissions")
|
||||
}
|
||||
raw, err := io.ReadAll(io.LimitReader(file, 129))
|
||||
if err != nil || len(raw) > 128 {
|
||||
diff --git a/cmd/access-engine/envelope_signing_test.go b/cmd/access-engine/envelope_signing_test.go
|
||||
--- a/cmd/access-engine/envelope_signing_test.go
|
||||
+++ b/cmd/access-engine/envelope_signing_test.go
|
||||
@@ -25,6 +25,18 @@
|
||||
t.Fatal("invalid startup config accepted")
|
||||
}
|
||||
}
|
||||
+ if err := os.Chmod(path, 0440); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if err := configureEnvelopeSigning(engine, path, "synthetic"); err != nil {
|
||||
+ t.Fatalf("projected 0440 mount refused: %v", err)
|
||||
+ }
|
||||
+ if err := os.Chmod(path, 0620); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if configureEnvelopeSigning(engine, path, "kid") == nil {
|
||||
+ t.Fatal("group-writable key accepted")
|
||||
+ }
|
||||
if err := os.Chmod(path, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
diff --git a/docs/decision-envelope-signature.md b/docs/decision-envelope-signature.md
|
||||
--- a/docs/decision-envelope-signature.md
|
||||
+++ b/docs/decision-envelope-signature.md
|
||||
@@ -132,7 +132,7 @@
|
||||
or sign responses. The follow-through adds `serve --envelope-signing-key-file`
|
||||
and `--envelope-signing-kid`. Configure both or neither. The mounted file contains
|
||||
one unpadded base64url 32-byte Ed25519 seed, optionally with trailing whitespace;
|
||||
-it must be regular and have no world permissions. No production key is supplied
|
||||
+it must be regular and have no group-write or world permissions (0440 is accepted). No production key is supplied
|
||||
or minted here. Configuration failures abort startup. The key is read once:
|
||||
rotation requires an owner rollout/restart, not an assumed hot reload.
|
||||
|
||||
|
|
@ -0,0 +1,547 @@
|
|||
diff --git a/cmd/access-engine/main.go b/cmd/access-engine/main.go
|
||||
--- a/cmd/access-engine/main.go
|
||||
+++ b/cmd/access-engine/main.go
|
||||
@@ -330,6 +330,8 @@
|
||||
registryPath := fs.String("registry", "", "registry snapshot JSON file")
|
||||
policyPath := fs.String("policy", "", "policy package Markdown file")
|
||||
logPath := fs.String("log", "", "optional JSONL decision log path")
|
||||
+ signingKeyFile := fs.String("envelope-signing-key-file", "", "owner-mounted base64url Ed25519 seed file")
|
||||
+ signingKID := fs.String("envelope-signing-kid", "", "independently admitted envelope signing key id")
|
||||
outboxDir := fs.String("outbox-dir", "", "durable emission outbox directory (FLEX-WP-0031); exclusive with --log")
|
||||
emissionSource := fs.String("emission-source", "", "audit-core source name for emitted decision events, e.g. flex-auth.tenant-engine")
|
||||
auditURL := fs.String("audit-url", "", "admitted audit-core /v1/events endpoint; empty disables drain and heartbeats")
|
||||
@@ -369,6 +371,9 @@
|
||||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
+ if err := configureEnvelopeSigning(engine, *signingKeyFile, *signingKID); err != nil {
|
||||
+ return fail(stderr, err)
|
||||
+ }
|
||||
var outbox *emission.Outbox
|
||||
if *outboxDir != "" {
|
||||
outbox, err = emission.Open(*outboxDir, *emissionSource)
|
||||
diff --git a/cmd/access-engine/envelope_signing.go b/cmd/access-engine/envelope_signing.go
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/cmd/access-engine/envelope_signing.go
|
||||
@@ -0,0 +1,42 @@
|
||||
+package main
|
||||
+
|
||||
+import (
|
||||
+ "crypto/ed25519"
|
||||
+ "encoding/base64"
|
||||
+ "fmt"
|
||||
+ "io"
|
||||
+ "os"
|
||||
+ "strings"
|
||||
+
|
||||
+ "github.com/netkingdom/access-engine/internal/decision"
|
||||
+)
|
||||
+
|
||||
+// configureEnvelopeSigning reads a mounted seed once at startup. Never accept
|
||||
+// the secret in argv, an environment value, an HTTP request or a log message.
|
||||
+func configureEnvelopeSigning(engine *decision.Engine, path, kid string) error {
|
||||
+ if path == "" && kid == "" {
|
||||
+ return nil
|
||||
+ }
|
||||
+ if path == "" || strings.TrimSpace(kid) == "" {
|
||||
+ return fmt.Errorf("envelope signing requires key file and kid")
|
||||
+ }
|
||||
+ file, err := os.Open(path)
|
||||
+ if err != nil {
|
||||
+ return fmt.Errorf("envelope signing key file unavailable")
|
||||
+ }
|
||||
+ defer file.Close()
|
||||
+ info, err := file.Stat()
|
||||
+ if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0007 != 0 {
|
||||
+ return fmt.Errorf("envelope signing key must be a regular file with no world permissions")
|
||||
+ }
|
||||
+ raw, err := io.ReadAll(io.LimitReader(file, 129))
|
||||
+ if err != nil || len(raw) > 128 {
|
||||
+ return fmt.Errorf("invalid envelope signing key file")
|
||||
+ }
|
||||
+ encoded := strings.TrimSpace(string(raw))
|
||||
+ seed, err := base64.RawURLEncoding.Strict().DecodeString(encoded)
|
||||
+ if err != nil || len(seed) != ed25519.SeedSize {
|
||||
+ return fmt.Errorf("invalid envelope signing seed")
|
||||
+ }
|
||||
+ return engine.SetEnvelopeSigningKey(ed25519.NewKeyFromSeed(seed), kid)
|
||||
+}
|
||||
diff --git a/cmd/access-engine/envelope_signing_test.go b/cmd/access-engine/envelope_signing_test.go
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/cmd/access-engine/envelope_signing_test.go
|
||||
@@ -0,0 +1,43 @@
|
||||
+package main
|
||||
+
|
||||
+import (
|
||||
+ "context"
|
||||
+ "encoding/base64"
|
||||
+ "os"
|
||||
+ "path/filepath"
|
||||
+ "testing"
|
||||
+)
|
||||
+
|
||||
+func TestEnvelopeSigningStartupConfiguration(t *testing.T) {
|
||||
+ engine, err := buildEngine(context.Background(), examplePath("registry_snapshot.json"), examplePath("policy_package.md"), "")
|
||||
+ if err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ path := filepath.Join(t.TempDir(), "seed")
|
||||
+ if err := os.WriteFile(path, []byte(base64.RawURLEncoding.EncodeToString(make([]byte, 32))), 0600); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if err := configureEnvelopeSigning(engine, path, "synthetic"); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ for _, input := range []struct{ path, kid string }{{path, ""}, {"", "kid"}, {path + "missing", "kid"}} {
|
||||
+ if configureEnvelopeSigning(engine, input.path, input.kid) == nil {
|
||||
+ t.Fatal("invalid startup config accepted")
|
||||
+ }
|
||||
+ }
|
||||
+ if err := os.Chmod(path, 0644); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if configureEnvelopeSigning(engine, path, "kid") == nil {
|
||||
+ t.Fatal("world-readable key accepted")
|
||||
+ }
|
||||
+ if err := os.Chmod(path, 0600); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if err := os.WriteFile(path, []byte("invalid seed"), 0600); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if configureEnvelopeSigning(engine, path, "kid") == nil {
|
||||
+ t.Fatal("invalid seed accepted")
|
||||
+ }
|
||||
+}
|
||||
diff --git a/internal/decision/engine.go b/internal/decision/engine.go
|
||||
--- a/internal/decision/engine.go
|
||||
+++ b/internal/decision/engine.go
|
||||
@@ -3,6 +3,7 @@
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
+ "crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
@@ -18,18 +19,21 @@
|
||||
"github.com/netkingdom/access-engine/internal/callerauth"
|
||||
"github.com/netkingdom/access-engine/internal/policy"
|
||||
"github.com/netkingdom/access-engine/internal/registry"
|
||||
+ "github.com/netkingdom/access-engine/internal/sign"
|
||||
"github.com/netkingdom/access-engine/pkg/api"
|
||||
)
|
||||
|
||||
// Engine evaluates deterministic standalone authorization checks against a
|
||||
// local registry and one validated policy package.
|
||||
type Engine struct {
|
||||
- store *registry.Store
|
||||
- policy *policy.Package
|
||||
- mu sync.RWMutex
|
||||
- history map[string]api.DecisionEnvelope
|
||||
- log DecisionRecorder
|
||||
- clock func() time.Time
|
||||
+ store *registry.Store
|
||||
+ policy *policy.Package
|
||||
+ mu sync.RWMutex
|
||||
+ history map[string]api.DecisionEnvelope
|
||||
+ log DecisionRecorder
|
||||
+ clock func() time.Time
|
||||
+ signingKey ed25519.PrivateKey
|
||||
+ signingKID string
|
||||
}
|
||||
|
||||
// DecisionRecorder persists decision envelopes.
|
||||
@@ -91,6 +95,19 @@
|
||||
}, nil
|
||||
}
|
||||
|
||||
+// SetEnvelopeSigningKey installs an owner-custodied key before serving requests.
|
||||
+// The key is copied so caller mutation cannot change the admitted signer.
|
||||
+func (e *Engine) SetEnvelopeSigningKey(key ed25519.PrivateKey, kid string) error {
|
||||
+ if len(key) != ed25519.PrivateKeySize || strings.TrimSpace(kid) == "" {
|
||||
+ return fmt.Errorf("valid Ed25519 private key and kid are required")
|
||||
+ }
|
||||
+ e.mu.Lock()
|
||||
+ defer e.mu.Unlock()
|
||||
+ e.signingKey = append(ed25519.PrivateKey(nil), key...)
|
||||
+ e.signingKID = kid
|
||||
+ return nil
|
||||
+}
|
||||
+
|
||||
// SetDecisionLog attaches a local decision recorder to the engine.
|
||||
func (e *Engine) SetDecisionLog(log DecisionRecorder) {
|
||||
e.mu.Lock()
|
||||
@@ -124,7 +141,16 @@
|
||||
return api.DecisionEnvelope{}, err
|
||||
}
|
||||
|
||||
- return e.envelope(ctx, normalized, request, expectation, facts), nil
|
||||
+ envelope := e.envelope(ctx, normalized, request, expectation, facts)
|
||||
+ e.mu.RLock()
|
||||
+ key, kid := e.signingKey, e.signingKID
|
||||
+ e.mu.RUnlock()
|
||||
+ if len(key) != 0 {
|
||||
+ if err := sign.Sign(&envelope, key, kid); err != nil {
|
||||
+ return api.DecisionEnvelope{}, fmt.Errorf("decision signing failed: %w", err)
|
||||
+ }
|
||||
+ }
|
||||
+ return envelope, nil
|
||||
}
|
||||
|
||||
// Check evaluates one subject/action/resource request. The decision is
|
||||
diff --git a/internal/decision/envelope_signing_test.go b/internal/decision/envelope_signing_test.go
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/internal/decision/envelope_signing_test.go
|
||||
@@ -0,0 +1,64 @@
|
||||
+package decision_test
|
||||
+
|
||||
+import (
|
||||
+ "context"
|
||||
+ "crypto/ed25519"
|
||||
+ "path/filepath"
|
||||
+ "reflect"
|
||||
+ "testing"
|
||||
+
|
||||
+ "github.com/netkingdom/access-engine/internal/sign"
|
||||
+ "github.com/netkingdom/access-engine/pkg/api"
|
||||
+)
|
||||
+
|
||||
+type signingRecorder struct{ records []api.DecisionEnvelope }
|
||||
+
|
||||
+func (r *signingRecorder) Append(e api.DecisionEnvelope) error {
|
||||
+ r.records = append(r.records, e)
|
||||
+ return nil
|
||||
+}
|
||||
+
|
||||
+func TestSignerAuthenticatesReturnedAndRecordedEnvelope(t *testing.T) {
|
||||
+ engine := newTestEngine(t)
|
||||
+ var request api.CheckRequest
|
||||
+ loadYAML(t, filepath.Join("..", "..", "examples", "caring", "check_request.yaml"), &request)
|
||||
+ unsigned, err := engine.Check(context.Background(), request)
|
||||
+ if err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ // Synthetic deterministic seed only; no estate credential is read.
|
||||
+ key := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize))
|
||||
+ public := append(ed25519.PublicKey(nil), key.Public().(ed25519.PublicKey)...)
|
||||
+ if err := engine.SetEnvelopeSigningKey(key, "synthetic-current"); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ key[0] ^= 1 // Setter must not retain the caller's mutable key slice.
|
||||
+ recorder := &signingRecorder{}
|
||||
+ engine.SetDecisionLog(recorder)
|
||||
+ signed, err := engine.Check(context.Background(), request)
|
||||
+ if err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if err := sign.Verify(signed, map[string]ed25519.PublicKey{"synthetic-current": public}); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if len(recorder.records) != 1 || !reflect.DeepEqual(signed, recorder.records[0]) {
|
||||
+ t.Fatal("recorded envelope differs from authenticated return")
|
||||
+ }
|
||||
+ if signed.Binding.RequestDigest != unsigned.Binding.RequestDigest {
|
||||
+ t.Fatal("signing changed request digest")
|
||||
+ }
|
||||
+ signed.Provenance.PolicyVersion = "altered"
|
||||
+ if sign.Verify(signed, map[string]ed25519.PublicKey{"synthetic-current": public}) == nil {
|
||||
+ t.Fatal("altered provenance verified")
|
||||
+ }
|
||||
+}
|
||||
+func TestSignerRejectsInvalidConfiguration(t *testing.T) {
|
||||
+ engine := newTestEngine(t)
|
||||
+ if engine.SetEnvelopeSigningKey(nil, "kid") == nil {
|
||||
+ t.Fatal("missing key accepted")
|
||||
+ }
|
||||
+ if engine.SetEnvelopeSigningKey(ed25519.NewKeyFromSeed(make([]byte, 32)), "") == nil {
|
||||
+ t.Fatal("missing kid accepted")
|
||||
+ }
|
||||
+}
|
||||
diff --git a/charts/access-engine/values.yaml b/charts/access-engine/values.yaml
|
||||
--- a/charts/access-engine/values.yaml
|
||||
+++ b/charts/access-engine/values.yaml
|
||||
@@ -85,3 +85,11 @@
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 192Mi
|
||||
+
|
||||
+# Inert until an independently admitted signer and existing custody Secret exist.
|
||||
+envelopeSigning:
|
||||
+ enabled: false
|
||||
+ kid: ""
|
||||
+ secretName: ""
|
||||
+ secretKey: ""
|
||||
+ mountPath: /var/run/secrets/decision-envelope-signing
|
||||
diff --git a/charts/access-engine/templates/deployment.yaml b/charts/access-engine/templates/deployment.yaml
|
||||
--- a/charts/access-engine/templates/deployment.yaml
|
||||
+++ b/charts/access-engine/templates/deployment.yaml
|
||||
@@ -10,6 +10,14 @@
|
||||
{{- end }}
|
||||
{{- if or (not (hasPrefix "/" .Values.auditDelivery.mountPath)) (contains ".." .Values.auditDelivery.mountPath) }}
|
||||
{{- fail "auditDelivery.mountPath must be an absolute normalized directory" }}
|
||||
+{{- end }}
|
||||
+{{- end }}
|
||||
+{{- if .Values.envelopeSigning.enabled }}
|
||||
+{{- if ne (first .Values.args) "serve" }}
|
||||
+{{- fail "envelopeSigning currently supports serve decision pins only" }}
|
||||
+{{- end }}
|
||||
+{{- if or (not (hasPrefix "/" .Values.envelopeSigning.mountPath)) (contains ".." .Values.envelopeSigning.mountPath) }}
|
||||
+{{- fail "envelopeSigning.mountPath must be an absolute normalized directory" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
apiVersion: apps/v1
|
||||
@@ -53,6 +61,12 @@
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
args:
|
||||
{{- toYaml .Values.args | nindent 12 }}
|
||||
+ {{- if .Values.envelopeSigning.enabled }}
|
||||
+ - --envelope-signing-key-file
|
||||
+ - {{ printf "%s/seed" .Values.envelopeSigning.mountPath | quote }}
|
||||
+ - --envelope-signing-kid
|
||||
+ - {{ required "envelopeSigning.kid must be independently admitted" .Values.envelopeSigning.kid | quote }}
|
||||
+ {{- end }}
|
||||
{{- if .Values.outbox.enabled }}
|
||||
- --outbox-dir
|
||||
- {{ .Values.outbox.mountPath | quote }}
|
||||
@@ -114,8 +128,13 @@
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: true
|
||||
- {{- if or .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "access-engine.callerAuth.enabled" .) "true") }}
|
||||
+ {{- if or .Values.envelopeSigning.enabled .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "access-engine.callerAuth.enabled" .) "true") }}
|
||||
volumeMounts:
|
||||
+ {{- if .Values.envelopeSigning.enabled }}
|
||||
+ - mountPath: {{ .Values.envelopeSigning.mountPath | quote }}
|
||||
+ name: access-engine-envelope-signing
|
||||
+ readOnly: true
|
||||
+ {{- end }}
|
||||
{{- if .Values.auditDelivery.enabled }}
|
||||
- mountPath: {{ .Values.auditDelivery.mountPath | quote }}
|
||||
name: access-engine-audit
|
||||
@@ -131,8 +150,17 @@
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
- {{- if or .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "access-engine.callerAuth.enabled" .) "true") }}
|
||||
+ {{- if or .Values.envelopeSigning.enabled .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "access-engine.callerAuth.enabled" .) "true") }}
|
||||
volumes:
|
||||
+ {{- if .Values.envelopeSigning.enabled }}
|
||||
+ - name: access-engine-envelope-signing
|
||||
+ secret:
|
||||
+ secretName: {{ required "envelopeSigning.secretName must name an admitted existing Secret" .Values.envelopeSigning.secretName | quote }}
|
||||
+ defaultMode: 0440
|
||||
+ items:
|
||||
+ - key: {{ required "envelopeSigning.secretKey is required" .Values.envelopeSigning.secretKey | quote }}
|
||||
+ path: seed
|
||||
+ {{- end }}
|
||||
{{- if .Values.auditDelivery.enabled }}
|
||||
- name: access-engine-audit
|
||||
secret:
|
||||
diff --git a/charts/flex-auth/values.yaml b/charts/flex-auth/values.yaml
|
||||
--- a/charts/flex-auth/values.yaml
|
||||
+++ b/charts/flex-auth/values.yaml
|
||||
@@ -85,3 +85,11 @@
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 192Mi
|
||||
+
|
||||
+# Inert until an independently admitted signer and existing custody Secret exist.
|
||||
+envelopeSigning:
|
||||
+ enabled: false
|
||||
+ kid: ""
|
||||
+ secretName: ""
|
||||
+ secretKey: ""
|
||||
+ mountPath: /var/run/secrets/decision-envelope-signing
|
||||
diff --git a/charts/flex-auth/templates/deployment.yaml b/charts/flex-auth/templates/deployment.yaml
|
||||
--- a/charts/flex-auth/templates/deployment.yaml
|
||||
+++ b/charts/flex-auth/templates/deployment.yaml
|
||||
@@ -10,6 +10,14 @@
|
||||
{{- end }}
|
||||
{{- if or (not (hasPrefix "/" .Values.auditDelivery.mountPath)) (contains ".." .Values.auditDelivery.mountPath) }}
|
||||
{{- fail "auditDelivery.mountPath must be an absolute normalized directory" }}
|
||||
+{{- end }}
|
||||
+{{- end }}
|
||||
+{{- if .Values.envelopeSigning.enabled }}
|
||||
+{{- if ne (first .Values.args) "serve" }}
|
||||
+{{- fail "envelopeSigning currently supports serve decision pins only" }}
|
||||
+{{- end }}
|
||||
+{{- if or (not (hasPrefix "/" .Values.envelopeSigning.mountPath)) (contains ".." .Values.envelopeSigning.mountPath) }}
|
||||
+{{- fail "envelopeSigning.mountPath must be an absolute normalized directory" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
apiVersion: apps/v1
|
||||
@@ -52,6 +60,12 @@
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
args:
|
||||
{{- toYaml .Values.args | nindent 12 }}
|
||||
+ {{- if .Values.envelopeSigning.enabled }}
|
||||
+ - --envelope-signing-key-file
|
||||
+ - {{ printf "%s/seed" .Values.envelopeSigning.mountPath | quote }}
|
||||
+ - --envelope-signing-kid
|
||||
+ - {{ required "envelopeSigning.kid must be independently admitted" .Values.envelopeSigning.kid | quote }}
|
||||
+ {{- end }}
|
||||
{{- if .Values.outbox.enabled }}
|
||||
- --outbox-dir
|
||||
- {{ .Values.outbox.mountPath | quote }}
|
||||
@@ -107,8 +121,13 @@
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: true
|
||||
- {{- if or .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "flex-auth.callerAuth.enabled" .) "true") }}
|
||||
+ {{- if or .Values.envelopeSigning.enabled .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "flex-auth.callerAuth.enabled" .) "true") }}
|
||||
volumeMounts:
|
||||
+ {{- if .Values.envelopeSigning.enabled }}
|
||||
+ - mountPath: {{ .Values.envelopeSigning.mountPath | quote }}
|
||||
+ name: flex-auth-envelope-signing
|
||||
+ readOnly: true
|
||||
+ {{- end }}
|
||||
{{- if .Values.auditDelivery.enabled }}
|
||||
- mountPath: {{ .Values.auditDelivery.mountPath | quote }}
|
||||
name: flex-auth-audit
|
||||
@@ -124,8 +143,17 @@
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
- {{- if or .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "flex-auth.callerAuth.enabled" .) "true") }}
|
||||
+ {{- if or .Values.envelopeSigning.enabled .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "flex-auth.callerAuth.enabled" .) "true") }}
|
||||
volumes:
|
||||
+ {{- if .Values.envelopeSigning.enabled }}
|
||||
+ - name: flex-auth-envelope-signing
|
||||
+ secret:
|
||||
+ secretName: {{ required "envelopeSigning.secretName must name an admitted existing Secret" .Values.envelopeSigning.secretName | quote }}
|
||||
+ defaultMode: 0440
|
||||
+ items:
|
||||
+ - key: {{ required "envelopeSigning.secretKey is required" .Values.envelopeSigning.secretKey | quote }}
|
||||
+ path: seed
|
||||
+ {{- end }}
|
||||
{{- if .Values.auditDelivery.enabled }}
|
||||
- name: flex-auth-audit
|
||||
secret:
|
||||
diff --git a/docs/decision-envelope-signature.md b/docs/decision-envelope-signature.md
|
||||
--- a/docs/decision-envelope-signature.md
|
||||
+++ b/docs/decision-envelope-signature.md
|
||||
@@ -5,7 +5,7 @@
|
||||
Contract: additive on `flex-auth.decision-record.v1`
|
||||
Does **not** change `request_digest`.
|
||||
|
||||
-The response channel is unauthenticated today (`FLEX-DEC-2026-010`). Digests
|
||||
+The original response channel was unauthenticated (`FLEX-DEC-2026-010`). Digests
|
||||
cannot fix that: every input to them is sent by the caller or published.
|
||||
Fail-closed cannot fix that either: it covers a missing PDP, not a lying one.
|
||||
|
||||
@@ -117,3 +117,52 @@
|
||||
pair: one genuine signed envelope and one whose payload was altered after
|
||||
signing. It does **not** mint the production key and does **not** wait for
|
||||
the OpenBao lane to exist before the code path is testable.
|
||||
+
|
||||
+## NetKingdom target source follow-through (2026-10-16)
|
||||
+
|
||||
+Owner source review of Secrets Engine R2 patch
|
||||
+`98c4300c42fe93759a55297fe6629f1d4aeed249c3625f7fd37c8ab523a08ca9`
|
||||
+verified unchanged `api`/`internal/sign` bytes and producer replay fixtures at
|
||||
+`62ac2b4e99dec2005c7c407fe633612578a7907d`. The consumer runs the Go producer
|
||||
+verifier and uses its returned typed envelope, then independently checks issuer,
|
||||
+policy package/version/digest, exact submitted request and lifetime. This avoids
|
||||
+another envelope canonicalization implementation or PDP.
|
||||
+
|
||||
+That revision implemented `Sign` only for fixtures: `serve` could not load a key
|
||||
+or sign responses. The follow-through adds `serve --envelope-signing-key-file`
|
||||
+and `--envelope-signing-kid`. Configure both or neither. The mounted file contains
|
||||
+one unpadded base64url 32-byte Ed25519 seed, optionally with trailing whitespace;
|
||||
+it must be regular and have no world permissions. No production key is supplied
|
||||
+or minted here. Configuration failures abort startup. The key is read once:
|
||||
+rotation requires an owner rollout/restart, not an assumed hot reload.
|
||||
+
|
||||
+Signing occurs after the complete envelope is constructed and before the durable
|
||||
+recorder, for check, batch and list decisions alike. Returned and recorded objects
|
||||
+therefore agree. Existing unsigned developer mode remains explicit; authenticated
|
||||
+Secrets Engine production refuses it. The signature schema, canonical bytes and
|
||||
+request digest are unchanged, so this patch does not require a consumer vendored
|
||||
+source refresh.
|
||||
+
|
||||
+Both retained `flex-auth` and new `access-engine` charts expose an inert
|
||||
+`envelopeSigning` block. Enabling requires `kid`, `secretName`, `secretKey`, and an
|
||||
+absolute `mountPath`. They mount an existing custody-owned Secret read-only;
|
||||
+they do not provision it. The existing Secrets pin values remain unchanged and
|
||||
+unsigned until an exact reviewed image/values rollout is admitted.
|
||||
+
|
||||
+The `/v1/keys` route described earlier is a discovery design, **not an implemented
|
||||
+route**. It must never bootstrap independent trust. For this first consumer, the
|
||||
+owner transports reviewed public keys plus exact issuer/policy pins independently
|
||||
+into the consumer's admission file. No runtime discovery dependency is needed.
|
||||
+Rotation admits new public key alongside old in every consumer, proves new-key
|
||||
+acceptance, rolls producer to new seed/kid, waits past the maximum outstanding
|
||||
+allow lifetime plus bounded clock uncertainty, removes old key, and proves old-key
|
||||
+refusal. A revoked/compromised old key is removed immediately rather than retained
|
||||
+for overlap. Retain public verification material in audit custody for historic
|
||||
+records without treating it as current runtime authority.
|
||||
+
|
||||
+Rollout remains blocked on independent public-key and policy-pin admission;
|
||||
+OpenBao/Platform custody and exact secret mount; reviewed producer image/digest;
|
||||
+owner-bound loopback tunnel and caller audience; installation of the reviewed
|
||||
+consumer verifier binary (the Python wheel alone omits it); exact consumer source
|
||||
+and hash drift check; and native positive/negative/rotation plus record-custody
|
||||
+receipts. Local fixtures and chart renders do not establish those outcomes.
|
||||
diff --git a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
--- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
+++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
@@ -4,7 +4,7 @@
|
||||
title: "Sign the decision envelope: the response channel is unauthenticated"
|
||||
domain: infotech
|
||||
repo: flex-auth
|
||||
-status: finished
|
||||
+status: active
|
||||
owner: claude
|
||||
topic_slug: netkingdom
|
||||
planning_priority: P1
|
||||
@@ -15,7 +15,7 @@
|
||||
- FLEX-WP-0019
|
||||
- FLEX-WP-0023
|
||||
created: "2026-09-06"
|
||||
-updated: "2026-09-14"
|
||||
+updated: "2026-10-16"
|
||||
state_hub_workstream_id: "90577acd-6910-548d-a13e-1dbfdfb8ed27"
|
||||
---
|
||||
|
||||
@@ -163,3 +163,37 @@
|
||||
this version. §16 already carries the same observation one layer up, about the
|
||||
standard's own publication path having no digest or freeze discipline; F1 is
|
||||
that observation applied to the artifact the standard regulates.
|
||||
+
|
||||
+## 5. Wire owner signing into the decision pin and retained deployment charts
|
||||
+
|
||||
+```task
|
||||
+id: FLEX-WP-0024-T05
|
||||
+status: done
|
||||
+priority: high
|
||||
+```
|
||||
+
|
||||
+2026-10-16 source follow-through: NetKingdom target Secrets Engine R2 owner review
|
||||
+found that the completed T03 signature package was used only by fixtures. Add
|
||||
+explicit mounted seed/kid configuration for `serve`, sign the final envelope before
|
||||
+recording, and expose inert existing-Secret mounts in both retained/new charts.
|
||||
+The API/sign verifier sources remain byte-identical. Local source validation and
|
||||
+review are in the Custodian `docs/changes/netkingdom-target-2026-10-16/access-verifier-review/`.
|
||||
+This is source preparation only, not signer admission or deployed acceptance.
|
||||
+
|
||||
+## 6. Admit custody, consumer trust, exact packages and native signed decisions
|
||||
+
|
||||
+```task
|
||||
+id: FLEX-WP-0024-T06
|
||||
+status: wait
|
||||
+priority: high
|
||||
+```
|
||||
+
|
||||
+Wait on Platform/OpenBao key custody, independent public-key and issuer/policy-pin
|
||||
+admission, exact image and existing-Secret values rollout, owner-bound caller/tunnel,
|
||||
+and Secrets Engine verifier installation. Follow `docs/decision-envelope-signature.md`
|
||||
+for overlap/removal semantics. Retain signed producer record/consumer acceptance,
|
||||
+forged/altered/unsigned/wrong-pin/expired denial and rotation-removal receipts;
|
||||
+then update actual deployed status. No signer or runtime trust is admitted by the
|
||||
+source review. This task owns the producer-side native gate; SECRETS-WP-0014 owns
|
||||
+consumer admission/enforcement. The old fixture completion never established this
|
||||
+gate, so the workplan is reopened rather than claiming it already passed.
|
||||
|
|
@ -0,0 +1,559 @@
|
|||
diff --git a/cmd/access-engine/main.go b/cmd/access-engine/main.go
|
||||
--- a/cmd/access-engine/main.go
|
||||
+++ b/cmd/access-engine/main.go
|
||||
@@ -330,6 +330,8 @@
|
||||
registryPath := fs.String("registry", "", "registry snapshot JSON file")
|
||||
policyPath := fs.String("policy", "", "policy package Markdown file")
|
||||
logPath := fs.String("log", "", "optional JSONL decision log path")
|
||||
+ signingKeyFile := fs.String("envelope-signing-key-file", "", "owner-mounted base64url Ed25519 seed file")
|
||||
+ signingKID := fs.String("envelope-signing-kid", "", "independently admitted envelope signing key id")
|
||||
outboxDir := fs.String("outbox-dir", "", "durable emission outbox directory (FLEX-WP-0031); exclusive with --log")
|
||||
emissionSource := fs.String("emission-source", "", "audit-core source name for emitted decision events, e.g. flex-auth.tenant-engine")
|
||||
auditURL := fs.String("audit-url", "", "admitted audit-core /v1/events endpoint; empty disables drain and heartbeats")
|
||||
@@ -369,6 +371,9 @@
|
||||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
+ if err := configureEnvelopeSigning(engine, *signingKeyFile, *signingKID); err != nil {
|
||||
+ return fail(stderr, err)
|
||||
+ }
|
||||
var outbox *emission.Outbox
|
||||
if *outboxDir != "" {
|
||||
outbox, err = emission.Open(*outboxDir, *emissionSource)
|
||||
diff --git a/cmd/access-engine/envelope_signing.go b/cmd/access-engine/envelope_signing.go
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/cmd/access-engine/envelope_signing.go
|
||||
@@ -0,0 +1,42 @@
|
||||
+package main
|
||||
+
|
||||
+import (
|
||||
+ "crypto/ed25519"
|
||||
+ "encoding/base64"
|
||||
+ "fmt"
|
||||
+ "io"
|
||||
+ "os"
|
||||
+ "strings"
|
||||
+
|
||||
+ "github.com/netkingdom/access-engine/internal/decision"
|
||||
+)
|
||||
+
|
||||
+// configureEnvelopeSigning reads a mounted seed once at startup. Never accept
|
||||
+// the secret in argv, an environment value, an HTTP request or a log message.
|
||||
+func configureEnvelopeSigning(engine *decision.Engine, path, kid string) error {
|
||||
+ if path == "" && kid == "" {
|
||||
+ return nil
|
||||
+ }
|
||||
+ if path == "" || strings.TrimSpace(kid) == "" {
|
||||
+ return fmt.Errorf("envelope signing requires key file and kid")
|
||||
+ }
|
||||
+ file, err := os.Open(path)
|
||||
+ if err != nil {
|
||||
+ return fmt.Errorf("envelope signing key file unavailable")
|
||||
+ }
|
||||
+ defer file.Close()
|
||||
+ info, err := file.Stat()
|
||||
+ if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0027 != 0 {
|
||||
+ return fmt.Errorf("envelope signing key must be a regular file without group-write or world permissions")
|
||||
+ }
|
||||
+ raw, err := io.ReadAll(io.LimitReader(file, 129))
|
||||
+ if err != nil || len(raw) > 128 {
|
||||
+ return fmt.Errorf("invalid envelope signing key file")
|
||||
+ }
|
||||
+ encoded := strings.TrimSpace(string(raw))
|
||||
+ seed, err := base64.RawURLEncoding.Strict().DecodeString(encoded)
|
||||
+ if err != nil || len(seed) != ed25519.SeedSize {
|
||||
+ return fmt.Errorf("invalid envelope signing seed")
|
||||
+ }
|
||||
+ return engine.SetEnvelopeSigningKey(ed25519.NewKeyFromSeed(seed), kid)
|
||||
+}
|
||||
diff --git a/cmd/access-engine/envelope_signing_test.go b/cmd/access-engine/envelope_signing_test.go
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/cmd/access-engine/envelope_signing_test.go
|
||||
@@ -0,0 +1,55 @@
|
||||
+package main
|
||||
+
|
||||
+import (
|
||||
+ "context"
|
||||
+ "encoding/base64"
|
||||
+ "os"
|
||||
+ "path/filepath"
|
||||
+ "testing"
|
||||
+)
|
||||
+
|
||||
+func TestEnvelopeSigningStartupConfiguration(t *testing.T) {
|
||||
+ engine, err := buildEngine(context.Background(), examplePath("registry_snapshot.json"), examplePath("policy_package.md"), "")
|
||||
+ if err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ path := filepath.Join(t.TempDir(), "seed")
|
||||
+ if err := os.WriteFile(path, []byte(base64.RawURLEncoding.EncodeToString(make([]byte, 32))), 0600); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if err := configureEnvelopeSigning(engine, path, "synthetic"); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ for _, input := range []struct{ path, kid string }{{path, ""}, {"", "kid"}, {path + "missing", "kid"}} {
|
||||
+ if configureEnvelopeSigning(engine, input.path, input.kid) == nil {
|
||||
+ t.Fatal("invalid startup config accepted")
|
||||
+ }
|
||||
+ }
|
||||
+ if err := os.Chmod(path, 0440); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if err := configureEnvelopeSigning(engine, path, "synthetic"); err != nil {
|
||||
+ t.Fatalf("projected 0440 mount refused: %v", err)
|
||||
+ }
|
||||
+ if err := os.Chmod(path, 0620); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if configureEnvelopeSigning(engine, path, "kid") == nil {
|
||||
+ t.Fatal("group-writable key accepted")
|
||||
+ }
|
||||
+ if err := os.Chmod(path, 0644); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if configureEnvelopeSigning(engine, path, "kid") == nil {
|
||||
+ t.Fatal("world-readable key accepted")
|
||||
+ }
|
||||
+ if err := os.Chmod(path, 0600); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if err := os.WriteFile(path, []byte("invalid seed"), 0600); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if configureEnvelopeSigning(engine, path, "kid") == nil {
|
||||
+ t.Fatal("invalid seed accepted")
|
||||
+ }
|
||||
+}
|
||||
diff --git a/internal/decision/engine.go b/internal/decision/engine.go
|
||||
--- a/internal/decision/engine.go
|
||||
+++ b/internal/decision/engine.go
|
||||
@@ -3,6 +3,7 @@
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
+ "crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
@@ -18,18 +19,21 @@
|
||||
"github.com/netkingdom/access-engine/internal/callerauth"
|
||||
"github.com/netkingdom/access-engine/internal/policy"
|
||||
"github.com/netkingdom/access-engine/internal/registry"
|
||||
+ "github.com/netkingdom/access-engine/internal/sign"
|
||||
"github.com/netkingdom/access-engine/pkg/api"
|
||||
)
|
||||
|
||||
// Engine evaluates deterministic standalone authorization checks against a
|
||||
// local registry and one validated policy package.
|
||||
type Engine struct {
|
||||
- store *registry.Store
|
||||
- policy *policy.Package
|
||||
- mu sync.RWMutex
|
||||
- history map[string]api.DecisionEnvelope
|
||||
- log DecisionRecorder
|
||||
- clock func() time.Time
|
||||
+ store *registry.Store
|
||||
+ policy *policy.Package
|
||||
+ mu sync.RWMutex
|
||||
+ history map[string]api.DecisionEnvelope
|
||||
+ log DecisionRecorder
|
||||
+ clock func() time.Time
|
||||
+ signingKey ed25519.PrivateKey
|
||||
+ signingKID string
|
||||
}
|
||||
|
||||
// DecisionRecorder persists decision envelopes.
|
||||
@@ -91,6 +95,19 @@
|
||||
}, nil
|
||||
}
|
||||
|
||||
+// SetEnvelopeSigningKey installs an owner-custodied key before serving requests.
|
||||
+// The key is copied so caller mutation cannot change the admitted signer.
|
||||
+func (e *Engine) SetEnvelopeSigningKey(key ed25519.PrivateKey, kid string) error {
|
||||
+ if len(key) != ed25519.PrivateKeySize || strings.TrimSpace(kid) == "" {
|
||||
+ return fmt.Errorf("valid Ed25519 private key and kid are required")
|
||||
+ }
|
||||
+ e.mu.Lock()
|
||||
+ defer e.mu.Unlock()
|
||||
+ e.signingKey = append(ed25519.PrivateKey(nil), key...)
|
||||
+ e.signingKID = kid
|
||||
+ return nil
|
||||
+}
|
||||
+
|
||||
// SetDecisionLog attaches a local decision recorder to the engine.
|
||||
func (e *Engine) SetDecisionLog(log DecisionRecorder) {
|
||||
e.mu.Lock()
|
||||
@@ -124,7 +141,16 @@
|
||||
return api.DecisionEnvelope{}, err
|
||||
}
|
||||
|
||||
- return e.envelope(ctx, normalized, request, expectation, facts), nil
|
||||
+ envelope := e.envelope(ctx, normalized, request, expectation, facts)
|
||||
+ e.mu.RLock()
|
||||
+ key, kid := e.signingKey, e.signingKID
|
||||
+ e.mu.RUnlock()
|
||||
+ if len(key) != 0 {
|
||||
+ if err := sign.Sign(&envelope, key, kid); err != nil {
|
||||
+ return api.DecisionEnvelope{}, fmt.Errorf("decision signing failed: %w", err)
|
||||
+ }
|
||||
+ }
|
||||
+ return envelope, nil
|
||||
}
|
||||
|
||||
// Check evaluates one subject/action/resource request. The decision is
|
||||
diff --git a/internal/decision/envelope_signing_test.go b/internal/decision/envelope_signing_test.go
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/internal/decision/envelope_signing_test.go
|
||||
@@ -0,0 +1,64 @@
|
||||
+package decision_test
|
||||
+
|
||||
+import (
|
||||
+ "context"
|
||||
+ "crypto/ed25519"
|
||||
+ "path/filepath"
|
||||
+ "reflect"
|
||||
+ "testing"
|
||||
+
|
||||
+ "github.com/netkingdom/access-engine/internal/sign"
|
||||
+ "github.com/netkingdom/access-engine/pkg/api"
|
||||
+)
|
||||
+
|
||||
+type signingRecorder struct{ records []api.DecisionEnvelope }
|
||||
+
|
||||
+func (r *signingRecorder) Append(e api.DecisionEnvelope) error {
|
||||
+ r.records = append(r.records, e)
|
||||
+ return nil
|
||||
+}
|
||||
+
|
||||
+func TestSignerAuthenticatesReturnedAndRecordedEnvelope(t *testing.T) {
|
||||
+ engine := newTestEngine(t)
|
||||
+ var request api.CheckRequest
|
||||
+ loadYAML(t, filepath.Join("..", "..", "examples", "caring", "check_request.yaml"), &request)
|
||||
+ unsigned, err := engine.Check(context.Background(), request)
|
||||
+ if err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ // Synthetic deterministic seed only; no estate credential is read.
|
||||
+ key := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize))
|
||||
+ public := append(ed25519.PublicKey(nil), key.Public().(ed25519.PublicKey)...)
|
||||
+ if err := engine.SetEnvelopeSigningKey(key, "synthetic-current"); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ key[0] ^= 1 // Setter must not retain the caller's mutable key slice.
|
||||
+ recorder := &signingRecorder{}
|
||||
+ engine.SetDecisionLog(recorder)
|
||||
+ signed, err := engine.Check(context.Background(), request)
|
||||
+ if err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if err := sign.Verify(signed, map[string]ed25519.PublicKey{"synthetic-current": public}); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if len(recorder.records) != 1 || !reflect.DeepEqual(signed, recorder.records[0]) {
|
||||
+ t.Fatal("recorded envelope differs from authenticated return")
|
||||
+ }
|
||||
+ if signed.Binding.RequestDigest != unsigned.Binding.RequestDigest {
|
||||
+ t.Fatal("signing changed request digest")
|
||||
+ }
|
||||
+ signed.Provenance.PolicyVersion = "altered"
|
||||
+ if sign.Verify(signed, map[string]ed25519.PublicKey{"synthetic-current": public}) == nil {
|
||||
+ t.Fatal("altered provenance verified")
|
||||
+ }
|
||||
+}
|
||||
+func TestSignerRejectsInvalidConfiguration(t *testing.T) {
|
||||
+ engine := newTestEngine(t)
|
||||
+ if engine.SetEnvelopeSigningKey(nil, "kid") == nil {
|
||||
+ t.Fatal("missing key accepted")
|
||||
+ }
|
||||
+ if engine.SetEnvelopeSigningKey(ed25519.NewKeyFromSeed(make([]byte, 32)), "") == nil {
|
||||
+ t.Fatal("missing kid accepted")
|
||||
+ }
|
||||
+}
|
||||
diff --git a/charts/access-engine/values.yaml b/charts/access-engine/values.yaml
|
||||
--- a/charts/access-engine/values.yaml
|
||||
+++ b/charts/access-engine/values.yaml
|
||||
@@ -85,3 +85,11 @@
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 192Mi
|
||||
+
|
||||
+# Inert until an independently admitted signer and existing custody Secret exist.
|
||||
+envelopeSigning:
|
||||
+ enabled: false
|
||||
+ kid: ""
|
||||
+ secretName: ""
|
||||
+ secretKey: ""
|
||||
+ mountPath: /var/run/secrets/decision-envelope-signing
|
||||
diff --git a/charts/access-engine/templates/deployment.yaml b/charts/access-engine/templates/deployment.yaml
|
||||
--- a/charts/access-engine/templates/deployment.yaml
|
||||
+++ b/charts/access-engine/templates/deployment.yaml
|
||||
@@ -10,6 +10,14 @@
|
||||
{{- end }}
|
||||
{{- if or (not (hasPrefix "/" .Values.auditDelivery.mountPath)) (contains ".." .Values.auditDelivery.mountPath) }}
|
||||
{{- fail "auditDelivery.mountPath must be an absolute normalized directory" }}
|
||||
+{{- end }}
|
||||
+{{- end }}
|
||||
+{{- if .Values.envelopeSigning.enabled }}
|
||||
+{{- if ne (first .Values.args) "serve" }}
|
||||
+{{- fail "envelopeSigning currently supports serve decision pins only" }}
|
||||
+{{- end }}
|
||||
+{{- if or (not (hasPrefix "/" .Values.envelopeSigning.mountPath)) (contains ".." .Values.envelopeSigning.mountPath) }}
|
||||
+{{- fail "envelopeSigning.mountPath must be an absolute normalized directory" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
apiVersion: apps/v1
|
||||
@@ -53,6 +61,12 @@
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
args:
|
||||
{{- toYaml .Values.args | nindent 12 }}
|
||||
+ {{- if .Values.envelopeSigning.enabled }}
|
||||
+ - --envelope-signing-key-file
|
||||
+ - {{ printf "%s/seed" .Values.envelopeSigning.mountPath | quote }}
|
||||
+ - --envelope-signing-kid
|
||||
+ - {{ required "envelopeSigning.kid must be independently admitted" .Values.envelopeSigning.kid | quote }}
|
||||
+ {{- end }}
|
||||
{{- if .Values.outbox.enabled }}
|
||||
- --outbox-dir
|
||||
- {{ .Values.outbox.mountPath | quote }}
|
||||
@@ -114,8 +128,13 @@
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: true
|
||||
- {{- if or .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "access-engine.callerAuth.enabled" .) "true") }}
|
||||
+ {{- if or .Values.envelopeSigning.enabled .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "access-engine.callerAuth.enabled" .) "true") }}
|
||||
volumeMounts:
|
||||
+ {{- if .Values.envelopeSigning.enabled }}
|
||||
+ - mountPath: {{ .Values.envelopeSigning.mountPath | quote }}
|
||||
+ name: access-engine-envelope-signing
|
||||
+ readOnly: true
|
||||
+ {{- end }}
|
||||
{{- if .Values.auditDelivery.enabled }}
|
||||
- mountPath: {{ .Values.auditDelivery.mountPath | quote }}
|
||||
name: access-engine-audit
|
||||
@@ -131,8 +150,17 @@
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
- {{- if or .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "access-engine.callerAuth.enabled" .) "true") }}
|
||||
+ {{- if or .Values.envelopeSigning.enabled .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "access-engine.callerAuth.enabled" .) "true") }}
|
||||
volumes:
|
||||
+ {{- if .Values.envelopeSigning.enabled }}
|
||||
+ - name: access-engine-envelope-signing
|
||||
+ secret:
|
||||
+ secretName: {{ required "envelopeSigning.secretName must name an admitted existing Secret" .Values.envelopeSigning.secretName | quote }}
|
||||
+ defaultMode: 0440
|
||||
+ items:
|
||||
+ - key: {{ required "envelopeSigning.secretKey is required" .Values.envelopeSigning.secretKey | quote }}
|
||||
+ path: seed
|
||||
+ {{- end }}
|
||||
{{- if .Values.auditDelivery.enabled }}
|
||||
- name: access-engine-audit
|
||||
secret:
|
||||
diff --git a/charts/flex-auth/values.yaml b/charts/flex-auth/values.yaml
|
||||
--- a/charts/flex-auth/values.yaml
|
||||
+++ b/charts/flex-auth/values.yaml
|
||||
@@ -85,3 +85,11 @@
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 192Mi
|
||||
+
|
||||
+# Inert until an independently admitted signer and existing custody Secret exist.
|
||||
+envelopeSigning:
|
||||
+ enabled: false
|
||||
+ kid: ""
|
||||
+ secretName: ""
|
||||
+ secretKey: ""
|
||||
+ mountPath: /var/run/secrets/decision-envelope-signing
|
||||
diff --git a/charts/flex-auth/templates/deployment.yaml b/charts/flex-auth/templates/deployment.yaml
|
||||
--- a/charts/flex-auth/templates/deployment.yaml
|
||||
+++ b/charts/flex-auth/templates/deployment.yaml
|
||||
@@ -10,6 +10,14 @@
|
||||
{{- end }}
|
||||
{{- if or (not (hasPrefix "/" .Values.auditDelivery.mountPath)) (contains ".." .Values.auditDelivery.mountPath) }}
|
||||
{{- fail "auditDelivery.mountPath must be an absolute normalized directory" }}
|
||||
+{{- end }}
|
||||
+{{- end }}
|
||||
+{{- if .Values.envelopeSigning.enabled }}
|
||||
+{{- if ne (first .Values.args) "serve" }}
|
||||
+{{- fail "envelopeSigning currently supports serve decision pins only" }}
|
||||
+{{- end }}
|
||||
+{{- if or (not (hasPrefix "/" .Values.envelopeSigning.mountPath)) (contains ".." .Values.envelopeSigning.mountPath) }}
|
||||
+{{- fail "envelopeSigning.mountPath must be an absolute normalized directory" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
apiVersion: apps/v1
|
||||
@@ -52,6 +60,12 @@
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
args:
|
||||
{{- toYaml .Values.args | nindent 12 }}
|
||||
+ {{- if .Values.envelopeSigning.enabled }}
|
||||
+ - --envelope-signing-key-file
|
||||
+ - {{ printf "%s/seed" .Values.envelopeSigning.mountPath | quote }}
|
||||
+ - --envelope-signing-kid
|
||||
+ - {{ required "envelopeSigning.kid must be independently admitted" .Values.envelopeSigning.kid | quote }}
|
||||
+ {{- end }}
|
||||
{{- if .Values.outbox.enabled }}
|
||||
- --outbox-dir
|
||||
- {{ .Values.outbox.mountPath | quote }}
|
||||
@@ -107,8 +121,13 @@
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: true
|
||||
- {{- if or .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "flex-auth.callerAuth.enabled" .) "true") }}
|
||||
+ {{- if or .Values.envelopeSigning.enabled .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "flex-auth.callerAuth.enabled" .) "true") }}
|
||||
volumeMounts:
|
||||
+ {{- if .Values.envelopeSigning.enabled }}
|
||||
+ - mountPath: {{ .Values.envelopeSigning.mountPath | quote }}
|
||||
+ name: flex-auth-envelope-signing
|
||||
+ readOnly: true
|
||||
+ {{- end }}
|
||||
{{- if .Values.auditDelivery.enabled }}
|
||||
- mountPath: {{ .Values.auditDelivery.mountPath | quote }}
|
||||
name: flex-auth-audit
|
||||
@@ -124,8 +143,17 @@
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
- {{- if or .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "flex-auth.callerAuth.enabled" .) "true") }}
|
||||
+ {{- if or .Values.envelopeSigning.enabled .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "flex-auth.callerAuth.enabled" .) "true") }}
|
||||
volumes:
|
||||
+ {{- if .Values.envelopeSigning.enabled }}
|
||||
+ - name: flex-auth-envelope-signing
|
||||
+ secret:
|
||||
+ secretName: {{ required "envelopeSigning.secretName must name an admitted existing Secret" .Values.envelopeSigning.secretName | quote }}
|
||||
+ defaultMode: 0440
|
||||
+ items:
|
||||
+ - key: {{ required "envelopeSigning.secretKey is required" .Values.envelopeSigning.secretKey | quote }}
|
||||
+ path: seed
|
||||
+ {{- end }}
|
||||
{{- if .Values.auditDelivery.enabled }}
|
||||
- name: flex-auth-audit
|
||||
secret:
|
||||
diff --git a/docs/decision-envelope-signature.md b/docs/decision-envelope-signature.md
|
||||
--- a/docs/decision-envelope-signature.md
|
||||
+++ b/docs/decision-envelope-signature.md
|
||||
@@ -5,7 +5,7 @@
|
||||
Contract: additive on `flex-auth.decision-record.v1`
|
||||
Does **not** change `request_digest`.
|
||||
|
||||
-The response channel is unauthenticated today (`FLEX-DEC-2026-010`). Digests
|
||||
+The original response channel was unauthenticated (`FLEX-DEC-2026-010`). Digests
|
||||
cannot fix that: every input to them is sent by the caller or published.
|
||||
Fail-closed cannot fix that either: it covers a missing PDP, not a lying one.
|
||||
|
||||
@@ -117,3 +117,52 @@
|
||||
pair: one genuine signed envelope and one whose payload was altered after
|
||||
signing. It does **not** mint the production key and does **not** wait for
|
||||
the OpenBao lane to exist before the code path is testable.
|
||||
+
|
||||
+## NetKingdom target source follow-through (2026-10-16)
|
||||
+
|
||||
+Owner source review of Secrets Engine R2 patch
|
||||
+`98c4300c42fe93759a55297fe6629f1d4aeed249c3625f7fd37c8ab523a08ca9`
|
||||
+verified unchanged `api`/`internal/sign` bytes and producer replay fixtures at
|
||||
+`62ac2b4e99dec2005c7c407fe633612578a7907d`. The consumer runs the Go producer
|
||||
+verifier and uses its returned typed envelope, then independently checks issuer,
|
||||
+policy package/version/digest, exact submitted request and lifetime. This avoids
|
||||
+another envelope canonicalization implementation or PDP.
|
||||
+
|
||||
+That revision implemented `Sign` only for fixtures: `serve` could not load a key
|
||||
+or sign responses. The follow-through adds `serve --envelope-signing-key-file`
|
||||
+and `--envelope-signing-kid`. Configure both or neither. The mounted file contains
|
||||
+one unpadded base64url 32-byte Ed25519 seed, optionally with trailing whitespace;
|
||||
+it must be regular and have no group-write or world permissions (0440 is accepted). No production key is supplied
|
||||
+or minted here. Configuration failures abort startup. The key is read once:
|
||||
+rotation requires an owner rollout/restart, not an assumed hot reload.
|
||||
+
|
||||
+Signing occurs after the complete envelope is constructed and before the durable
|
||||
+recorder, for check, batch and list decisions alike. Returned and recorded objects
|
||||
+therefore agree. Existing unsigned developer mode remains explicit; authenticated
|
||||
+Secrets Engine production refuses it. The signature schema, canonical bytes and
|
||||
+request digest are unchanged, so this patch does not require a consumer vendored
|
||||
+source refresh.
|
||||
+
|
||||
+Both retained `flex-auth` and new `access-engine` charts expose an inert
|
||||
+`envelopeSigning` block. Enabling requires `kid`, `secretName`, `secretKey`, and an
|
||||
+absolute `mountPath`. They mount an existing custody-owned Secret read-only;
|
||||
+they do not provision it. The existing Secrets pin values remain unchanged and
|
||||
+unsigned until an exact reviewed image/values rollout is admitted.
|
||||
+
|
||||
+The `/v1/keys` route described earlier is a discovery design, **not an implemented
|
||||
+route**. It must never bootstrap independent trust. For this first consumer, the
|
||||
+owner transports reviewed public keys plus exact issuer/policy pins independently
|
||||
+into the consumer's admission file. No runtime discovery dependency is needed.
|
||||
+Rotation admits new public key alongside old in every consumer, proves new-key
|
||||
+acceptance, rolls producer to new seed/kid, waits past the maximum outstanding
|
||||
+allow lifetime plus bounded clock uncertainty, removes old key, and proves old-key
|
||||
+refusal. A revoked/compromised old key is removed immediately rather than retained
|
||||
+for overlap. Retain public verification material in audit custody for historic
|
||||
+records without treating it as current runtime authority.
|
||||
+
|
||||
+Rollout remains blocked on independent public-key and policy-pin admission;
|
||||
+OpenBao/Platform custody and exact secret mount; reviewed producer image/digest;
|
||||
+owner-bound loopback tunnel and caller audience; installation of the reviewed
|
||||
+consumer verifier binary (the Python wheel alone omits it); exact consumer source
|
||||
+and hash drift check; and native positive/negative/rotation plus record-custody
|
||||
+receipts. Local fixtures and chart renders do not establish those outcomes.
|
||||
diff --git a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
--- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
+++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md
|
||||
@@ -4,7 +4,7 @@
|
||||
title: "Sign the decision envelope: the response channel is unauthenticated"
|
||||
domain: infotech
|
||||
repo: flex-auth
|
||||
-status: finished
|
||||
+status: active
|
||||
owner: claude
|
||||
topic_slug: netkingdom
|
||||
planning_priority: P1
|
||||
@@ -15,7 +15,7 @@
|
||||
- FLEX-WP-0019
|
||||
- FLEX-WP-0023
|
||||
created: "2026-09-06"
|
||||
-updated: "2026-09-14"
|
||||
+updated: "2026-10-16"
|
||||
state_hub_workstream_id: "90577acd-6910-548d-a13e-1dbfdfb8ed27"
|
||||
---
|
||||
|
||||
@@ -163,3 +163,37 @@
|
||||
this version. §16 already carries the same observation one layer up, about the
|
||||
standard's own publication path having no digest or freeze discipline; F1 is
|
||||
that observation applied to the artifact the standard regulates.
|
||||
+
|
||||
+## 5. Wire owner signing into the decision pin and retained deployment charts
|
||||
+
|
||||
+```task
|
||||
+id: FLEX-WP-0024-T05
|
||||
+status: done
|
||||
+priority: high
|
||||
+```
|
||||
+
|
||||
+2026-10-16 source follow-through: NetKingdom target Secrets Engine R2 owner review
|
||||
+found that the completed T03 signature package was used only by fixtures. Add
|
||||
+explicit mounted seed/kid configuration for `serve`, sign the final envelope before
|
||||
+recording, and expose inert existing-Secret mounts in both retained/new charts.
|
||||
+The API/sign verifier sources remain byte-identical. Local source validation and
|
||||
+review are in the Custodian `docs/changes/netkingdom-target-2026-10-16/access-verifier-review/`.
|
||||
+This is source preparation only, not signer admission or deployed acceptance.
|
||||
+
|
||||
+## 6. Admit custody, consumer trust, exact packages and native signed decisions
|
||||
+
|
||||
+```task
|
||||
+id: FLEX-WP-0024-T06
|
||||
+status: wait
|
||||
+priority: high
|
||||
+```
|
||||
+
|
||||
+Wait on Platform/OpenBao key custody, independent public-key and issuer/policy-pin
|
||||
+admission, exact image and existing-Secret values rollout, owner-bound caller/tunnel,
|
||||
+and Secrets Engine verifier installation. Follow `docs/decision-envelope-signature.md`
|
||||
+for overlap/removal semantics. Retain signed producer record/consumer acceptance,
|
||||
+forged/altered/unsigned/wrong-pin/expired denial and rotation-removal receipts;
|
||||
+then update actual deployed status. No signer or runtime trust is admitted by the
|
||||
+source review. This task owns the producer-side native gate; SECRETS-WP-0014 owns
|
||||
+consumer admission/enforcement. The old fixture completion never established this
|
||||
+gate, so the workplan is reopened rather than claiming it already passed.
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Disposable local CLI->HTTP->exact R2 verifier proof; no production custody."""
|
||||
import argparse, base64, copy, hashlib, json, os, socket, subprocess, sys, tempfile, time
|
||||
from pathlib import Path
|
||||
from urllib.request import Request, build_opener, ProxyHandler
|
||||
|
||||
p=argparse.ArgumentParser();p.add_argument('--producer',type=Path,required=True);p.add_argument('--consumer',type=Path,required=True);p.add_argument('--receipt',type=Path,required=True);a=p.parse_args()
|
||||
producer=a.producer.resolve();consumer=a.consumer.resolve()
|
||||
sys.path.insert(0,str(consumer/'src'))
|
||||
from secrets_engine.decision_check import check_decision
|
||||
from secrets_engine.decision_signature import load_trust, authenticate_response
|
||||
from secrets_engine.authorization import validate_decision_envelope
|
||||
from secrets_engine.errors import DecisionError
|
||||
|
||||
def digest(path):return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
env={k:os.environ[k] for k in ('PATH','HOME','GOPATH') if k in os.environ}
|
||||
env.update(GOCACHE='/tmp/netkingdom-go-cache',GOMAXPROCS='2',GOFLAGS='-buildvcs=false')
|
||||
metadata_source='''package main
|
||||
import("context";"crypto/ed25519";"encoding/base64";"encoding/json";"os";"github.com/netkingdom/access-engine/internal/policy")
|
||||
func main(){ p,e:=policy.LoadAndValidateFile(context.Background(),os.Args[1]);if e!=nil{panic(e)};k:=ed25519.NewKeyFromSeed(make([]byte,32)); json.NewEncoder(os.Stdout).Encode(map[string]string{"public":base64.RawURLEncoding.EncodeToString(k.Public().(ed25519.PublicKey)),"policy_package":p.Metadata.ID,"policy_version":p.Metadata.Version,"policy_package_digest":p.Digest()}) }
|
||||
'''
|
||||
results={}
|
||||
with tempfile.TemporaryDirectory(prefix='access-serve-consumer-') as raw:
|
||||
d=Path(raw);binary=d/'access-engine';verifier=d/'verify';policy=producer/'examples/secrets-engine/policy_package.md'
|
||||
subprocess.run(['go','build','-p','2','-o',str(binary),'./cmd/access-engine'],cwd=producer,env=env,check=True)
|
||||
subprocess.run(['go','build','-p','2','-o',str(verifier),'.'],cwd=consumer/'verifier',env=env,check=True)
|
||||
# Located inside owner module solely to call the existing authoritative digest
|
||||
# function offline. Never derive expected pins from the HTTP responder.
|
||||
helper=producer/'tools/netkingdom_smoke_metadata.go'
|
||||
if helper.exists():raise RuntimeError('refusing to replace existing source')
|
||||
try:
|
||||
helper.write_text(metadata_source)
|
||||
metadata=json.loads(subprocess.check_output(['go','run',str(helper),str(policy)],cwd=producer,env=env))
|
||||
finally:helper.unlink(missing_ok=True)
|
||||
seed=d/'seed';seed.write_bytes(base64.urlsafe_b64encode(bytes(32)).rstrip(b'='));seed.chmod(0o440)
|
||||
trust={'schema':'secrets-engine.pdp-trust.v1','evaluator':'flex-auth/local','mode':'standalone',**{k:v for k,v in metadata.items() if k!='public'},'keys':{'synthetic-local-smoke':metadata['public']}}
|
||||
trust_path=d/'trust.json';trust_path.write_text(json.dumps(trust));trust_path.chmod(0o600)
|
||||
token=d/'token';token.write_text('synthetic-local-only');token.chmod(0o600)
|
||||
request=json.loads((producer/'examples/secrets-engine/check_request_allow_rotate.json').read_text())
|
||||
base=[str(binary),'serve','--registry',str(producer/'examples/secrets-engine/registry_snapshot.json'),'--policy',str(policy)]
|
||||
def start(signed):
|
||||
with socket.socket() as sock:sock.bind(('127.0.0.1',0));port=sock.getsockname()[1]
|
||||
log=d/('signed.jsonl' if signed else 'unsigned.jsonl')
|
||||
args=base+['--addr',f'127.0.0.1:{port}','--log',str(log)]
|
||||
if signed:args+=['--envelope-signing-key-file',str(seed),'--envelope-signing-kid','synthetic-local-smoke']
|
||||
process=subprocess.Popen(args,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,env={})
|
||||
origin=f'http://127.0.0.1:{port}'
|
||||
opener=build_opener(ProxyHandler({}))
|
||||
for _ in range(100):
|
||||
if process.poll() is not None:raise RuntimeError('disposable serve failed startup')
|
||||
try:
|
||||
with opener.open(origin+'/healthz',timeout=.2):return process,origin,log
|
||||
except OSError:time.sleep(.05)
|
||||
process.terminate();process.wait();raise RuntimeError('disposable serve not ready')
|
||||
proc,origin,log=start(True)
|
||||
try:
|
||||
authenticated=check_decision(base_url=origin,token_file=token,request=request,trust_file=trust_path,verifier=verifier)
|
||||
validate_decision_envelope(authenticated,request,accepted_policy_packages={metadata['policy_package']},accepted_policy_versions={metadata['policy_version']})
|
||||
recorded=json.loads(log.read_text().splitlines()[-1]);assert authenticated==recorded
|
||||
results['signed_cli_http_r2_consumer_and_request_lifetime_validation']='pass'
|
||||
results['returned_typed_authenticated_envelope_equals_recorded_envelope']='pass'
|
||||
altered=copy.deepcopy(recorded);altered['binding']['action']='destroy'
|
||||
try:authenticate_response(json.dumps(altered).encode(),trust=load_trust(trust_path),verifier=verifier)
|
||||
except DecisionError:results['tampered_actual_signed_response_refusal']='pass'
|
||||
else:raise AssertionError('tampered response accepted')
|
||||
finally:proc.terminate();proc.wait(timeout=5)
|
||||
proc,origin,log=start(False)
|
||||
try:
|
||||
try:check_decision(base_url=origin,token_file=token,request=request,trust_file=trust_path,verifier=verifier)
|
||||
except DecisionError:results['unsigned_actual_cli_http_response_refusal']='pass'
|
||||
else:raise AssertionError('unsigned response accepted')
|
||||
assert json.loads(log.read_text().splitlines()[-1])['signature']['mode']=='unsigned'
|
||||
finally:proc.terminate();proc.wait(timeout=5)
|
||||
# Actual CLI startup must reject a group-writable seed before listening.
|
||||
seed.chmod(0o620)
|
||||
result=subprocess.run(base+['--addr','127.0.0.1:0','--envelope-signing-key-file',str(seed),'--envelope-signing-kid','synthetic-local-smoke'],env={},capture_output=True,timeout=15)
|
||||
assert result.returncode!=0 and b'group-write' in result.stderr
|
||||
results['group_writable_seed_cli_startup_refusal']='pass'
|
||||
a.receipt.write_text(json.dumps({'schema':'access-engine.local-serve-consumer-proof.v1','results':results,'independent_source_policy_pin':{k:v for k,v in metadata.items() if k!='public'},'synthetic_public_key':metadata['public'],'synthetic_key_only':True,'producer_binary_sha256':digest(binary),'consumer_verifier_sha256':digest(verifier),'consumer_source_manifest_sha256':digest(consumer/'verifier/producer-source.json'),'signed_envelope':authenticated,'record_equals_response':True,'boundary':'disposable literal loopback serve; caller-auth disabled with synthetic token, no production credential or authorization; proves CLI signer wiring and exact R2 verification only'},indent=2)+'\n')
|
||||
print(json.dumps(results,sort_keys=True))
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
# Combined runtime source integration — 16 October 2026
|
||||
|
||||
The next source blocker is the rein runtime lock: it still pins Glas before the
|
||||
published workload-only transport patch. [The lock patch](rein-runtime-lock.patch)
|
||||
updates only Glas `30be8180` → `7357115c`, against rein `be5ef400`.
|
||||
[Exact source inputs](source-inputs.json) retain llm-connect `37436bb5` and
|
||||
sandboxer `221126cb`; [lock verification](runtime-lock-check.json) passes.
|
||||
[The owner work-record note](owner-work-record-note.md) is prepared for integration
|
||||
under REINAH-WP-0008-T07 and GLAS-WP-0012. The initial preparation preserved canonical sources; the reviewed lock and T07 note
|
||||
were subsequently integrated and published as described below.
|
||||
|
||||
|
||||
## Published owner source and rebuilt artifact
|
||||
|
||||
Rein source `9b88cadaa817e5abea7950b4a264d4e2739a5ddb` is published on `origin/main`
|
||||
through normal State Hub consistency sync, with a clean canonical checkout.
|
||||
[Publication receipt](canonical-publication.json) retains the existing C-20
|
||||
unlinked dependency and C-39 T07 missing decision-id warnings; no check failed.
|
||||
Only the reviewed lock and existing T07 workplan note were committed. T07 remains
|
||||
waiting; its native gates are unchanged.
|
||||
|
||||
[Canonical source inputs](canonical-source-inputs.json) and
|
||||
[lock check](canonical-lock-check.json) identify the clean detached sources used
|
||||
by the existing owner builder. [Canonical build metadata](canonical-build-result.json)
|
||||
records complete runtime SHA-256
|
||||
`f59959b605a51abd2901becde14e7abd852c2bd3f848004b51d250d1cb81af6d`,
|
||||
at `/tmp/netkingdom-runtime-canonical-20261016/runtime`. The artifact is retained
|
||||
only as a candidate; it is not installed or admitted.
|
||||
|
||||
[Actual manager/gateway proof](canonical-manager-proof.json) passes against this
|
||||
new artifact with the unchanged, unverified `harness.agent-dev-local@1.1.1`.
|
||||
It repeats installed rein/Glas/Claude startup, synthetic file/environment/child
|
||||
read-denial, read-only runtime, exact identity, private state, deterministic
|
||||
workspace commit, task removal and teardown. The separate synthetic route fixture
|
||||
proves delivery only to the explicit workload, output redaction, empty routes for
|
||||
housekeeping and clean following execution. Actual manager wrong-consumer,
|
||||
expiry and cancellation refusals/cleanup pass. No model/provider call, real
|
||||
credential read or spend occurred. [Digest after proof](canonical-digest-after-proof.json)
|
||||
is unchanged. [Seven canonical rein lock/rehearsal tests](canonical-rein-tests.txt)
|
||||
pass; Glas inputs remain exactly those whose 141 tests passed above.
|
||||
|
||||
[Progress receipt](canonical-progress-receipt.json) records the source delivery
|
||||
under existing T07. Native installation/admission, required spend/approval gates,
|
||||
real model acceptance and current interactive assignment migration remain open.
|
||||
|
||||
## Preserved initial preparation
|
||||
|
||||
The existing owner builder produced `/tmp/netkingdom-runtime-next-20261016/runtime`
|
||||
with complete SHA-256
|
||||
`634e65fee5517ebee0d1080805fd84c81bd46af26a2b38ec1ab14bd2be17bf8f`.
|
||||
[Build metadata](build-result.json) records all package versions, exact source
|
||||
commits, native Claude 2.1.266 binary hash, frozen uv.lock hash and source-content
|
||||
manifest. This includes isolated preparation commit `c811fc43`, not an owner
|
||||
publication. An owner-integrated revision must produce a newly identified
|
||||
artifact; it cannot inherit this digest.
|
||||
|
||||
[Actual manager/gateway proof](manager-proof.json) uses the candidate interpreter
|
||||
and installed packages. Exact `harness.agent-dev-local@1.1.1` remains unverified;
|
||||
no profile override is used for its startup/denial/cleanup proof. A separate,
|
||||
explicitly labelled synthetic credential-route fixture exercises workload-only
|
||||
delivery through the actual manager. [Proof source](prove-candidate-manager.py)
|
||||
is reproducible with the candidate interpreter:
|
||||
|
||||
```bash
|
||||
env -i PATH=/usr/bin:/bin HOME=/tmp SANDBOXER_NO_STATE_HUB=1 \
|
||||
/tmp/netkingdom-runtime-next-20261016/runtime/bin/python3 -I -B \
|
||||
docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/prove-candidate-manager.py \
|
||||
634e65fee5517ebee0d1080805fd84c81bd46af26a2b38ec1ab14bd2be17bf8f
|
||||
```
|
||||
|
||||
All [141 Glas tests](glas-tests.txt) and [seven rein lock/rehearsal tests](rein-tests.txt)
|
||||
pass. [Digest after proofs](digest-after-proof.json) matches the built artifact.
|
||||
[Proof attempts](proof-attempts.json) retain the first runner failure: the manager
|
||||
correctly raised PermissionError, while the runner expected a different refusal
|
||||
exception; finally cleanup succeeded, and the runner catch was corrected.
|
||||
|
||||
Native installation/admission, real-model acceptance, current interactive
|
||||
assignment migration, denial through its external tool paths and runner scope
|
||||
acceptance remain open. No real credentials, provider request, model spend,
|
||||
queue access, native pin changes, readiness promotion or supervision grant were
|
||||
used. Existing failed artifacts and owner gates remain authoritative.
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"runtime": {
|
||||
"path": "/tmp/netkingdom-runtime-next-20261016/runtime",
|
||||
"sha256": "634e65fee5517ebee0d1080805fd84c81bd46af26a2b38ec1ab14bd2be17bf8f"
|
||||
},
|
||||
"build": {
|
||||
"python": "3.12.3",
|
||||
"packages": {
|
||||
"h11": "0.16.0",
|
||||
"httpcore": "1.0.9",
|
||||
"llm-connect": "0.1.0",
|
||||
"fastapi": "0.141.1",
|
||||
"anyio": "4.14.2",
|
||||
"annotated-doc": "0.0.5",
|
||||
"idna": "3.18",
|
||||
"shellingham": "1.5.4",
|
||||
"certifi": "2026.7.22",
|
||||
"starlette": "1.6.0",
|
||||
"PyYAML": "6.0.3",
|
||||
"typer": "0.27.1",
|
||||
"toml": "0.10.2",
|
||||
"pydantic_core": "2.46.4",
|
||||
"sandboxer": "0.0.0",
|
||||
"typing_extensions": "4.16.0",
|
||||
"httpx": "0.28.1",
|
||||
"Pygments": "2.20.0",
|
||||
"markdown-it-py": "4.2.0",
|
||||
"pydantic": "2.13.4",
|
||||
"rich": "15.0.0",
|
||||
"rein-aharness": "0.1.0",
|
||||
"annotated-types": "0.8.0",
|
||||
"glas-harness": "0.1.0",
|
||||
"typing-inspection": "0.4.4",
|
||||
"mdurl": "0.1.2"
|
||||
},
|
||||
"claude": {
|
||||
"path": "/opt/sandboxer/runtime/bin/claude",
|
||||
"sha256": "19842705e989393fce936804df6d2ab034860e24b8f8880357981d87ffd83fac",
|
||||
"expected_version": "2.1.266",
|
||||
"source": "/home/worsch/.local/share/sandboxer/runtimes/818b1b7c8292e904a05e3f215fef9b67a61e7c00d34bfa36689352e8003df359/bin/claude"
|
||||
},
|
||||
"relocated_entrypoints": [
|
||||
"fastapi",
|
||||
"glas-harness",
|
||||
"httpx",
|
||||
"idna",
|
||||
"llm-connect",
|
||||
"markdown-it",
|
||||
"pygmentize",
|
||||
"rein-aharness",
|
||||
"sandboxer",
|
||||
"typer"
|
||||
],
|
||||
"source_revisions": {
|
||||
"rein-aharness": "c811fc430f80e214b43cfcca5dc9e7f786d5f559",
|
||||
"llm-connect": "37436bb562a3f20abc3b88defc5bae6508c32e7e",
|
||||
"glas-harness": "7357115c8311e55ee3d495f5266f501a17ec3c89",
|
||||
"sandboxer": "221126cb77ea41ec283598a23d628f760a51bdea"
|
||||
},
|
||||
"source_contents": {
|
||||
"files_verified": 168,
|
||||
"manifest_sha256": "3c5cfd95c0accce81f59fbf06f501db238cca38975cf75d518a7c51986b28968"
|
||||
},
|
||||
"owner_runtime": true,
|
||||
"runtime_contract": {
|
||||
"dependencies": [
|
||||
{
|
||||
"clean": true,
|
||||
"commit": "37436bb562a3f20abc3b88defc5bae6508c32e7e",
|
||||
"distribution": "llm-connect",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
{
|
||||
"clean": true,
|
||||
"commit": "7357115c8311e55ee3d495f5266f501a17ec3c89",
|
||||
"distribution": "glas-harness",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
{
|
||||
"clean": true,
|
||||
"commit": "221126cb77ea41ec283598a23d628f760a51bdea",
|
||||
"distribution": "sandboxer",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
],
|
||||
"ok": true,
|
||||
"schema_version": "1"
|
||||
},
|
||||
"uv_lock_sha256": "8775904b5c0b19aa16e09c25f7e33d1877abeef894b2d0127843c94fcff32683"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"runtime_sha256_after_proofs": "f59959b605a51abd2901becde14e7abd852c2bd3f848004b51d250d1cb81af6d"}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"ok": true,
|
||||
"runtime_sha256": "f59959b605a51abd2901becde14e7abd852c2bd3f848004b51d250d1cb81af6d",
|
||||
"gateway": {
|
||||
"ok": true,
|
||||
"profile": "harness.agent-dev-local@1.1.1",
|
||||
"readiness": "unverified",
|
||||
"proof": {
|
||||
"installed_packages": true,
|
||||
"host_file_denied": true,
|
||||
"inherited_env_and_child_denied": true,
|
||||
"runtime_readonly": true,
|
||||
"rein_cli_started": true,
|
||||
"glas_cli_started": true,
|
||||
"claude_version": "2.1.266",
|
||||
"loopback_only": true,
|
||||
"task_mode": "0600",
|
||||
"identity_exact": true,
|
||||
"credential_refs": [],
|
||||
"private_home": true
|
||||
},
|
||||
"task_removed_before_teardown": true,
|
||||
"workspace_removed": true,
|
||||
"state": "destroyed",
|
||||
"source_unchanged": true,
|
||||
"deterministic_commit": true,
|
||||
"model_run_proven": false
|
||||
},
|
||||
"synthetic_route_fixture": {
|
||||
"scope": "separate synthetic credential-route fixture using actual manager and gateway",
|
||||
"routes_by_owner_command": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"synthetic"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"exactly_one_explicit_workload_delivery": true,
|
||||
"provider_output_redacted": true,
|
||||
"subsequent_housekeeping_clean": true,
|
||||
"workspace_removed": true,
|
||||
"readiness_changed": false
|
||||
},
|
||||
"manager": {
|
||||
"wrong_consumer_refused": {
|
||||
"actor": true,
|
||||
"project": true,
|
||||
"run_id": true
|
||||
},
|
||||
"expired_execution_refused": true,
|
||||
"expiry_cleanup": true,
|
||||
"cancellation_cleanup": true,
|
||||
"execution_after_cancellation_refused": true
|
||||
},
|
||||
"model_calls": 0,
|
||||
"real_credentials_read": false,
|
||||
"native_admission": false,
|
||||
"interactive_assignment_migrated": false
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"workplan_id": "187c2599-84a6-5764-a14f-7e9121975bf2",
|
||||
"id": "c7c0080e-17d7-42b9-b621-511ed0eaf7a7",
|
||||
"topic_id": null,
|
||||
"task_id": "80318842-ccaa-5484-a54a-3d9981eaec8b",
|
||||
"decision_id": null,
|
||||
"event_type": "note",
|
||||
"summary": "REINAH-WP-0008-T07 source preparation published as rein9b88cadaa817e5abea7950b4a264d4e2739a5ddb: exact Glas7357115c runtime lock, retained llm37436bb5/sandboxer221126cb. Canonical-source runtime f59959b605a51abd2901becde14e7abd852c2bd3f848004b51d250d1cb81af6d passes actual manager/gateway startup, synthetic denial, deterministic tool/commit effect, workload-only synthetic delivery/redaction, identity mismatch refusal, expiry/cancellation cleanup. Prior failed proof-runner attempt and artifact history retained. T07 remains wait; no install/admission, model/provider call, credential read, spend or readiness/supervision promotion. Evidence the-custodian/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/.",
|
||||
"detail": null,
|
||||
"author": "codex",
|
||||
"session_id": null,
|
||||
"created_at": "2026-10-16T18:59:58.773864Z",
|
||||
"workstream_id": "187c2599-84a6-5764-a14f-7e9121975bf2"
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"runtime_sha256_after_proofs": "634e65fee5517ebee0d1080805fd84c81bd46af26a2b38ec1ab14bd2be17bf8f"}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
........................................................................ [ 51%]
|
||||
..................................................................... [100%]
|
||||
141 passed in 6.13s
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"ok": true,
|
||||
"runtime_sha256": "634e65fee5517ebee0d1080805fd84c81bd46af26a2b38ec1ab14bd2be17bf8f",
|
||||
"gateway": {
|
||||
"ok": true,
|
||||
"profile": "harness.agent-dev-local@1.1.1",
|
||||
"readiness": "unverified",
|
||||
"proof": {
|
||||
"installed_packages": true,
|
||||
"host_file_denied": true,
|
||||
"inherited_env_and_child_denied": true,
|
||||
"runtime_readonly": true,
|
||||
"rein_cli_started": true,
|
||||
"glas_cli_started": true,
|
||||
"claude_version": "2.1.266",
|
||||
"loopback_only": true,
|
||||
"task_mode": "0600",
|
||||
"identity_exact": true,
|
||||
"credential_refs": [],
|
||||
"private_home": true
|
||||
},
|
||||
"task_removed_before_teardown": true,
|
||||
"workspace_removed": true,
|
||||
"state": "destroyed",
|
||||
"source_unchanged": true,
|
||||
"deterministic_commit": true,
|
||||
"model_run_proven": false
|
||||
},
|
||||
"synthetic_route_fixture": {
|
||||
"scope": "separate synthetic credential-route fixture using actual manager and gateway",
|
||||
"routes_by_owner_command": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"synthetic"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"exactly_one_explicit_workload_delivery": true,
|
||||
"provider_output_redacted": true,
|
||||
"subsequent_housekeeping_clean": true,
|
||||
"workspace_removed": true,
|
||||
"readiness_changed": false
|
||||
},
|
||||
"manager": {
|
||||
"wrong_consumer_refused": {
|
||||
"actor": true,
|
||||
"project": true,
|
||||
"run_id": true
|
||||
},
|
||||
"expired_execution_refused": true,
|
||||
"expiry_cleanup": true,
|
||||
"cancellation_cleanup": true,
|
||||
"execution_after_cancellation_refused": true
|
||||
},
|
||||
"model_calls": 0,
|
||||
"real_credentials_read": false,
|
||||
"native_admission": false,
|
||||
"interactive_assignment_migrated": false
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"README.md": "15c2c174b41848263ba5f4bb5d07cb96d7829a4ca8d016496a2dc56797bd02be",
|
||||
"build-result.json": "421c43c45912d85ffc7b419f247feb97272c30281e53efb35c6c8e004eed7bf3",
|
||||
"canonical-build-provenance.json": "5de8f9630d33f1b8cdda24c4413893f5d7926286c9f586d9aa9976e8c182cf08",
|
||||
"canonical-build-result.json": "b8963af0107698b8d8d0329eea94eb2ea017ea16facb8f3a3db8ba46f61400de",
|
||||
"canonical-digest-after-proof.json": "5e68924669327aca862df1f2eb1bf5cb081aba6cc24157d6ef95f30f8c1e2df0",
|
||||
"canonical-lock-check.json": "1da98f6b76022b80ca2bbbb36347a58e632660d99a27f971d7d6d391898cfa29",
|
||||
"canonical-manager-proof.json": "1fc6b0c79939ea4706ff786e4cd040ab2385850a4617325681234bed7c3414c3",
|
||||
"canonical-manager-proof.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"canonical-progress-receipt.json": "e0c040addcf89d8bf0084d787fd067c6f16e4c0bff12ebddb8ab290ec6420840",
|
||||
"canonical-publication.json": "0a3d1d65e0ab8caf5a6fadbc4354ebfbb339622f8870b1606ed04fb24e4a17c8",
|
||||
"canonical-rein-tests.txt": "1874b47a6432969fc829a7aa42f0385f4307a8d7af57a127368403918405344e",
|
||||
"canonical-source-inputs.json": "76af7574bbf4d743e964bf8d62d3492be11adf36e757d931cd211ed0ed725cff",
|
||||
"digest-after-proof.json": "e73991e56513cb1d53f7d6ee95f9edcadf7b89c3c1728e81cc033869de84d8e4",
|
||||
"glas-tests.txt": "452bd934cc8bec117252184314bc7a4f6026bd8f62ef207cf4cabd4bbcacab71",
|
||||
"manager-proof.json": "701cc2ef9b2ea056e07f55b6a1637632fe52db78d3ecb1212858e7e09aad2e25",
|
||||
"owner-work-record-note.md": "976a58573ed2ce43ff53f7c8c7704b7d8fbcbb714d407d4376970ee3af08d8b5",
|
||||
"proof-attempts.json": "562ded7cde54d310556235583e87d5e9d07567c7df5a9901c0405db04853479a",
|
||||
"prove-candidate-manager.py": "8de7443001b2a6ea7ca50be937736420b360efa91aaef00178b6df8c84ded6a8",
|
||||
"rein-runtime-lock.patch": "5a83b85125bb40ce7ae209904ef73585d3c446ed4c9d549474098ab17db0b4cf",
|
||||
"rein-tests.txt": "55493507b7aa18a8cc82091b1ff98b8c42bd6ab2fff3e518139c9c364bccb147",
|
||||
"runtime-lock-check.json": "1da98f6b76022b80ca2bbbb36347a58e632660d99a27f971d7d6d391898cfa29",
|
||||
"source-inputs.json": "c0af2bf748cd24ffd1bf64387ba454ca46f6a1a59e57af42049e72a06abf6406"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue