Prepare signed lifecycle runtime admission and receipt verification
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Failing after 59s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
tegwick 2026-10-16 10:48:28 +02:00
parent ec21518e25
commit 919ce25a37
9 changed files with 364 additions and 3 deletions

View file

@ -51,7 +51,7 @@ jobs:
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

View file

@ -83,6 +83,8 @@ spec:
{{- 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

View file

@ -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."
}
}

View file

@ -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"
}
}

View file

@ -11,8 +11,16 @@ role: policy-decision-point
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

View file

@ -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()

View file

@ -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())

View file

@ -13,6 +13,8 @@ args:
- /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:

View file

@ -184,7 +184,7 @@ This is source preparation only, not signer admission or deployed acceptance.
```task
id: FLEX-WP-0024-T06
status: wait
status: progress
priority: high
```
@ -197,3 +197,32 @@ then update actual deployed status. No signer or runtime trust is admitted by th
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.