Reject advisory policy envelopes before lifecycle effects
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
parent
caab9a90ec
commit
1e0df2f4a3
13 changed files with 2854 additions and 0 deletions
122
docs/evidence/2026-10-10-lifecycle-consumer-pin-repair.md
Normal file
122
docs/evidence/2026-10-10-lifecycle-consumer-pin-repair.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# Lifecycle consumer pin repair — 2026-10-10
|
||||
|
||||
Initial preparation is retained below. The config-only repair was approved and
|
||||
verified live on 2026-10-11; the dated execution appendix distinguishes it from
|
||||
the separate, unpublished purpose-hardening image. No human disposition is claimed.
|
||||
Root agent observed native State Hub `POST /v1/decisions` returning HTTP 503
|
||||
`invalid_decision`, and read only fixed runtime policy fields: lifecycle package
|
||||
`informed-decision.lifecycle`, version `v1`, digest
|
||||
`sha256:cee6a79f74e81d0d389a2c2e2451a92be3434ea9dfcee4986e8c7e6cfdbb1827`.
|
||||
The provider owner pin is now version `v2`, digest
|
||||
`sha256:da01e045fc108adec6b489e5d51f7e57366793016c5a64776963780a6aae457c`.
|
||||
|
||||
Access Engine `pkg/api/canonical.go` defines and emits only envelope contract
|
||||
`flex-auth.decision-record.v1`. FLEX-WP-0035 adds advisory binding purpose
|
||||
`readback`; enforcement binding purpose stays absent. Lifecycle policy version
|
||||
v2 must not be mistaken for envelope contract version v2. Informed Decision's
|
||||
existing exact package/version/digest check correctly refuses a stale pin.
|
||||
Readiness does not prove that a consumer pin matches a provider release.
|
||||
|
||||
Root's sanitized native provider probe returned HTTP 200, envelope contract
|
||||
`flex-auth.decision-record.v1`, binding purpose absent, matched policy version
|
||||
v2, provenance lifecycle package v2/da01e045, and submitted digest match true.
|
||||
Consumer runtime remained v1/cee6a79. Native pod Ready=1 and image 7f5c6cb.
|
||||
Reported CPU requested headroom: 3460m/4000m (86% allocated); this repair retains
|
||||
5m request and Recreate, and does not add a concurrent pod.
|
||||
|
||||
## Exact proposed action
|
||||
|
||||
Stable proposal: `INFD-WP-0009-T02/config-repin-v1`.
|
||||
Update the lifecycle pin in `deploy/lifecycle/admission.candidate.json` and
|
||||
`deploy/lifecycle/service.candidate.json`. Replace immutable runtime ConfigMap
|
||||
`informed-decision-runtime-c81577741a09` with
|
||||
`informed-decision-runtime-6c221878d051`, and update the Deployment's
|
||||
`configuration` volume reference to the new name. Same namespace and service.
|
||||
The runtime JSON differs only in lifecycle version/digest.
|
||||
|
||||
Service image remains
|
||||
`forgejo.coulomb.social/coulomb/informed-decision@sha256:7f5c6cb29c9f70a026bfe30e72934274ddc69261676cf6d476f81dd32812d173`.
|
||||
The existing image needs no source parser patch to consume this owner policy.
|
||||
Local inspected service source HEAD is `bee5ad9af60ab21bcc88423307ce02cc2ab5631b`;
|
||||
local provider source HEAD is `f5fad7c2851708be41c82891430deffe5f4af0ac`.
|
||||
No schema migration, credentials, review-policy pin, audience or network
|
||||
change. Initial lifecycle repair does not implement expansion INFD-WP-0008.
|
||||
|
||||
## Verification
|
||||
|
||||
Real Access Engine HTTP component with synthetic caller custody, current owner
|
||||
lifecycle fixture and existing PolicyClient: three tests passed (one collaboration
|
||||
flow, two stale-pin regressions). Each stale version or digest produces exact
|
||||
503 `invalid_decision`, writes no memo/lifecycle event, then succeeds with the
|
||||
exact current owner pin. This proves restoring compatible policy pins suffices;
|
||||
no parser relaxation is needed. Staged ConfigMap exactly matches the established
|
||||
renderer output for the repinned admission input; retained customizations in
|
||||
the existing Deployment are preserved.
|
||||
|
||||
Before execution: retain exact live Deployment, old runtime ConfigMap, current
|
||||
image digest and database evidence counts; verify live runtime JSON differs from
|
||||
candidate only as reviewed. Run server-side dry-run and diff through the current
|
||||
owner GitOps release path and retain their results. Check railiance01 headroom:
|
||||
existing request is 5m CPU, unchanged; Recreate stops the single writer first.
|
||||
Expect a brief interruption. No concurrent writer is admitted.
|
||||
|
||||
After execution: verify exact same image and new ConfigMap reference, rollout
|
||||
completion and readyz; prove native State Hub registration/publication with
|
||||
stable write keys and retained provider observations/receipts. Ready alone is
|
||||
insufficient. Human acceptance remains a separate human act.
|
||||
|
||||
## Rollback and risk
|
||||
|
||||
Rollback: restore the old Deployment configuration volume reference and its
|
||||
retained immutable ConfigMap through the same GitOps path. Preserve the existing
|
||||
schema-5 database and any new evidence; do not restore an old database merely
|
||||
to revert policy pins. The old pin intentionally returns to fail-closed 503
|
||||
against the v2 provider; coordinate owner rollback only if the provider itself
|
||||
must be reverted. No provider rollback belongs to this proposal.
|
||||
|
||||
No cloud/resource expansion or paid inference; expected incremental cost EUR 0.
|
||||
Risk: brief service interruption and continued refusal if the runtime provider
|
||||
pin differs; verification stops closure until native receipts exist.
|
||||
|
||||
## Source hashes
|
||||
|
||||
See `2026-10-10-lifecycle-consumer-pin-repair.sources.json` for exact original and
|
||||
proposed release artifact SHA-256 values. Supervisor approval, approved digest,
|
||||
executed digest and verified outcome must be recorded by the root executor;
|
||||
this packet supplies preparation only.
|
||||
|
||||
|
||||
## Separate source hardening (not in config-only live proposal)
|
||||
|
||||
PolicyClient now explicitly refuses advisory or malformed binding purpose; only
|
||||
absence or the exact empty string is the canonical enforcement form. Current
|
||||
service image 7f5c6cb does not contain this patch. Unit coverage includes nine
|
||||
purpose variants; a real provider allow envelope with unchanged binding digest,
|
||||
package pins and TTL is injected with `purpose=readback` and must refuse the
|
||||
write, then the original enforcement response succeeds. These tests cover the
|
||||
missing consumer guard without accepting an unpublished envelope v2 contract.
|
||||
INFD-WP-0009-T04 retains separate image preparation/native release work. T02's
|
||||
same-image config repair remains unchanged.
|
||||
|
||||
Validation after source hardening: 73 policy/purpose, real-provider component
|
||||
and deployment-render tests passed; 21 lifecycle-service checks passed. Review
|
||||
controller component checks (53) require its other owner binary and were skipped;
|
||||
they are not counted as verified. No live hardening rollout is claimed.
|
||||
|
||||
## Config-only execution appendix — 2026-10-11
|
||||
|
||||
Founder approved exact revision `7db8485ffecdebfabd77a9daec085553c74bf0bf`.
|
||||
The existing manual Argo Application synced only the new runtime ConfigMap and
|
||||
Deployment, with prune disabled. Application Succeeded/Healthy; generation24
|
||||
observed24 Ready1 and `/readyz`200 were verified. Lifecycle consumer version
|
||||
v2/da01e045 and unchanged service image7f5c6cb were observed. Native State Hub
|
||||
registration/publication succeeded with stable write keys; review delivery later
|
||||
completed after correcting a separate caller packet-validation error.
|
||||
Custodian retains exact approval, rendered digest, executed JSON patch, outcomes
|
||||
and rollback in `docs/evidence/2026-10-10-security-consolidation/`. Original
|
||||
preparation hashes above remain historical evidence, not rewritten approval.
|
||||
|
||||
Purpose-hardening T04 preparation now has a built, scanned local image and
|
||||
synthetic installed/backup checks in `2026-10-11-purpose-hardening/README.md`.
|
||||
That image is neither published nor deployed; config-only approval does not
|
||||
cover it.
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
[
|
||||
{
|
||||
"repo": "informed-decision",
|
||||
"path": "deploy/lifecycle/admission.candidate.json",
|
||||
"original_sha256": "fc5d6fb8ab833f268427d05f01c9c40db4b7fdd30840a5bdd6660dc6277e3cba",
|
||||
"staged_sha256": "22f830736ee56f6ed9b62f47d243e4e24a1b68263b261e78e2f1cfcb85adbd50"
|
||||
},
|
||||
{
|
||||
"repo": "informed-decision",
|
||||
"path": "deploy/lifecycle/service.candidate.json",
|
||||
"original_sha256": "fcfd29e0ed438b8fc3dd2e719db5a914ec3d947775bf38cae8deeb5d2269e954",
|
||||
"staged_sha256": "7a676532836affd6d75c7f1c41a5f937d8c8c809f63e3264ccce612730d5ba7b"
|
||||
},
|
||||
{
|
||||
"repo": "informed-decision",
|
||||
"path": "tests/test_lifecycle_policy_component.py",
|
||||
"original_sha256": "5fc8e57f499ca0c2882d4fcbfc5a5f6af46315d34b3d03d9a3a6dd95e225487b",
|
||||
"staged_sha256": "7a08fece31ddb5fd80ccdb67c5fc2821e328d8751e33c55714fd398982f71039"
|
||||
},
|
||||
{
|
||||
"repo": "informed-decision",
|
||||
"path": "informed_decision/policy.py",
|
||||
"original_sha256": "a00b93b34caee5f190dea750c53b7bca819b993b8ef2f499ef9525d7b8c11842",
|
||||
"staged_sha256": "9d7c1be4cb6199d14ef0aaead5f85951f45b0ccbd7c800a7fde922a253566b62"
|
||||
},
|
||||
{
|
||||
"repo": "informed-decision",
|
||||
"path": "tests/test_policy_client.py",
|
||||
"original_sha256": "7e3285e7b0381612f40c13a2f2b8f92d6520a4415b44437bc7538cf87f5adbf7",
|
||||
"staged_sha256": "2d743fa7f624f667b714be488197cda0cc82056dee44b5457e28786f09447622"
|
||||
}
|
||||
]
|
||||
5
docs/evidence/2026-10-11-purpose-hardening/Containerfile
Normal file
5
docs/evidence/2026-10-11-purpose-hardening/Containerfile
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
FROM forgejo.coulomb.social/coulomb/informed-decision@sha256:7f5c6cb29c9f70a026bfe30e72934274ddc69261676cf6d476f81dd32812d173
|
||||
LABEL org.opencontainers.image.base.digest="sha256:7f5c6cb29c9f70a026bfe30e72934274ddc69261676cf6d476f81dd32812d173"
|
||||
LABEL social.coulomb.source.policy.sha256="9d7c1be4cb6199d14ef0aaead5f85951f45b0ccbd7c800a7fde922a253566b62"
|
||||
LABEL social.coulomb.source.base.revision="7db8485ffecdebfabd77a9daec085553c74bf0bf"
|
||||
COPY policy.py /opt/venv/lib/python3.12/site-packages/informed_decision/policy.py
|
||||
62
docs/evidence/2026-10-11-purpose-hardening/README.md
Normal file
62
docs/evidence/2026-10-11-purpose-hardening/README.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Advisory-purpose hardening image preparation — 2026-10-11
|
||||
|
||||
INFD-WP-0009-T04 remains in progress. This candidate is locally built and tested;
|
||||
it has no registry publication or deployment receipt. Proposal ID:
|
||||
`INFD-WP-0009-T04/purpose-overlay-v1`. No live action is authorized by this packet.
|
||||
|
||||
Base: `forgejo.coulomb.social/coulomb/informed-decision@sha256:7f5c6cb29c9f70a026bfe30e72934274ddc69261676cf6d476f81dd32812d173`.
|
||||
Base installed policy.py equals HEAD7db8485 policy SHA-256a00b93b34caee5f190dea750c53b7bca819b993b8ef2f499ef9525d7b8c11842.
|
||||
Overlay source SHA-2569d7c1be4cb6199d14ef0aaead5f85951f45b0ccbd7c800a7fde922a253566b62.
|
||||
Only policy.py is added as one layer. Dependency, schema/store, runtime user,
|
||||
entrypoint, env and ports remain exact. `provenance.json` binds source and build
|
||||
recipe; source patch remains uncommitted during preparation and no whole-tree
|
||||
commit provenance is claimed.
|
||||
|
||||
Local candidate `informed-decision:purpose-hardening-20261011` has index digest
|
||||
`sha256:b8b58944786015a5375304241c149d1dab885eb539d6d8945f56b01a3c2bc821`,
|
||||
platform manifest918bbdce7337b716edfeb99b9343037dd141bb870aa3f32d7e4394c410c65b7d,
|
||||
configf187897d7322f10e06d53c153a6fa7e6f7403b47a9b59cf6edd12285ec479310.
|
||||
These local digests are not registry receipts. Verify eventual published digest
|
||||
and installed source before authoring a final admitted Deployment pin.
|
||||
|
||||
Evidence: `checks.json` proves base layer/config equality and fresh scan DB
|
||||
(2026-10-10 updated18:59Z, downloaded23:32Z), zero HIGH/CRITICAL findings.
|
||||
`scan.json` retains package findings; scanner warns Alpine3.24 is not on its
|
||||
EOL list. Vulnerability scan is bounded and not a universal security claim.
|
||||
`container.json` retains 13 checks, including private single writer, uncertain
|
||||
submission preservation, restart, consistent backup and separate synthetic
|
||||
restore. All temporary containers/volumes were removed. `installed-purpose.py`
|
||||
passes 10 installed-client forms: absent and empty allow, eight other/advisory
|
||||
forms refuse. Native identity/PDP/approval/audit proof is not claimed.
|
||||
|
||||
Rebuild: make a fresh private temporary context, copy exact policy.py from owner
|
||||
source and this Containerfile to it, check both hashes against provenance.json,
|
||||
then `docker build --network none --pull=false -f <context>/Containerfile -t
|
||||
informed-decision:purpose-hardening-20261011 <context>`. Use only the immutable
|
||||
base and these files; normal repo .dockerignore excludes a direct module copy.
|
||||
No package download or broader dirty-source wheel enters this overlay.
|
||||
|
||||
## Concrete proposed release
|
||||
|
||||
After registry publication/verification, update only admission.candidate.json
|
||||
image and rendered service.candidate.json Deployment image, preserving current
|
||||
ConfigMap6c221878d051, schema5, review/lifecycle policy pins, requester custody,
|
||||
audience, network, PVC, 5m request, replicas1 and Recreate. Retain exact source
|
||||
revision/digest and server dry-run/diff through manual Argo before approval.
|
||||
Obtain separate supervisor approval of that concrete immutable image revision;
|
||||
the config-only7db8485 approval cannot be reused.
|
||||
|
||||
Before cutover retain metadata-only counts and unique consistent native snapshot
|
||||
in protected custody; verify restore with the candidate on a separate protected
|
||||
volume without printing record content or credentials. Synthetic restore here
|
||||
proves mechanism only. Same-schema rollback retains current DB/evidence and
|
||||
restores image7f5c6cb through owner Argo; no data downgrade or replay is intended.
|
||||
Risk: brief Recreate interruption, no concurrent writer; incremental EUR0 for
|
||||
existing infrastructure, no paid inference. This source correction refuses an
|
||||
advisory envelope before effects while preserving exact current enforcement.
|
||||
|
||||
After approved release verify exact image/config, health, current pins and native
|
||||
register/publication with stable keys; inject a native-provider-derived advisory
|
||||
response in an isolated installed-client probe and prove refusal before writes.
|
||||
Do not contaminate production PDP config or create human approvals to test this
|
||||
guard. Reconcile lifecycle/audit receipts and retain failed/uncertain outcomes.
|
||||
27
docs/evidence/2026-10-11-purpose-hardening/checks.json
Normal file
27
docs/evidence/2026-10-11-purpose-hardening/checks.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"candidate_image_id": "sha256:b8b58944786015a5375304241c149d1dab885eb539d6d8945f56b01a3c2bc821",
|
||||
"repo_digests": [
|
||||
"informed-decision@sha256:b8b58944786015a5375304241c149d1dab885eb539d6d8945f56b01a3c2bc821"
|
||||
],
|
||||
"base_layers_preserved": true,
|
||||
"additional_layers": 1,
|
||||
"runtime_config_preserved": {
|
||||
"User": true,
|
||||
"Entrypoint": true,
|
||||
"Cmd": true,
|
||||
"Env": true,
|
||||
"ExposedPorts": true,
|
||||
"WorkingDir": true
|
||||
},
|
||||
"scanner": "aquasec/trivy@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969",
|
||||
"db": {
|
||||
"Version": 2,
|
||||
"NextUpdate": "2026-10-11T18:59:50.889010572Z",
|
||||
"UpdatedAt": "2026-10-10T18:59:50.889010923Z",
|
||||
"DownloadedAt": "2026-10-10T23:32:32.380033131Z"
|
||||
},
|
||||
"high_critical": [],
|
||||
"scan_scope": "candidate OS/package vulnerabilities; not security attestation; scan warning Alpine3.24 not on EOL list",
|
||||
"published": false,
|
||||
"deployed": false
|
||||
}
|
||||
32
docs/evidence/2026-10-11-purpose-hardening/container.json
Normal file
32
docs/evidence/2026-10-11-purpose-hardening/container.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"status": "passed",
|
||||
"image_id": "sha256:b8b58944786015a5375304241c149d1dab885eb539d6d8945f56b01a3c2bc821",
|
||||
"repo_digests": [
|
||||
"informed-decision@sha256:b8b58944786015a5375304241c149d1dab885eb539d6d8945f56b01a3c2bc821"
|
||||
],
|
||||
"checks_passed": 13,
|
||||
"checks": [
|
||||
"installed container entrypoint serves with projected configuration",
|
||||
"non-root read-only runtime has no external network or published port",
|
||||
"private config/database modes enforced; package installer absent",
|
||||
"configured review refuses anonymous access and reports audit-unready status",
|
||||
"private listener refuses anonymous lifecycle access and exposes no browser routes",
|
||||
"public listener exposes no private workload route",
|
||||
"a second writer cannot acquire the active evidence volume",
|
||||
"synthetic uncertain intent and undelivered evidence remain explicit",
|
||||
"consistent backup created while serving; overwrite refused",
|
||||
"SIGTERM closes the serving writer without forced kill",
|
||||
"restart preserves immutable content and unresolved submission without retry",
|
||||
"separate restored volume preserves exact content and unresolved state",
|
||||
"incomplete owner configuration exits before serving"
|
||||
],
|
||||
"fixture_only": true,
|
||||
"external_network": "none",
|
||||
"content_snapshot_sha256": "6ff72dc6578d8a4c0803ec4330e6144ae18c3182e29ab5506b039cedefb468ea",
|
||||
"native_identity_policy_audit_proven": false,
|
||||
"published": false,
|
||||
"deployed": false,
|
||||
"factory_attempts": 0,
|
||||
"paid_model_calls": 0,
|
||||
"cleanup_complete": true
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import copy,json,time
|
||||
from datetime import datetime,timezone
|
||||
from informed_decision.policy import PolicyClient,CONTRACT,submitted_digest
|
||||
pin='sha256:'+'b'*64
|
||||
req={'id':'image-purpose-fixture','tenant':'tenant:platform','subject':{'id':'synthetic','type':'service','tenant':'tenant:platform'},'resource':{'id':'memo:fixture','type':'memo','system':'informed-decision','tenant':'tenant:platform'},'action':'register','context':{}}
|
||||
class Transport:
|
||||
def request(self,*args,**kwargs):
|
||||
now=time.time(); stamp=lambda t:datetime.fromtimestamp(t,timezone.utc).isoformat()
|
||||
b={**copy.deepcopy({k:req[k] for k in ('tenant','subject','action','resource','context')}),'request_digest':'sha256:'+'e'*64,'submitted_request_digest':submitted_digest(req)}
|
||||
if self.purpose!='ABSENT':b['purpose']=self.purpose
|
||||
d={'id':'synthetic-decision','contract_version':CONTRACT,'request_id':req['id'],'effect':'allow','matched_policy_version':'v1','subject':req['subject'],'resource':req['resource'],'binding':b,'lifetime':{'kind':'ttl','not_before':stamp(now-1),'expires_at':stamp(now+60)},'provenance':{'policy_package':'fixture','policy_version':'v1','policy_package_digest':pin,'registry_snapshot_digest':'sha256:'+'c'*64,'evaluator':'flex-auth/fixture','decision_time':stamp(now)}}
|
||||
return 200,d
|
||||
checks=[]
|
||||
for purpose in ('ABSENT','','readback','enforcement','unknown',None,False,0,[],{}):
|
||||
t=Transport();t.purpose=purpose
|
||||
c=PolicyClient('https://fixture.invalid',lambda:'synthetic-only',package='fixture',version='v1',package_digest=pin,transport=t)
|
||||
o=c.check(req)
|
||||
if purpose in ('ABSENT',''):assert o.outcome=='allow';o.require_current(time.time())
|
||||
else:assert o.outcome=='invalid_decision' and o.decision_json is None
|
||||
checks.append(str(purpose))
|
||||
print(json.dumps({'installed_image_purpose_variants':len(checks),'binding_forms_allowed':2,'advisory_or_malformed_refused':8,'synthetic_only':True,'native_provider':False}))
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"source_base_revision": "7db8485ffecdebfabd77a9daec085553c74bf0bf",
|
||||
"base_image": "forgejo.coulomb.social/coulomb/informed-decision@sha256:7f5c6cb29c9f70a026bfe30e72934274ddc69261676cf6d476f81dd32812d173",
|
||||
"base_policy_sha256": "a00b93b34caee5f190dea750c53b7bca819b993b8ef2f499ef9525d7b8c11842",
|
||||
"policy_sha256": "9d7c1be4cb6199d14ef0aaead5f85951f45b0ccbd7c800a7fde922a253566b62",
|
||||
"containerfile_sha256": "42277e419014a56c0d24399a3aa45b2b0b27a7c22e0d225110007fa71460d345",
|
||||
"scope": "single policy.py overlay; source correction uncommitted; not a claim whole worktree committed",
|
||||
"tag": "informed-decision:purpose-hardening-20261011"
|
||||
}
|
||||
2354
docs/evidence/2026-10-11-purpose-hardening/scan.json
Normal file
2354
docs/evidence/2026-10-11-purpose-hardening/scan.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -149,6 +149,12 @@ class PolicyClient:
|
|||
if not isinstance(data.get("id"), str) or not data["id"] or len(data["id"]) > 256:
|
||||
raise ValueError()
|
||||
binding, provenance = data["binding"], data["provenance"]
|
||||
# FLEX-DEC-2026-019: advisory readback decisions are never act
|
||||
# authorization. Canonical enforcement purpose is absent (Go
|
||||
# omitempty) or the exact empty string; malformed values fail closed.
|
||||
purpose = binding.get("purpose", "")
|
||||
if type(purpose) is not str or purpose != "":
|
||||
raise ValueError()
|
||||
if (binding["submitted_request_digest"] != submitted
|
||||
or not DIGEST.fullmatch(binding["request_digest"])
|
||||
or binding["tenant"] != request["tenant"] or binding["action"] != request["action"]
|
||||
|
|
|
|||
|
|
@ -54,3 +54,66 @@ def test_initial_audience_collaborates_across_revisions_with_real_pdp(workspace,
|
|||
reopened.retrieve(foreign, memo.id)
|
||||
assert calls and all(c['known'] and c['audiences'] == ['flex-auth'] for c in calls)
|
||||
assert (fixture / 'pin.json').read_bytes() == pins
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stale_field", ["version", "package_digest"])
|
||||
def test_stale_consumer_pin_fails_closed_until_exact_owner_repin(workspace, tmp_path, signing_key, stale_field):
|
||||
binary = os.environ.get('INFD_FLEX_AUTH_BINARY')
|
||||
source = os.environ.get('INFD_FLEX_AUTH_SOURCE')
|
||||
if not binary or not source:
|
||||
pytest.skip('set INFD_FLEX_AUTH_BINARY and INFD_FLEX_AUTH_SOURCE for candidate owner policy')
|
||||
fixture = Path(source) / 'examples/informed-decision-lifecycle'
|
||||
with flex_component(binary, tmp_path, signing_key, fixture=fixture) as (pdp, calls):
|
||||
repo, memo = workspace
|
||||
app = LifecycleService(repo, pdp)
|
||||
hub = Caller(Attribution('tenant:platform', Actor('system:serviceaccount:state-hub:state-hub', ActorKind.SERVICE)),
|
||||
time.time() + 600, dumps({'principal_type_source': 'authentication-derived',
|
||||
'tenant_source': 'registration-supplied', 'authentication_audience': 'informed-decision-lifecycle',
|
||||
'workload_uid': 'synthetic-verified-uid'}))
|
||||
current = getattr(pdp, stale_field)
|
||||
setattr(pdp, stale_field, 'v1' if stale_field == 'version' else
|
||||
'sha256:cee6a79f74e81d0d389a2c2e2451a92be3434ea9dfcee4986e8c7e6cfdbb1827')
|
||||
with pytest.raises(LifecycleError) as refused:
|
||||
app.register(hub, memo, key='repin-proof')
|
||||
assert refused.value.status == 503 and refused.value.code == 'invalid_decision'
|
||||
with repo.store._connection() as db:
|
||||
assert db.execute('SELECT count(*) FROM memos').fetchone()[0] == 0
|
||||
assert db.execute('SELECT count(*) FROM lifecycle_events').fetchone()[0] == 0
|
||||
setattr(pdp, stale_field, current)
|
||||
app.register(hub, memo, key='repin-proof')
|
||||
assert app.retrieve(hub, memo.id) == memo
|
||||
assert calls and all(c['known'] and c['audiences'] == ['flex-auth'] for c in calls)
|
||||
|
||||
|
||||
def test_real_enforcement_response_with_readback_purpose_cannot_authorize(workspace, tmp_path, signing_key):
|
||||
binary = os.environ.get('INFD_FLEX_AUTH_BINARY')
|
||||
source = os.environ.get('INFD_FLEX_AUTH_SOURCE')
|
||||
if not binary or not source:
|
||||
pytest.skip('set INFD_FLEX_AUTH_BINARY and INFD_FLEX_AUTH_SOURCE for candidate owner policy')
|
||||
fixture = Path(source) / 'examples/informed-decision-lifecycle'
|
||||
with flex_component(binary, tmp_path, signing_key, fixture=fixture) as (pdp, calls):
|
||||
repo, memo = workspace
|
||||
app = LifecycleService(repo, pdp)
|
||||
hub = Caller(Attribution('tenant:platform', Actor('system:serviceaccount:state-hub:state-hub', ActorKind.SERVICE)),
|
||||
time.time() + 600, dumps({'principal_type_source': 'authentication-derived',
|
||||
'tenant_source': 'registration-supplied', 'authentication_audience': 'informed-decision-lifecycle',
|
||||
'workload_uid': 'synthetic-verified-uid'}))
|
||||
original = pdp.transport.request
|
||||
def advisory_response(*args, **kwargs):
|
||||
status, data = original(*args, **kwargs)
|
||||
assert status == 200 and data['effect'] == 'allow'
|
||||
assert data['binding'].get('purpose', '') == ''
|
||||
# Keep the real request binding, digest, pins and TTL unchanged:
|
||||
# even an otherwise exact allow cannot cross the advisory boundary.
|
||||
data['binding']['purpose'] = 'readback'
|
||||
return status, data
|
||||
pdp.transport.request = advisory_response
|
||||
with pytest.raises(LifecycleError) as refused:
|
||||
app.register(hub, memo, key='advisory-proof')
|
||||
assert refused.value.status == 503 and refused.value.code == 'invalid_decision'
|
||||
with repo.store._connection() as db:
|
||||
assert db.execute('SELECT count(*) FROM memos').fetchone()[0] == 0
|
||||
assert db.execute('SELECT count(*) FROM lifecycle_events').fetchone()[0] == 0
|
||||
pdp.transport.request = original
|
||||
app.register(hub, memo, key='advisory-proof')
|
||||
assert app.retrieve(hub, memo.id) == memo
|
||||
|
|
|
|||
|
|
@ -113,3 +113,17 @@ def test_transport_and_missing_caller_have_typed_observations():
|
|||
client=policy(Broken());assert client.check(request()).outcome=="policy_unavailable"
|
||||
client.token_provider=lambda:None
|
||||
assert client.check(request()).outcome=="caller_unavailable"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("purpose", ["", "readback", "enforcement", "unknown", None, False, 0, [], {}])
|
||||
def test_binding_purpose_only_allows_exact_enforcement_form(purpose):
|
||||
transport = PolicyTransport()
|
||||
transport.change = lambda d: d["binding"].update(purpose=purpose)
|
||||
observed = policy(transport).check(request())
|
||||
if type(purpose) is str and purpose == "":
|
||||
observed.require_current(time.time())
|
||||
assert json.loads(observed.decision_json)["binding"]["purpose"] == ""
|
||||
else:
|
||||
assert observed.outcome == "invalid_decision" and observed.decision_json is None
|
||||
with pytest.raises(PolicyError):
|
||||
observed.require_current(time.time())
|
||||
|
|
|
|||
107
workplans/INFD-WP-0009-lifecycle-consumer-pin-repair.md
Normal file
107
workplans/INFD-WP-0009-lifecycle-consumer-pin-repair.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
---
|
||||
id: INFD-WP-0009
|
||||
type: workplan
|
||||
title: "Repair lifecycle consumer pin after Access Engine v2 policy release"
|
||||
domain: infotech
|
||||
repo: informed-decision
|
||||
status: active
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
created: "2026-10-10"
|
||||
updated: "2026-10-11"
|
||||
flavor: implementation
|
||||
depends_on: []
|
||||
related:
|
||||
- FLEX-WP-0035
|
||||
- INFD-WP-0007
|
||||
state_hub_workstream_id: "77eaeabf-0644-595f-b3d5-1168e6a294c4"
|
||||
---
|
||||
|
||||
## Diagnose and prepare exact config repair
|
||||
|
||||
```task
|
||||
id: INFD-WP-0009-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "5c30c308-8fe2-5956-b0c1-66c80bcb07e9"
|
||||
```
|
||||
|
||||
Native State Hub lifecycle submission returns HTTP 503 `invalid_decision`.
|
||||
The service remains Ready but its configured lifecycle package is v1/cee6a79;
|
||||
Access Engine lifecycle provider now runs v2/da01e045 after FLEX-WP-0035.
|
||||
The envelope contract remains `flex-auth.decision-record.v1`; this is a policy
|
||||
pin mismatch, not an envelope compatibility migration. Keep exact package,
|
||||
version and digest enforcement. Source preparation and regression evidence are
|
||||
in `docs/evidence/2026-10-10-lifecycle-consumer-pin-repair.md`.
|
||||
|
||||
## Release and verify the config-only repair
|
||||
|
||||
```task
|
||||
id: INFD-WP-0009-T02
|
||||
status: done
|
||||
priority: high
|
||||
needs_human: false
|
||||
state_hub_task_id: "1e6684d7-7676-5c70-b701-9dad124b4f07"
|
||||
```
|
||||
|
||||
Apply only the reviewed immutable runtime ConfigMap and Deployment reference
|
||||
through the existing owner GitOps path after exact scoped supervisor approval.
|
||||
Image, schema, credentials, audience and review policy remain pinned. Retain
|
||||
server dry-run/diff, readiness and native State Hub register/publish receipts;
|
||||
retry with stable write keys, do not infer success from readiness. Root agent
|
||||
owns live evidence and approval recording. This is a repair of the shipped
|
||||
initial lifecycle, separate from INFD-WP-0008 expansion.
|
||||
|
||||
**Verified 2026-10-11:** founder approved exact config-only revision
|
||||
`7db8485ffecdebfabd77a9daec085553c74bf0bf`. Manual Argo Application operation
|
||||
synced only `informed-decision-runtime-6c221878d051` and Deployment, prune false.
|
||||
Application Succeeded/Healthy; generation24 observed24 Ready1; `/readyz`200;
|
||||
consumer v2/da01e045 and unchanged image7f5c6cb verified. Native lifecycle
|
||||
registration and publication succeeded (events28/29). A later review request
|
||||
rejected packet validation; this separate caller error does not undo verified
|
||||
authoring. Custodian evidence records original approval, execution and rollback:
|
||||
`docs/evidence/2026-10-10-security-consolidation/infd-pin-repair-preflight.json`.
|
||||
|
||||
## Explicitly reject advisory purpose at consumer boundary
|
||||
|
||||
```task
|
||||
id: INFD-WP-0009-T03
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "ee798d7d-5e60-5247-9c9b-285a7f65cf11"
|
||||
```
|
||||
|
||||
PolicyClient now validates binding purpose: absent or exact empty string only;
|
||||
readback and malformed purposes are refused. Nine purpose variants and a real
|
||||
provider response contamination test preserve the binding, pin, expiry and
|
||||
obligations gates. This task covers source implementation and regression tests.
|
||||
The current runtime image lacks this patch. Service build/native release belongs
|
||||
to T04, separate from the config-only T02 repair.
|
||||
|
||||
|
||||
## Release the advisory-purpose source correction
|
||||
|
||||
```task
|
||||
id: INFD-WP-0009-T04
|
||||
status: progress
|
||||
priority: medium
|
||||
state_hub_task_id: "0e1bb868-7bc2-5d4d-953c-e0edaf660ad0"
|
||||
```
|
||||
|
||||
Build and scan a new service image including T03, retain provenance and backup
|
||||
compatibility evidence, prepare a separate scoped image proposal, and verify
|
||||
native binding/refusal after the owner's approved release. Do not fold an image
|
||||
change into the config-only T02 proposal. Current runtime 7f5c6cb remains exact.
|
||||
|
||||
**Prepared 2026-10-11:** narrow single-module image overlay on immutable running
|
||||
7f5c6cb; exact source policy SHA-256 9d7c1be4cb6199d14ef0aaead5f85951f45b0ccbd7c800a7fde922a253566b62.
|
||||
Local image/index b8b58944786015a5375304241c149d1dab885eb539d6d8945f56b01a3c2bc821
|
||||
is built and retained, unpublished and undeployed. Base layer prefix and runtime
|
||||
configuration are preserved; installed-image 10 purpose variants pass; 13
|
||||
container checks include restart and synthetic consistent backup/restore. A
|
||||
fresh Trivy database scan has zero HIGH/CRITICAL findings, with its Alpine EOL
|
||||
warning retained. See `docs/evidence/2026-10-11-purpose-hardening/README.md`.
|
||||
Remaining gates: source-bound registry publication and exact digest verification,
|
||||
fresh native snapshot/restore compatibility, owner GitOps server dry-run/diff,
|
||||
separate scoped supervisor approval and native post-release binding/refusal.
|
||||
No release approval or actual production-data restore is inferred from fixtures.
|
||||
Loading…
Add table
Add a link
Reference in a new issue