the-custodian/docs/changes/netkingdom-target-2026-10-16/access-native-source/b1/native-prep-r2-and-b1-full.patch
codex a4c1663558
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Reject generated-only commits / generated-commit-guard (push) Successful in 16s
docs: preserve reviewed NetKingdom session artifacts
2026-11-01 22:40:29 +01:00

1201 lines
60 KiB
Diff

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,43 @@
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.
+
+
+2026-10-16 separate B1 source follow-through wires ServicePilotInputs through actual
+HTTP/TokenReview into a signed-DENY-only engine entry. Generic `execute-ccr` calls
+refuse across every policy, including the old separately named positive fixture;
+that proof stays pinned to ec21518e. Original context/submitted digest stay unchanged.
+Actual disposable serve/TLS TokenReview/HTTP signed-DENY and bypass/refusal tests are
+source evidence only. B2 depends on independently reviewed Approval fact contract
+and canonical vectors; B3 additionally needs exact source/normalization numeric
+representation and full trusted provider joins. No current policy false gate,
+production trust, native deployment or protected effect is enabled by B1.
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
@@ -22,6 +22,7 @@
"github.com/netkingdom/access-engine/internal/emission"
"github.com/netkingdom/access-engine/internal/policy"
"github.com/netkingdom/access-engine/internal/registry"
+ "github.com/netkingdom/access-engine/internal/taskexecution"
"github.com/netkingdom/access-engine/pkg/api"
)
@@ -397,7 +398,11 @@
return fail(stderr, err)
}
- mux := newServeMuxWithCallerAuth(engine, authenticator)
+ serviceInputs, err := buildServicePilotInputs(engine, callerauth.Mode(*callerAuthMode), *callerAudience, *callerKubernetesURL, *callerReviewerTokenFile, *callerCAFile, callerBindings.StringMap())
+ if err != nil {
+ return fail(stderr, err)
+ }
+ mux := newServeMuxWithServicePilotInputs(engine, authenticator, serviceInputs)
if outbox != nil {
mountEmission(mux, outbox)
}
@@ -448,6 +453,10 @@
}
func newServeMuxWithCallerAuth(engine *decisioncore.Engine, authenticator *callerauth.Authenticator) *http.ServeMux {
+ return newServeMuxWithServicePilotInputs(engine, authenticator, nil)
+}
+
+func newServeMuxWithServicePilotInputs(engine *decisioncore.Engine, authenticator *callerauth.Authenticator, serviceInputs *taskexecution.ServicePilotInputs) *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("content-type", "application/json")
@@ -458,6 +467,25 @@
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
+ if engine.IsServicePilot() {
+ if serviceInputs == nil {
+ http.Error(w, "service input unavailable", http.StatusServiceUnavailable)
+ return
+ }
+ request, err := taskexecution.DecodeServiceRequest(r.Body)
+ if err != nil || request.Purpose != "" {
+ http.Error(w, "invalid service task request", http.StatusBadRequest)
+ return
+ }
+ input, err := serviceInputs.Prepare(r.Context(), r.Header.Get("Authorization"), request, time.Now().UTC())
+ if err != nil {
+ http.Error(w, "service task input refused", http.StatusForbidden)
+ return
+ }
+ decision, err := engine.CheckServicePilot(r.Context(), input)
+ writeHTTP(w, decision, err)
+ return
+ }
var request api.CheckRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
@@ -478,6 +506,10 @@
writeHTTP(w, decision, err)
})
mux.HandleFunc("/v1/batch_check", func(w http.ResponseWriter, r *http.Request) {
+ if engine.IsServicePilot() {
+ http.Error(w, "service task batch unavailable", http.StatusForbidden)
+ return
+ }
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
@@ -500,6 +532,22 @@
writeHTTP(w, decisions, err)
})
return mux
+}
+
+// Only the existing exact service pilot package selects this source path.
+// This is not an eligibility toggle; successful preparation still only DENYs.
+func buildServicePilotInputs(engine *decisioncore.Engine, mode callerauth.Mode, audience, kubernetesURL, reviewerTokenFile, caFile string, bindings map[string]string) (*taskexecution.ServicePilotInputs, error) {
+ if !engine.IsServicePilot() {
+ return nil, nil
+ }
+ if mode != callerauth.ModeEnforce || audience != "flex-auth" || len(bindings) != 1 || bindings["secrets-engine"] != taskexecution.ServicePilotPrincipal {
+ return nil, fmt.Errorf("service pilot requires exact enforced retained caller mapping")
+ }
+ reviewer, err := callerauth.NewKubernetesTokenReviewer(kubernetesURL, audience, reviewerTokenFile, caFile)
+ if err != nil {
+ return nil, err
+ }
+ return taskexecution.NewServicePilotInputs(reviewer)
}
func buildCallerAuthenticator(mode callerauth.Mode, audience, kubernetesURL, reviewerTokenFile, caFile string, bindings map[string]string, stderr io.Writer) (*callerauth.Authenticator, error) {
@@ -615,7 +663,7 @@
func writeHTTP(w http.ResponseWriter, value any, err error) {
w.Header().Set("content-type", "application/json")
- if errors.Is(err, decisioncore.ErrRecordNotCommitted) {
+ if errors.Is(err, decisioncore.ErrRecordNotCommitted) || errors.Is(err, decisioncore.ErrServicePilotUnavailable) {
// Unavailable evaluation is not an authorization verdict (GH-DEC-2026-024).
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
diff --git a/cmd/access-engine/service_pilot_http_test.go b/cmd/access-engine/service_pilot_http_test.go
new file mode 100644
--- /dev/null
+++ b/cmd/access-engine/service_pilot_http_test.go
@@ -0,0 +1,376 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "crypto/ed25519"
+ "encoding/base64"
+ "encoding/json"
+ "encoding/pem"
+ "io"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/netkingdom/access-engine/internal/audit"
+ "github.com/netkingdom/access-engine/internal/callerauth"
+ "github.com/netkingdom/access-engine/internal/decision"
+ "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/internal/taskexecution"
+ "github.com/netkingdom/access-engine/pkg/api"
+)
+
+func pilotRequest(now time.Time) api.CheckRequest {
+ return api.CheckRequest{ID: "source-only-service-http", Tenant: "tenant:platform", Subject: api.SubjectRef{ID: "secrets-engine", Type: "service"}, Action: "execute-ccr", Resource: api.ResourceRef{ID: "catalog:synthetic", Type: "secret-catalog-lane", System: "secrets-engine", Attributes: map[string]any{"stage": "prod"}}, Context: map[string]any{"human_control": true, "service_actor_binding": taskexecution.DeclaredServiceBinding(), "execution_contract": map[string]any{"contract_version": "SECRETS-EXEC-20261011-v1", "operation": "metadata-apply-ccr", "execution_actor": "service:secrets-engine", "operation_deadline": now.Add(30 * time.Second).Format(time.RFC3339Nano)}, "approval": map[string]any{"caller_claim": "unchanged and untrusted in B1"}}}
+}
+func pilotEngine(t *testing.T) *decision.Engine {
+ t.Helper()
+ p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ engine, err := decision.NewEngine(registry.NewStore(), p)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return engine
+}
+func pilotToken(kind string, now time.Time) string {
+ payload := map[string]any{"case": kind, "exp": now.Add(2 * time.Minute).Unix()}
+ if kind == "expired" {
+ payload["exp"] = now.Add(-time.Second).Unix()
+ }
+ if kind == "no-expiry" {
+ delete(payload, "exp")
+ }
+ raw, _ := json.Marshal(payload)
+ return "synthetic." + base64.RawURLEncoding.EncodeToString(raw) + ".not-a-real-token"
+}
+func TestServicePilotRealHTTPAndTokenReviewSignedDeny(t *testing.T) {
+ now := time.Now().UTC()
+ var reviews atomic.Int64
+ reviewer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ reviews.Add(1)
+ if r.URL.Path != "/apis/authentication.k8s.io/v1/tokenreviews" || r.Header.Get("Authorization") != "Bearer synthetic-reviewer" {
+ t.Error("unexpected reviewer route/auth")
+ w.WriteHeader(403)
+ return
+ }
+ var body struct {
+ Spec struct {
+ Token string `json:"token"`
+ Audiences []string `json:"audiences"`
+ } `json:"spec"`
+ }
+ if json.NewDecoder(r.Body).Decode(&body) != nil {
+ w.WriteHeader(400)
+ return
+ }
+ if !reflect.DeepEqual(body.Spec.Audiences, []string{"flex-auth", "access-engine"}) {
+ t.Error("unexpected bounded runtime audience request")
+ }
+ pieces := strings.Split(body.Spec.Token, ".")
+ raw, _ := base64.RawURLEncoding.DecodeString(pieces[1])
+ var claims map[string]any
+ json.Unmarshal(raw, &claims)
+ principal := taskexecution.ServicePilotPrincipal
+ audiences := []string{"flex-auth"}
+ authenticated := true
+ switch claims["case"] {
+ case "foreign":
+ principal = "system:serviceaccount:other:other"
+ case "alternate-audience":
+ audiences = []string{"access-engine"}
+ case "wrong-audience":
+ audiences = []string{"openbao"}
+ case "unauthenticated":
+ authenticated = false
+ }
+ json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"authenticated": authenticated, "audiences": audiences, "user": map[string]any{"username": principal}}})
+ }))
+ defer reviewer.Close()
+ folder := t.TempDir()
+ ca := filepath.Join(folder, "ca.crt")
+ reviewToken := filepath.Join(folder, "reviewer-token")
+ os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: reviewer.Certificate().Raw}), 0600)
+ os.WriteFile(reviewToken, []byte("synthetic-reviewer"), 0600)
+ engine := pilotEngine(t)
+ private := ed25519.NewKeyFromSeed(make([]byte, 32))
+ if engine.SetEnvelopeSigningKey(private, "synthetic-b1") != nil {
+ t.Fatal("signer config")
+ }
+ log := audit.NewJSONLDecisionLog(filepath.Join(folder, "decisions.jsonl"))
+ engine.SetDecisionLog(log)
+ bindings := map[string]string{"secrets-engine": taskexecution.ServicePilotPrincipal}
+ inputs, err := buildServicePilotInputs(engine, callerauth.ModeEnforce, "flex-auth", reviewer.URL, reviewToken, ca, bindings)
+ if err != nil {
+ t.Fatal(err)
+ }
+ server := httptest.NewServer(newServeMuxWithServicePilotInputs(engine, callerauth.Disabled(), inputs))
+ defer server.Close()
+ post := func(path string, request api.CheckRequest, token string) (int, []byte) {
+ raw, _ := json.Marshal(request)
+ req, _ := http.NewRequest("POST", server.URL+path, bytes.NewReader(raw))
+ if token != "" {
+ req.Header.Set("Authorization", "Bearer "+token)
+ }
+ response, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer response.Body.Close()
+ body, _ := io.ReadAll(response.Body)
+ return response.StatusCode, body
+ }
+ original := pilotRequest(now)
+ status, body := post("/v1/check", original, pilotToken("valid", now))
+ if status != 200 {
+ t.Fatalf("status=%d", status)
+ }
+ var envelope api.DecisionEnvelope
+ if json.Unmarshal(body, &envelope) != nil {
+ t.Fatal("decode")
+ }
+ if envelope.Effect != api.DecisionEffectDeny || envelope.Reason != "service_execution_integration_unadmitted" {
+ t.Fatal("B1 changed policy gate")
+ }
+ if sign.Verify(envelope, map[string]ed25519.PublicKey{"synthetic-b1": private.Public().(ed25519.PublicKey)}) != nil {
+ t.Fatal("actual response signature")
+ }
+ if envelope.Binding.SubmittedRequestDigest != api.RequestDigest(original) || !reflect.DeepEqual(envelope.Binding.Context, original.Context) {
+ t.Fatal("original submitted request/context changed")
+ }
+ if envelope.Provenance.Caller == nil || envelope.Provenance.Caller.Mode != "enforce" || envelope.Provenance.Caller.Principal != taskexecution.ServicePilotPrincipal || envelope.Provenance.Caller.Audience != "flex-auth" {
+ t.Fatal("caller provenance")
+ }
+ records, err := log.ReadAll()
+ if err != nil || len(records) != 1 || !reflect.DeepEqual(records[0], envelope) {
+ t.Fatal("recorded signed response differs")
+ }
+ if reviews.Load() != 1 {
+ t.Fatal("request was reviewed more than once")
+ }
+ for _, kind := range []string{"foreign", "alternate-audience", "wrong-audience", "expired", "no-expiry", "unauthenticated"} {
+ t.Run(kind, func(t *testing.T) {
+ status, _ := post("/v1/check", pilotRequest(now), pilotToken(kind, now))
+ if status == 200 {
+ t.Fatal("invalid source caller accepted")
+ }
+ })
+ }
+ for _, mutate := range []func(*api.CheckRequest){func(r *api.CheckRequest) { r.Subject.ID = "other" }, func(r *api.CheckRequest) { r.Subject.Type = "human" }, func(r *api.CheckRequest) { r.Tenant = "other" }, func(r *api.CheckRequest) { r.Context["service_actor_binding"].(map[string]any)["extra"] = true }, func(r *api.CheckRequest) { r.Context["verified"] = true }, func(r *api.CheckRequest) { r.Context["delegation"] = true }, func(r *api.CheckRequest) {
+ r.Context["execution_contract"].(map[string]any)["operation_deadline"] = now.Add(5 * time.Minute).Format(time.RFC3339Nano)
+ }} {
+ request := pilotRequest(now)
+ mutate(&request)
+ if status, _ := post("/v1/check", request, pilotToken("valid", now)); status == 200 {
+ t.Fatal("invalid public request accepted")
+ }
+ }
+ if status, _ := post("/v1/check", pilotRequest(now), ""); status == 200 {
+ t.Fatal("missing caller accepted")
+ }
+ if status, _ := post("/v1/batch_check", pilotRequest(now), pilotToken("valid", now)); status != 403 {
+ t.Fatal("batch bypass")
+ }
+ records, _ = log.ReadAll()
+ if len(records) != 1 {
+ t.Fatal("refused request entered decision record")
+ }
+
+ // Exercise the actual production serve flag/configuration path in a disposable
+ // subprocess of this compiled test binary. Its environment contains only the
+ // synthetic argument handoff; no ambient backend credentials are inherited.
+ seedFile := filepath.Join(folder, "synthetic-seed")
+ os.WriteFile(seedFile, []byte(base64.RawURLEncoding.EncodeToString(make([]byte, 32))), 0440)
+ listener, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatal(err)
+ }
+ address := listener.Addr().String()
+ listener.Close()
+ registryPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json"))
+ policyPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md"))
+ cliRecord := filepath.Join(folder, "cli-records.jsonl")
+ cliArgs := []string{"serve", "--addr", address, "--registry", registryPath, "--policy", policyPath, "--log", cliRecord, "--caller-auth-mode", "enforce", "--caller-audience", "flex-auth", "--caller-kubernetes-url", reviewer.URL, "--caller-reviewer-token-file", reviewToken, "--caller-ca-file", ca, "--caller-binding", "secrets-engine=" + taskexecution.ServicePilotPrincipal, "--envelope-signing-key-file", seedFile, "--envelope-signing-kid", "synthetic-b1"}
+ argsJSON, _ := json.Marshal(cliArgs)
+ child := exec.Command(os.Args[0], "-test.run=^TestServicePilotChildCLI$")
+ child.Env = []string{"ACCESS_B1_SERVER_ARGS=" + string(argsJSON)}
+ child.Stdout = io.Discard
+ child.Stderr = io.Discard
+ if child.Start() != nil {
+ t.Fatal("start disposable CLI")
+ }
+ defer func() { child.Process.Kill(); child.Wait() }()
+ ready := false
+ for attempt := 0; attempt < 100; attempt++ {
+ resp, err := http.Get("http://" + address + "/healthz")
+ if err == nil {
+ resp.Body.Close()
+ ready = true
+ break
+ }
+ time.Sleep(20 * time.Millisecond)
+ }
+ if !ready {
+ t.Fatal("disposable production serve path not ready")
+ }
+ raw, _ := json.Marshal(original)
+ cliRequest, _ := http.NewRequest("POST", "http://"+address+"/v1/check", bytes.NewReader(raw))
+ cliRequest.Header.Set("Authorization", "Bearer "+pilotToken("valid", now))
+ cliResponse, err := http.DefaultClient.Do(cliRequest)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer cliResponse.Body.Close()
+ var cliEnvelope api.DecisionEnvelope
+ if cliResponse.StatusCode != 200 || json.NewDecoder(cliResponse.Body).Decode(&cliEnvelope) != nil {
+ t.Fatal("production serve path refused supported source input")
+ }
+ if cliEnvelope.Effect != api.DecisionEffectDeny || cliEnvelope.Reason != "service_execution_integration_unadmitted" || sign.Verify(cliEnvelope, map[string]ed25519.PublicKey{"synthetic-b1": private.Public().(ed25519.PublicKey)}) != nil {
+ t.Fatal("production CLI did not emit signed DENY")
+ }
+ if cliEnvelope.Binding.SubmittedRequestDigest != api.RequestDigest(original) || !reflect.DeepEqual(cliEnvelope.Binding.Context, original.Context) {
+ t.Fatal("production CLI changed original request/context")
+ }
+ cliRecords, err := audit.NewJSONLDecisionLog(cliRecord).ReadAll()
+ if err != nil || len(cliRecords) != 1 || !reflect.DeepEqual(cliRecords[0], cliEnvelope) {
+ t.Fatal("production CLI response/record differ")
+ }
+ for _, mode := range []callerauth.Mode{callerauth.ModeWarn, callerauth.ModeDisabled} {
+ if _, err := buildServicePilotInputs(engine, mode, "flex-auth", reviewer.URL, reviewToken, ca, bindings); err == nil {
+ t.Fatal("non-enforcing task mode accepted")
+ }
+ }
+ if _, err := buildServicePilotInputs(engine, callerauth.ModeEnforce, "access-engine", reviewer.URL, reviewToken, ca, bindings); err == nil {
+ t.Fatal("task pin renamed audience accepted")
+ }
+ missing := httptest.NewRecorder()
+ req := httptest.NewRequest("POST", "/v1/check", bytes.NewReader(body))
+ newServeMuxWithCallerAuth(engine, callerauth.Disabled()).ServeHTTP(missing, req)
+ if missing.Code != 503 {
+ t.Fatal("unwired generic handler accepted task pin")
+ }
+}
+func TestServicePilotGenericEngineAndCLIBypassesRefuse(t *testing.T) {
+ engine := pilotEngine(t)
+ request := pilotRequest(time.Now())
+ if _, err := engine.Check(context.Background(), request); err == nil {
+ t.Fatal("generic Check bypass")
+ }
+ if _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{}); err == nil {
+ t.Fatal("generic Batch bypass")
+ }
+ if _, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{}); err == nil {
+ t.Fatal("generic List bypass")
+ }
+ if _, err := engine.CheckServicePilot(context.Background(), taskexecution.ServiceInput{}); err == nil {
+ t.Fatal("zero input trusted")
+ }
+ folder := t.TempDir()
+ path := filepath.Join(folder, "request.json")
+ raw, _ := json.Marshal(request)
+ os.WriteFile(path, raw, 0600)
+ var stdout, stderr bytes.Buffer
+ code := run([]string{"check", "--registry", filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json"), "--policy", filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md"), "--request", path}, &stdout, &stderr)
+ if code == 0 || stdout.Len() != 0 {
+ t.Fatal("generic CLI emitted task verdict")
+ }
+}
+
+// The earlier separately named positive fixture policy is not an exception:
+// generic task actions cannot bypass the typed path on any loaded package.
+func TestServicePilotSeparatelyNamedSyntheticAllowCannotBypass(t *testing.T) {
+ p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ p.Metadata.ID = "synthetic-legacy-stack-positive-only"
+ p.RegoModule = "package " + p.Metadata.Package + "\ndefault decision := {\"effect\":\"allow\",\"reason\":\"synthetic_only\"}\n"
+ request := pilotRequest(time.Now())
+ result, err := p.Evaluate(context.Background(), request)
+ if err != nil || result.Effect != api.DecisionEffectAllow {
+ t.Fatal("synthetic precondition did not allow")
+ }
+ engine, err := decision.NewEngine(registry.NewStore(), p)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := engine.Check(context.Background(), request); err == nil {
+ t.Fatal("separately named generic task allow escaped")
+ }
+ if _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{Action: "execute-ccr"}); err == nil {
+ t.Fatal("generic task batch escaped")
+ }
+ if _, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{Action: "execute-ccr"}); err == nil {
+ t.Fatal("generic task list escaped")
+ }
+ raw, _ := json.Marshal(request)
+ recorder := httptest.NewRecorder()
+ req := httptest.NewRequest("POST", "/v1/check", bytes.NewReader(raw))
+ newServeMux(engine).ServeHTTP(recorder, req)
+ if recorder.Code != 503 {
+ t.Fatal("separate synthetic generic HTTP task route did not refuse")
+ }
+}
+
+func TestServicePilotWithholdsUnsignedOrAccidentalAllowBeforeRecord(t *testing.T) {
+ now := time.Now().UTC()
+ adapter, err := taskexecution.NewServicePilotInputs(fixedTokenReviewer{identity: callerauth.Identity{Username: taskexecution.ServicePilotPrincipal, Audiences: []string{"flex-auth"}, NotAfter: now.Add(time.Minute)}})
+ if err != nil {
+ t.Fatal(err)
+ }
+ input, err := adapter.Prepare(context.Background(), "Bearer synthetic", pilotRequest(now), now)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, accidentalAllow := range []bool{false, true} {
+ t.Run(map[bool]string{false: "unsigned", true: "accidental-allow"}[accidentalAllow], func(t *testing.T) {
+ p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if accidentalAllow {
+ p.RegoModule = "package " + p.Metadata.Package + "\ndefault decision := {\"effect\":\"allow\",\"reason\":\"synthetic_only\"}\n"
+ }
+ engine, err := decision.NewEngine(registry.NewStore(), p)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if accidentalAllow {
+ engine.SetEnvelopeSigningKey(ed25519.NewKeyFromSeed(make([]byte, 32)), "synthetic")
+ }
+ log := audit.NewJSONLDecisionLog(filepath.Join(t.TempDir(), "records.jsonl"))
+ engine.SetDecisionLog(log)
+ if _, err := engine.CheckServicePilot(context.Background(), input); err == nil {
+ t.Fatal("B1 released unsupported result")
+ }
+ records, err := log.ReadAll()
+ if err != nil || len(records) != 0 {
+ t.Fatal("withheld result entered recorder")
+ }
+ })
+ }
+}
+
+func TestServicePilotChildCLI(t *testing.T) {
+ raw := os.Getenv("ACCESS_B1_SERVER_ARGS")
+ if raw == "" {
+ return
+ }
+ var args []string
+ if json.Unmarshal([]byte(raw), &args) != nil {
+ os.Exit(2)
+ }
+ os.Exit(run(args, os.Stdout, os.Stderr))
+}
diff --git a/internal/decision/engine.go b/internal/decision/engine.go
--- a/internal/decision/engine.go
+++ b/internal/decision/engine.go
@@ -20,6 +20,7 @@
"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/internal/taskexecution"
"github.com/netkingdom/access-engine/pkg/api"
)
@@ -153,9 +154,44 @@
return envelope, nil
}
+// IsServicePilot selects the existing exact source package, never a request flag.
+func (e *Engine) IsServicePilot() bool {
+ return e.policy.Metadata.ID == taskexecution.ServicePilotPackageID && e.policy.Metadata.Version == taskexecution.ServicePilotPackageVersion
+}
+
+// ErrServicePilotUnavailable withholds any result that is not this B1 slice's
+// signed DENY. Provider joins and executable eligibility remain unimplemented.
+var ErrServicePilotUnavailable = errors.New("service pilot source path unavailable")
+
+// CheckServicePilot records only a signed DENY after authenticating the exact
+// immutable service input. It does not validate Approval facts or grant an act.
+func (e *Engine) CheckServicePilot(ctx context.Context, input taskexecution.ServiceInput) (api.DecisionEnvelope, error) {
+ if !e.IsServicePilot() {
+ return api.DecisionEnvelope{}, ErrServicePilotUnavailable
+ }
+ request, err := input.RequestAt(e.now())
+ if err != nil {
+ return api.DecisionEnvelope{}, err
+ }
+ envelope, err := e.decide(callerauth.WithRecord(ctx, input.Caller()), request)
+ if err != nil {
+ return api.DecisionEnvelope{}, err
+ }
+ if envelope.Effect != api.DecisionEffectDeny || envelope.Signature == nil || envelope.Signature.Mode != api.EnvelopeSignatureSigned {
+ return api.DecisionEnvelope{}, ErrServicePilotUnavailable
+ }
+ if err := e.recordDecisions([]api.DecisionEnvelope{envelope}); err != nil {
+ return api.DecisionEnvelope{}, err
+ }
+ return envelope, nil
+}
+
// Check evaluates one subject/action/resource request. The decision is
// released only after its required record commits (GH-DEC-2026-024).
func (e *Engine) Check(ctx context.Context, request api.CheckRequest) (api.DecisionEnvelope, error) {
+ if e.IsServicePilot() || request.Action == "execute-ccr" {
+ return api.DecisionEnvelope{}, ErrServicePilotUnavailable
+ }
decision, err := e.decide(ctx, request)
if err != nil {
return api.DecisionEnvelope{}, err
@@ -169,6 +205,9 @@
// BatchCheck evaluates one subject/action/context tuple against resources in
// request order, and commits the whole batch in one durable write.
func (e *Engine) BatchCheck(ctx context.Context, request api.BatchCheckRequest) ([]api.DecisionEnvelope, error) {
+ if e.IsServicePilot() || request.Action == "execute-ccr" {
+ return nil, ErrServicePilotUnavailable
+ }
decisions := make([]api.DecisionEnvelope, 0, len(request.Resources))
for _, resource := range request.Resources {
decision, err := e.decide(ctx, api.CheckRequest{
@@ -193,6 +232,9 @@
// ListAllowed evaluates candidate resources and returns only allow decisions.
func (e *Engine) ListAllowed(ctx context.Context, request ListAllowedRequest) ([]api.DecisionEnvelope, error) {
+ if e.IsServicePilot() || request.Action == "execute-ccr" {
+ return nil, ErrServicePilotUnavailable
+ }
candidates := e.store.ResourceRefs(request.System, request.ResourceType)
decisions := make([]api.DecisionEnvelope, 0, len(candidates))
for _, resource := range candidates {
diff --git a/internal/taskexecution/service_input.go b/internal/taskexecution/service_input.go
--- a/internal/taskexecution/service_input.go
+++ b/internal/taskexecution/service_input.go
@@ -1,20 +1,27 @@
package taskexecution
import (
+ "bytes"
"context"
"encoding/json"
"errors"
"github.com/netkingdom/access-engine/internal/callerauth"
"github.com/netkingdom/access-engine/pkg/api"
+ "io"
"time"
)
var ErrServiceInput = errors.New("service task execution input refused")
+const ServicePilotPackageID = "secrets-engine.task-execution.service-pilot"
+const ServicePilotPackageVersion = "v1"
+
const ServicePilotMappingVersion = "proposed-service-task-caller/v1"
const ServicePilotPrincipal = "system:serviceaccount:secrets-engine:secrets-engine"
-// ServicePilotInputs is source preparation only. It installs the explicit
+// ServicePilotInputs installs the exact B1 service input mapping for signed DENY.
+// It does not validate Approval facts or establish executable eligibility.
+// It installs the explicit
// proposed fixed subject-to-transport mapping, never a request-selected mapping.
// No constructor parameter can select warn/disabled mode or another subject.
type ServicePilotInputs struct{ auth *callerauth.Authenticator }
@@ -46,6 +53,44 @@
return r, nil
}
func (s ServiceInput) Caller() callerauth.Record { return s.caller }
+
+// RequestAt rechecks transport and operation lifetime at the runtime use site.
+// Request() remains a detached source snapshot, never an execution capability.
+func (s ServiceInput) RequestAt(now time.Time) (api.CheckRequest, error) {
+ r, err := s.Request()
+ if err != nil || now.IsZero() || s.caller.Audience != "flex-auth" || !s.caller.NotAfter.After(now) {
+ return api.CheckRequest{}, ErrServiceInput
+ }
+ contract, ok := r.Context["execution_contract"].(map[string]any)
+ if !ok {
+ return api.CheckRequest{}, ErrServiceInput
+ }
+ value, ok := contract["operation_deadline"].(string)
+ if !ok {
+ return api.CheckRequest{}, ErrServiceInput
+ }
+ deadline, err := time.Parse(time.RFC3339Nano, value)
+ if err != nil || !deadline.After(now) || deadline.After(s.caller.NotAfter) {
+ return api.CheckRequest{}, ErrServiceInput
+ }
+ return r, nil
+}
+
+// DecodeServiceRequest reuses the owner strict JSON scanner for this bounded
+// task-only route. It never changes producer canonicalization or legacy input.
+func DecodeServiceRequest(reader io.Reader) (api.CheckRequest, error) {
+ raw, err := io.ReadAll(io.LimitReader(reader, 524289))
+ var request api.CheckRequest
+ if err != nil || len(raw) > 524288 || strictObject(raw, &request) != nil {
+ return request, ErrServiceInput
+ }
+ decoder := json.NewDecoder(bytes.NewReader(raw))
+ decoder.DisallowUnknownFields()
+ if decoder.Decode(&request) != nil {
+ return api.CheckRequest{}, ErrServiceInput
+ }
+ return request, nil
+}
// DeclaredServiceBinding is public immutable metadata to bind at approval issue.
// It is not authentication evidence; Prepare independently verifies its principal.
diff --git a/internal/taskexecution/service_input_test.go b/internal/taskexecution/service_input_test.go
--- a/internal/taskexecution/service_input_test.go
+++ b/internal/taskexecution/service_input_test.go
@@ -1,7 +1,9 @@
package taskexecution
import (
+ "bytes"
"context"
+ "encoding/json"
"github.com/netkingdom/access-engine/internal/callerauth"
"github.com/netkingdom/access-engine/pkg/api"
"testing"
@@ -79,3 +81,35 @@
t.Fatal("missing owner reviewer accepted")
}
}
+
+func TestServiceInputRuntimeRecheckAndStrictHTTPDecode(t *testing.T) {
+ now := time.Now().UTC()
+ r := serviceRequest(t, now)
+ reviewer := &serviceReviewer{identity: callerauth.Identity{Username: ServicePilotPrincipal, Audiences: []string{"flex-auth"}, NotAfter: now.Add(120 * time.Second)}}
+ adapter, _ := NewServicePilotInputs(reviewer)
+ input, err := adapter.Prepare(context.Background(), "Bearer synthetic", r, now)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := input.RequestAt(now); err != nil {
+ t.Fatal(err)
+ }
+ for _, when := range []time.Time{time.Time{}, now.Add(60 * time.Second), now.Add(120 * time.Second)} {
+ if _, err := input.RequestAt(when); err == nil {
+ t.Fatal("stale/invalid runtime input accepted")
+ }
+ }
+ raw, _ := json.Marshal(r)
+ decoded, err := DecodeServiceRequest(bytes.NewReader(raw))
+ if err != nil || api.RequestDigest(decoded) != api.RequestDigest(r) {
+ t.Fatal("valid decode changed canonical submitted input")
+ }
+ bad := [][]byte{append(append([]byte{}, raw...), []byte(" {}")...), []byte(`{"action":"execute-ccr","action":"apply"}`), []byte(`{"context"
+ "bytes"
+ "encoding/json":{"human_control":true,"human_control":false}}`), []byte(`{"unknown_task_flag":true}`), bytes.Repeat([]byte(" "), 524289)}
+ for _, body := range bad {
+ if _, err := DecodeServiceRequest(bytes.NewReader(body)); err == nil {
+ t.Fatal("malformed/oversized task request accepted")
+ }
+ }
+}
diff --git a/internal/taskexecution/identity.go b/internal/taskexecution/identity.go
--- a/internal/taskexecution/identity.go
+++ b/internal/taskexecution/identity.go
@@ -1,5 +1,6 @@
-// Package taskexecution contains proposed source-only identity input validation.
-// It is not registered with HTTP evaluation and grants no executable admission.
+// Package taskexecution contains task input validation, not policy evaluation.
+// The human component remains source-only. The B1 service HTTP path can emit
+// only signed DENY and grants no executable admission.
package taskexecution
import (
diff --git a/docs/service-task-execution-contract.md b/docs/service-task-execution-contract.md
--- a/docs/service-task-execution-contract.md
+++ b/docs/service-task-execution-contract.md
@@ -51,9 +51,32 @@
Maker tests use a synthetic TokenReviewer, not actual Kubernetes authentication.
Actual deployed TokenReview origin/CA/RBAC/audience/projection rotation and mapping
-admission must be proved before runtime use. The adapter is not wired into HTTP
-routes, decision engine evaluation or a native executor; source package remains
-hard-closed. A forged public service_actor_binding alone remains insufficient.
+admission must be proved before runtime use. The B1 source path now wires the actual service adapter into `/v1/check` only
+when the exact installed service-pilot v1 package is selected. Startup requires
+enforced retained `flex-auth` audience and the one exact Secrets mapping. The
+actual reviewer authenticates once; the detached ServiceInput is rechecked for
+caller/operation expiry at use. The single existing decision engine records only
+a signed DENY on this path; unsigned output or an accidental policy allow is
+withheld before recording. No native executor or Approval fact join is installed.
+A forged public service_actor_binding alone remains insufficient.
+
+Generic Check, BatchCheck, ListAllowed and CLI/HTTP evaluation cannot evaluate
+`execute-ccr` under any package; the dedicated typed path is required. The exact
+service pin also refuses generic calls and batch requests. This deliberately
+invalidates the older separately named synthetic positive stack fixture on the
+new source. Its positive proof remains historical at Access `ec21518e`, not proof
+for B1. Do not add a fixture exception; a positive successor must use reviewed
+B2/B3 provider joins. The literal service policy false gate is unchanged.
+
+B1 retains the exact original request context, carried claim, submitted digest
+and producer-owned canonicalization. Its bounded strict JSON decoder refuses
+duplicates, trailing/unknown typed fields and oversized input. The caller's
+Approval blob remains unverified and cannot enable anything. B2 requires the
+Approval owner's fact contract/vector review; B3 additionally requires exact
+server-owned native-act representation and the complete trusted join. Fresh
+provider observations stay out of band: no refreshed context or claim replacement
+may relax strict Secrets request correspondence. Access never invokes a
+credential-carrying Python subprocess, consumes approval or performs the effect.
## Exact operation and independent gates
@@ -129,7 +152,8 @@
admit grant/baseline/estate or perform an effect. Those are explicit implementation
requirements; a matching predicate and reviewed TokenReview input are insufficient.
The service adapter carries authenticated transport evidence outside the canonical
-request but is not connected to HTTP evaluation/provider validation. A caller's
+request. B1 connects it only to signed-DENY HTTP evaluation; provider validation and
+executable eligibility remain absent. A caller's
service_actor_binding remains a public declaration independently compared by the
adapter, never a substitute for runtime provenance or exact operation authority.