Package protected review runtime and prepare deployment admission
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
bb5b607bbd
commit
bda9381f07
20 changed files with 3534 additions and 7 deletions
5
.dockerignore
Normal file
5
.dockerignore
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
**
|
||||
!Containerfile
|
||||
!requirements.lock
|
||||
!dist/
|
||||
!dist/informed_decision-*.whl
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -3,3 +3,5 @@ __pycache__/
|
|||
.pytest_cache/
|
||||
*.egg-info/
|
||||
.venv/
|
||||
dist/
|
||||
build/
|
||||
|
|
|
|||
24
Containerfile
Normal file
24
Containerfile
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Base matches the approved Approval Engine Alpine family; scan each candidate.
|
||||
FROM python:3.12-alpine@sha256:b64631e04e4920160c50fbe8d8df828f7f35f06f425cb44aa09bca53e708a35a AS build
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PATH=/opt/venv/bin:$PATH
|
||||
RUN python -m venv /opt/venv
|
||||
COPY requirements.lock /build/requirements.lock
|
||||
RUN pip install --no-cache-dir --require-hashes --only-binary=:all: -r /build/requirements.lock
|
||||
COPY dist/informed_decision-*.whl /build/
|
||||
RUN pip install --no-cache-dir --no-deps --no-index /build/informed_decision-*.whl
|
||||
|
||||
FROM python:3.12-alpine@sha256:b64631e04e4920160c50fbe8d8df828f7f35f06f425cb44aa09bca53e708a35a
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PATH=/opt/venv/bin:$PATH
|
||||
RUN apk add --no-cache 'libuuid>=2.42.3-r1' \
|
||||
&& addgroup -S -g 10001 informed \
|
||||
&& adduser -S -u 10001 -G informed -H informed
|
||||
COPY --from=build /opt/venv /opt/venv
|
||||
# No package installer or build tooling belongs in the running service.
|
||||
RUN rm -rf /usr/local/bin/pip* /usr/local/lib/python3.12/site-packages/pip* \
|
||||
/usr/local/lib/python3.12/site-packages/setuptools* /usr/local/lib/python3.12/site-packages/pkg_resources \
|
||||
/opt/venv/bin/pip* /opt/venv/lib/python3.12/site-packages/pip* \
|
||||
/opt/venv/lib/python3.12/site-packages/setuptools* /opt/venv/lib/python3.12/site-packages/pkg_resources
|
||||
WORKDIR /app
|
||||
USER 10001:10001
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["informed-decision-container"]
|
||||
10
Makefile
10
Makefile
|
|
@ -1,4 +1,12 @@
|
|||
.PHONY: test check sync
|
||||
.PHONY: test check sync package image-build
|
||||
|
||||
IMAGE ?= informed-decision:local
|
||||
|
||||
package:
|
||||
uv build --wheel
|
||||
|
||||
image-build: package
|
||||
docker build -f Containerfile -t $(IMAGE) .
|
||||
|
||||
test:
|
||||
python3 -m pytest -q
|
||||
|
|
|
|||
16
SCOPE.md
16
SCOPE.md
|
|
@ -10,8 +10,9 @@
|
|||
durable evidence store and scheduled Audit Core delivery are implemented.
|
||||
The approval surface is not deployed.**
|
||||
|
||||
What exists and is tested (344 tests and 12 Chromium checks, including actual
|
||||
Flex Auth, Approval Engine and Audit Core with synthetic identity/custody):
|
||||
What exists and is tested (371 automated tests and 11 container checks; the
|
||||
previous browser milestone passed 12 Chromium checks). Component tests use
|
||||
actual Flex Auth, Approval Engine and Audit Core with synthetic identity/custody:
|
||||
|
||||
- layer and stance declarations — `layer.yaml`, `pep-stance.yaml`,
|
||||
`informed_decision/stance.py`, with published-equals-shipped asserted;
|
||||
|
|
@ -39,10 +40,15 @@ Flex Auth, Approval Engine and Audit Core with synthetic identity/custody):
|
|||
accept/return/discuss/decline and visible original/unresolved entry state;
|
||||
- `runtime.py` — explicit owner configuration, rotating credential-file readers,
|
||||
30-second audit draining, heartbeat/reconciliation and delivery readiness.
|
||||
- `Containerfile`, `container.py`, `requirements.lock` and deployment renderer —
|
||||
a locally built/scanned image, private projected-config handoff, one serving
|
||||
writer, backup/inspection commands and eight review-only Kubernetes objects.
|
||||
Network-isolated container restart/restore preserves unresolved submissions.
|
||||
|
||||
Remaining: native policy package/caller/assignment admission, registered human
|
||||
login and deployed binding, independent production audit custody, packaging,
|
||||
backup/restore and operator recovery admission. The legacy `evidence.Outbox` remains an
|
||||
login and deployed binding, independent production audit custody, image
|
||||
publication/cutover, platform backup/restore and operator recovery admission.
|
||||
The legacy `evidence.Outbox` remains an
|
||||
in-memory test double; the new `Store` supplies durable atomicity. Browser
|
||||
sessions are ephemeral, with no approval state.
|
||||
Without owner runtime configuration `/readyz` returns 503. With it, readiness
|
||||
|
|
@ -51,6 +57,8 @@ The origin's last observed deployment was an nginx placeholder on 2026-09-10.
|
|||
See [browser-authentication.md](docs/browser-authentication.md) and
|
||||
[durable-review-evidence.md](docs/durable-review-evidence.md) and
|
||||
[protected-browser-review.md](docs/protected-browser-review.md).
|
||||
The [deployment packet](deploy/README.md) records native wiring observations,
|
||||
the missing Approval Engine namespace and remaining owner admission.
|
||||
|
||||
`INFD-WP-0001-T08` remains open for the live end-to-end proof, which is gated on
|
||||
`APPROVAL-WP-0002-T01` and a deployed `approval-engine`.
|
||||
|
|
|
|||
184
deploy/README.md
Normal file
184
deploy/README.md
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
# Review service deployment candidate
|
||||
|
||||
`INFD-WP-0001-T08` owns this packet. It prepares the existing
|
||||
`decisions.coulomb.social` origin for the review service. **No image publication,
|
||||
production configuration, credential provisioning or cluster apply is performed
|
||||
by the build, renderer or tests.** The origin is still the Railiance Apps nginx
|
||||
placeholder. Native policy, custody, registration and service admission remain
|
||||
required before cutover.
|
||||
|
||||
## Artifact and local proof
|
||||
|
||||
```sh
|
||||
make image-build IMAGE=informed-decision:local
|
||||
python tools/smoke_container.py --image informed-decision:local --receipt /tmp/infd-container-proof.json
|
||||
```
|
||||
|
||||
`Containerfile` uses the digest-pinned Alpine base already used by Approval
|
||||
Engine, installs the hashed `requirements.lock`, then installs the built wheel
|
||||
without dependency resolution. The image contains no pip or build installer.
|
||||
Its user/group is 10001; runtime deployments must use a read-only root filesystem
|
||||
and drop capabilities. Rebuild the wheel before building the image. Refresh
|
||||
dependency pins deliberately with `uv pip compile pyproject.toml --python-version
|
||||
3.12 --generate-hashes --output-file requirements.lock`, then repeat checks and
|
||||
the image scan. A successful local build is not a published registry digest.
|
||||
|
||||
The dated evidence pins the exact wheel/image/runtime-file hashes and scanner.
|
||||
The local image passed 11 container checks and a HIGH/CRITICAL Trivy scan with
|
||||
zero findings. The scanner also emitted an Alpine lifecycle-list warning; the
|
||||
receipt retains it. This scan is evidence about the selected severities and
|
||||
database at that time, not a general security attestation.
|
||||
|
||||
The smoke harness creates and removes its own labeled containers/volumes. It
|
||||
uses `--network none`, publishes no port and mounts only synthetic configuration
|
||||
and credentials. It tests the installed entrypoint, private file ownership,
|
||||
anonymous refusal, unhealthy audit readiness, exclusion of a second writer,
|
||||
consistent backup, restart, and restore onto another volume. A synthetic
|
||||
unresolved submission and its undelivered evidence survive unchanged. No native
|
||||
issuer, PDP, Approval Engine or Audit Core is contacted. Actual component/browser
|
||||
contracts have separate receipts from the previous integration milestone.
|
||||
|
||||
The lifecycle check caught the initial PID-1 process requiring Docker's forced
|
||||
kill (exit 137). The container now handles SIGTERM through Waitress shutdown and
|
||||
the audit pump's stop path; the final image exits cleanly within the tested
|
||||
15-second window. Kubernetes allows 45 seconds. Forced termination still cannot
|
||||
justify retrying an unresolved approval entry.
|
||||
|
||||
## Private storage and projected configuration
|
||||
|
||||
The deployment uses a single replica with **Recreate**, a ReadWriteOnce PVC and
|
||||
an OS lock held for the life of the serving process. Do not add replicas, a
|
||||
rolling surge or another writer: sessions are process-local and this store is a
|
||||
local-filesystem design. The lock rejects accidental concurrent service starts;
|
||||
it is not an admission of network-filesystem locking semantics.
|
||||
|
||||
Kubernetes projected ConfigMaps are root-owned and symlinked. The container
|
||||
entrypoint reads at most 16 KiB from `/configuration/runtime.json`, requires
|
||||
`evidence_db=/data/private/review.sqlite`, and copies the configuration into
|
||||
owned ephemeral storage at `/run/informed-decision/private/runtime.json` (0600).
|
||||
It creates `/data/private` as the process user with mode 0700 and refuses unsafe
|
||||
existing ownership/modes. It does not silently chmod or take over existing data.
|
||||
The evidence database remains 0600. Credential callbacks read rotating projected
|
||||
files directly; bearer tokens are never copied into SQLite or the config snapshot.
|
||||
|
||||
The pod uses fsGroup 10001 with `OnRootMismatch`. Its storage driver must make
|
||||
the volume root writable by that group while preserving the private child's
|
||||
ownership/modes on later mounts. A restored volume whose root triggers a
|
||||
recursive permission rewrite needs owner repair before admission, not relaxed
|
||||
store checks. The local-path class exists on Railiance; its backup/retention and
|
||||
restore admission are still the platform owner's work.
|
||||
|
||||
The normal workstation entrypoint continues listening on 127.0.0.1. The container
|
||||
entrypoint explicitly selects 0.0.0.0; the public issuer/callback remain fixed,
|
||||
and Host/forwarded headers cannot alter them.
|
||||
|
||||
## Render the review packet
|
||||
|
||||
Copy `admission.example.json` outside Git and replace its intentionally invalid
|
||||
placeholders with the published image manifest digest, admitted PDP endpoint and
|
||||
package/version/digest, exact PDP pod label, observed/admitted issuer IPs and
|
||||
storage class. The renderer accepts no inline token or broad egress CIDR.
|
||||
|
||||
```sh
|
||||
python tools/render_deployment.py --input /operator/review-inputs.json --output /tmp/infd-candidate.json
|
||||
kubectl --kubeconfig /operator/railiance-kubeconfig apply --dry-run=server -f /tmp/infd-candidate.json
|
||||
```
|
||||
|
||||
Rendering and server dry-run do not admit the values supplied. The renderer
|
||||
produces eight review objects: service account, PVC, immutable configuration,
|
||||
Recreate Deployment, Service, the review pod's NetworkPolicy, and two exact
|
||||
counterparty ingress proposals. It creates no Secret, RBAC grant, Namespace,
|
||||
Ingress or policy assignment. The service retains the existing name/port and
|
||||
selects the review component. The Deployment keeps the existing immutable
|
||||
selector so the same origin can be replaced under a reviewed cutover.
|
||||
|
||||
The incoming Traefik peer is namespace AND pod label. Egress permits DNS,
|
||||
exact Approval/Audit/PDP peers, exact public KeyCape addresses on 443, and the
|
||||
observed Traefik websecure peer on 8443 for enforcement after Service DNAT.
|
||||
Railiance metadata on 2026-09-11 confirmed the Traefik label/8443 port and
|
||||
`kc.coulomb.social -> 92.205.62.239`. These observations do not prove the CNI
|
||||
path; test the actual issuer token/JWKS reachability after admission. The shared
|
||||
HTTPS router is an L3/L4 boundary, not a hostname authorization policy; the
|
||||
client itself pins the issuer and refuses redirects.
|
||||
|
||||
Approval Engine's existing caller policy permits Secrets Engine or namespaces
|
||||
carrying its client label. That does not admit this review pod. The packet adds
|
||||
an **owner-reviewed proposal** ANDing namespace, app label and component rather
|
||||
than labelling the whole namespace as an approved caller. Flex Auth receives a
|
||||
similarly exact peer proposal. Audit Core already owns its prepared Informed
|
||||
Decision ingress under `AUDIT-WP-0009-T11`; do not create a competing copy.
|
||||
|
||||
Seven of eight objects passed server dry-run in their actual namespaces. The
|
||||
Approval Engine peer failed because namespace `approval-engine` is absent.
|
||||
The identical policy shape passed with only its metadata namespace/name mapped
|
||||
to an existing namespace for schema validation. That does not close the missing
|
||||
owner deployment. Schema checks used fixture policy pins and an unpublished image
|
||||
reference; their rendered output is not an admitted production configuration.
|
||||
|
||||
## Serving health and acceptance readiness
|
||||
|
||||
Startup, Kubernetes readiness and liveness use `/healthz`. An audit dependency
|
||||
outage must not restart the writer or remove the service's refusal/recovery
|
||||
pages from the origin. `/readyz` separately reports recent audit delivery health,
|
||||
and the application refuses accept while it is unhealthy. Monitor `/readyz`
|
||||
and pending/blocked outbox counts explicitly; Kubernetes `Ready=True` alone is
|
||||
not deployment acceptance or native policy proof. A healthy process can still
|
||||
refuse every action when policy/custody has not been admitted.
|
||||
|
||||
## Native admission and cutover
|
||||
|
||||
Use the existing owners and records:
|
||||
|
||||
1. Flex Auth/Informed Decision admit the exact request package, assignments and
|
||||
caller `informed-decision=system:serviceaccount:informed-decision:review`.
|
||||
The projected token's audience is `flex-auth`; no browser token substitutes.
|
||||
The service account spelling is a candidate until this return is recorded.
|
||||
2. Platform/Audit Core complete `AUDIT-WP-0009-T11`: the candidate expects
|
||||
`informed-decision-audit` with key `token`, projected read-only. Its sender is
|
||||
exact `informed-decision`, tenant `tenant:platform`, write-only, load-bearing,
|
||||
redact. Run `warden route find/show` before any credential request. Rendering
|
||||
the Secret reference provisions nothing and confirms no custody name.
|
||||
3. Approval Engine admits its namespace/service and the exact review caller;
|
||||
KeyCape rolls out the already published browser registration. Railiance Apps
|
||||
admits image publication/cutover against its existing origin. Complete
|
||||
private-state backup/restore, monitoring and rollback ownership.
|
||||
4. Capture the current Deployment and Service configuration, close new review
|
||||
traffic for cutover, and retain any existing source-held evidence. Only then
|
||||
apply the admitted packet. Its Recreate rollout intentionally incurs downtime.
|
||||
5. Check both process health and application readiness, policy caller/subject
|
||||
allow/refusal cases, genuine human login and a declared human-control approval.
|
||||
Retrieve the independent audit receipt and the original presentation. Recheck
|
||||
the callback's TLS/redirect/cookie behavior and source custody after restart.
|
||||
|
||||
The image publication, workload/policy/custody admission and rollout remain
|
||||
distinct. No signed decision attribution, native approval or factory admission
|
||||
is inferred from this packet. `FLEX-WP-0024` and the commitment-only custody
|
||||
limitations remain as recorded in the consumer contract.
|
||||
|
||||
## Backup, restore and rollback
|
||||
|
||||
The installed `informed-decision-admin` supports local custody operations only:
|
||||
|
||||
```sh
|
||||
informed-decision-admin inspect --db /data/private/review.sqlite
|
||||
informed-decision-admin backup --db /data/private/review.sqlite --output /backup/private/review.sqlite
|
||||
```
|
||||
|
||||
The backup parent must already be owned/private 0700; the destination must not
|
||||
exist. Backup uses SQLite's consistent backup API while the service may be live.
|
||||
Do not copy a running SQLite file while ignoring WAL. Inspection prints counts
|
||||
and schema version, not memo content or credentials. These are privileged
|
||||
operator custody commands, not browser endpoints. An independent backup copy,
|
||||
retention, access controls and an actual platform restoration drill remain owed.
|
||||
|
||||
For restore, stop the serving writer, restore to a separately prepared private
|
||||
volume, verify the immutable content and unresolved submission state, then
|
||||
mount that volume as `/data`. A restart requires a new human login. Never reset
|
||||
an unresolved intent or POST again to manufacture a presentation correlation.
|
||||
Do not roll schema v2 back to an older reader against the same live database.
|
||||
|
||||
For a failed initial cutover, stop the new writer, retain the PVC and restore
|
||||
Railiance Apps' **Deployment and Service** from the prior origin manifest.
|
||||
Restoring only the Deployment is insufficient: the candidate Service selector
|
||||
also names the review component. Returning to the placeholder is a service
|
||||
rollback, not deletion or reconciliation of any approval/evidence already held.
|
||||
12
deploy/admission.example.json
Normal file
12
deploy/admission.example.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"image": "REQUIRED: forgejo.coulomb.social/coulomb/informed-decision@sha256:<published manifest digest>",
|
||||
"policy": {
|
||||
"origin": "REQUIRED: http://<admitted service>.flex-auth.svc.cluster.local:8080",
|
||||
"package": "REQUIRED: owner-admitted package",
|
||||
"version": "REQUIRED: owner-admitted version",
|
||||
"package_digest": "REQUIRED: sha256:<native package digest>",
|
||||
"pod_name": "REQUIRED: exact app.kubernetes.io/name label of admitted PDP"
|
||||
},
|
||||
"keycape_egress_ips": ["REQUIRED: observed and admitted public issuer IPv4/IPv6 address"],
|
||||
"storage_class": "REQUIRED: admitted local-filesystem ReadWriteOnce storage class"
|
||||
}
|
||||
143
docs/evidence/2026-09-11-container-candidate.json
Normal file
143
docs/evidence/2026-09-11-container-candidate.json
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
{
|
||||
"schema": "informed-decision.container-candidate.v1",
|
||||
"observed_at": "2026-09-10T23:15:35.252989+00:00",
|
||||
"base_commit": "83849b75d44cab3d01d7ea56f6c839aef8e20046",
|
||||
"contract_sources": {
|
||||
"approval-engine": "a0a602976eef818f36dde35f76f7f2e589bd051b",
|
||||
"audit-core": "5c0ad522fb36092aa7ec2e8d72f63a5a91853b5b",
|
||||
"flex-auth": "88b354377c8e26b162f1234e673072f1c06dcd89",
|
||||
"railiance-apps": "eff457ef52a29afc7b9beccf38480a9955c5233e"
|
||||
},
|
||||
"source_task": "INFD-WP-0001-T08",
|
||||
"status": "progress",
|
||||
"image": {
|
||||
"local_tag": "informed-decision:hfact-20260911",
|
||||
"local_image_id": "sha256:c882d6c10ce61b29eed06594a0eb01abe5b1adb136a0343101f991c2305701b4",
|
||||
"wheel_sha256": "00930d4fe7415c93f36ee82735abcbc69ca7cad694e2963b8f0ad8abdac6cb34",
|
||||
"runtime_files": {
|
||||
"container.py": "a8e7516ef6a7f110d8ed95493ae3b9492d913cc7a5650b88a519334ba544f9dd",
|
||||
"presentation.py": "54375ccd8358fac41de220bccadbee09f8b7c8ebbeeafaafd381a7456331da50",
|
||||
"disposition.py": "5ddd19c2444f8e6c9bb090d04045cf1227a34554e000f07b3f677325bf8089f8",
|
||||
"records.py": "82f696151895c51b168144a3078d05764da3499b96917c465629ce0836fa8e33",
|
||||
"audit.py": "06da36a8f3451b4abc7a46da2df66d5939524c170658b879499b2a47a1b10cd3",
|
||||
"oidc.py": "35b37bae67968c2179b22af8151afe2981d5468debf4488771eaf3121b75e221",
|
||||
"__main__.py": "3ba1467fa2d47fcf0bd032e8694904450775adf763a6e6f9e4888cc3ce3ed92b",
|
||||
"stance.py": "62b96b76eba4ec277344172f10f6233e979dda4d079f972d9a55c4e5aed96c3c",
|
||||
"policy.py": "7bf814a8332ff054bf5f1f651d8816a408138b1f197b17da0abb7f87411e8812",
|
||||
"review.py": "c2d2aa1cf48d10d85b3606acf2830ab22701c1274221e5e2760d29901adfd786",
|
||||
"http_transport.py": "fc9c1fd05cc89120d70bfcdb4c60fe244b81c8054d21505df8cfa9c5a229f7bc",
|
||||
"approval_client.py": "56c201dda358cb4bef11a15da141bb68562cdde9e37b4a880bf1e9257ae82cb6",
|
||||
"runtime.py": "6bc6a74657f526743cca6c6cdc0a86fff43d8282e0b867dc9288344636abe5eb",
|
||||
"provenance.py": "de02c6bf162865ba9619d74cfd68dd0a5d7f81e6c9e427f67f848cb2c2214f83",
|
||||
"memo.py": "697058863a4e6de2c143a2433eaaf5e15e5e7375dc5459af6911b146567e8b5d",
|
||||
"canonicalize.py": "517bbc6b81cc839636b97863ebdefd38aa375d10f4915feb9bf21867f992f765",
|
||||
"evidence.py": "5f84997f66409c7b6cec6ab57e73ad5bba2c1b2d669604adcbf36e72a824a88d",
|
||||
"__init__.py": "0ffebe048ba583e7b623f60e8f449950dc5fd49afce2398381fa143eee834e19",
|
||||
"approval_http.py": "203f462ff78f1409f2f924d4723197fdd502087c7f6da319f46ffffce2a9d478",
|
||||
"web.py": "97f7e040237fa3b755e39d13e628181dc2a3f6a31b4331ca7284bef05f8d6038",
|
||||
"store.py": "7a58b36d409cb4161b65985545e13573a5196779b4c6e95ca03ce45c6fada808",
|
||||
"ui.py": "69fb7fc1794c8842f42e7f0f5a261c4cf64176053ef0a532db46ee6566621219"
|
||||
},
|
||||
"installed_versions": {
|
||||
"informed-decision": "0.2.0",
|
||||
"PyJWT": "2.13.0",
|
||||
"cryptography": "50.0.1",
|
||||
"cffi": "2.1.1",
|
||||
"pycparser": "3.0",
|
||||
"waitress": "3.0.2"
|
||||
},
|
||||
"requirements_lock_sha256": "539072cb7def057c170c065363d365fe7fb7cf540bef04b2a4c9fab8ee05a7db",
|
||||
"all_22_runtime_files_match_source": true,
|
||||
"published": false
|
||||
},
|
||||
"verification": {
|
||||
"command": "INFD_APPROVAL_ENGINE_SOURCE=/home/worsch/approval-engine INFD_AUDIT_CORE_SOURCE=/home/worsch/audit-core INFD_FLEX_AUTH_BINARY=<compiled-flex-auth> make check",
|
||||
"tests_passed": 371,
|
||||
"tests_failed": 0,
|
||||
"tests_skipped": 0,
|
||||
"new_tests": 27,
|
||||
"schema_json_valid": true,
|
||||
"container_checks_passed": 11,
|
||||
"container_receipt": "2026-09-11-container-smoke.json",
|
||||
"container_network": "none",
|
||||
"fixture_cleanup_complete": true,
|
||||
"prior_browser_checks": {
|
||||
"owner_commit": "83849b75d44cab3d01d7ea56f6c839aef8e20046",
|
||||
"passed": 12,
|
||||
"rerun_in_this_increment": false
|
||||
}
|
||||
},
|
||||
"scan": {
|
||||
"scanner": "aquasec/trivy@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969",
|
||||
"created_at": "2026-09-10T23:13:55.674183643Z",
|
||||
"high": 0,
|
||||
"critical": 0,
|
||||
"exit_code": 0,
|
||||
"report": "2026-09-11-container-scan.json",
|
||||
"warning": "Alpine 3.24 is not on the scanner lifecycle/EOL list; no lifecycle acceptance is inferred."
|
||||
},
|
||||
"cluster_schema_check": {
|
||||
"apply_mode": "dry-run=server",
|
||||
"candidate_objects": 8,
|
||||
"exact_namespace_objects_passed": 7,
|
||||
"refusal": "approval-engine namespace absent; peer ingress cannot be created there yet",
|
||||
"representative_namespace_schema_check_passed": true,
|
||||
"representative_change": "metadata namespace/name only for the missing peer rule",
|
||||
"fixture_policy_pins": true,
|
||||
"unpublished_image_reference": true,
|
||||
"cluster_changes_applied": 0,
|
||||
"schema_only_image_reference": "unpublished pre-shutdown-fix local image 3d2b62f; image bytes are not evaluated by API schema validation"
|
||||
},
|
||||
"native_metadata_observations": {
|
||||
"traefik_namespace": "kube-system",
|
||||
"traefik_app_label": "traefik",
|
||||
"websecure_container_port": 8443,
|
||||
"keycape_public_addresses": [
|
||||
"92.205.62.239"
|
||||
],
|
||||
"storage_class": "local-path",
|
||||
"storage_provisioner": "rancher.io/local-path"
|
||||
},
|
||||
"source_sha256": {
|
||||
".dockerignore": "fa81d3c056779e8fd5fea58022e42cf9704b16e4d24d75896a2ef2f413f426e8",
|
||||
".gitignore": "bde5cbd5d9797b283d92cd9c69b12981a2539f4b7fb6d191ab63dd015c4a2cc3",
|
||||
"Containerfile": "a598563e9b2b4a80fd3c3ac580cf1e63ec44466a893d0068566036487aed20d2",
|
||||
"Makefile": "15ffb7f2621dacd6145609119994193cd0c7962f01b4e869d4a415200f55e9fa",
|
||||
"SCOPE.md": "d7e59d6906326846e4bc9ca8300215596fd316b7af9abce55dad67b229b1aa8d",
|
||||
"deploy/README.md": "5705c1c317834b71087692b2209bbe33b447c5f4ffcbc7b2434cc0b1f87a165b",
|
||||
"deploy/admission.example.json": "09de47289135c415927d524edf6c20f256a6f10fb467ceebccf08f221e64dfdb",
|
||||
"docs/evidence/2026-09-11-container-scan.json": "e926951ef4f085542116b8bb9c68c9717688957ad2913b0c94fd141c11f4dd03",
|
||||
"docs/evidence/2026-09-11-container-smoke.json": "6a71afa39cfd58b1502e300d8a9176905006b0d6bcd4f4989e200dcf371eeb38",
|
||||
"docs/protected-browser-review.md": "6c9b1323e0eb28553033a9d7c3e5e0f8c2c02350f13ea5e5da1395499baf81c9",
|
||||
"informed_decision/container.py": "a8e7516ef6a7f110d8ed95493ae3b9492d913cc7a5650b88a519334ba544f9dd",
|
||||
"informed_decision/web.py": "97f7e040237fa3b755e39d13e628181dc2a3f6a31b4331ca7284bef05f8d6038",
|
||||
"pyproject.toml": "5ce28585861413c0835699c5e14ac0ae80185fdf342dc1c765f1d9a0d1b6e211",
|
||||
"requirements.lock": "539072cb7def057c170c065363d365fe7fb7cf540bef04b2a4c9fab8ee05a7db",
|
||||
"tests/test_container_runtime.py": "475bf316bd9a1a014ccdbdb3267f40f42678d89849dc2e29d8345c27d1fc68e3",
|
||||
"tests/test_deployment_candidate.py": "296bc1090db5e33e025ccb53d780c0523ab6368278d8cf1778f7df91af13fe50",
|
||||
"tools/render_deployment.py": "51ea03afeb6c8986dfeb05bd76f4aa7fa8a22e8c6c1e522cc92dcaa6e348e377",
|
||||
"tools/smoke_container.py": "1385986cbef1d673e7ea3ed57c8d50b5ca34520a298eaf70e1a9af988b210644",
|
||||
"workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md": "4cc4a953a8e17adacc9d8fc19f1fd2cab441e8e8b47df07d533c2bf2732f25b4"
|
||||
},
|
||||
"remaining": [
|
||||
"native policy package/caller/assignment and exact peer admission",
|
||||
"AUDIT-WP-0009-T11 custody and protected receiver registration",
|
||||
"image publication and approved Railiance Apps cutover",
|
||||
"Approval Engine namespace/service admission and registered human binding",
|
||||
"native CNI/readiness/audit receipt proof and platform backup/restore operating admission",
|
||||
"existing T08 broader product acceptance"
|
||||
],
|
||||
"native_secret_reads": 0,
|
||||
"cluster_apply": false,
|
||||
"factory_attempts": 0,
|
||||
"paid_model_calls": 0,
|
||||
"lifecycle_finding": {
|
||||
"initial_local_image": "sha256:3d2b62f6ce8914b04c4d944d30996331b3c80028a3763da53055cb4b8d71a7c3",
|
||||
"initial_exit_code": 137,
|
||||
"corrected_exit_code": 0,
|
||||
"graceful_stop_window_seconds": 15,
|
||||
"correction": "explicit PID-1 SIGTERM handler through Waitress and audit-pump shutdown"
|
||||
},
|
||||
"integrated_parent_commit": "bb5b607bbd3ae518f3dc101dc6402753564b84ed",
|
||||
"concurrent_owner_change_preserved": "T07 premise correction; no runtime code changed"
|
||||
}
|
||||
2321
docs/evidence/2026-09-11-container-scan.json
Normal file
2321
docs/evidence/2026-09-11-container-scan.json
Normal file
File diff suppressed because it is too large
Load diff
30
docs/evidence/2026-09-11-container-smoke.json
Normal file
30
docs/evidence/2026-09-11-container-smoke.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"status": "passed",
|
||||
"image_id": "sha256:c882d6c10ce61b29eed06594a0eb01abe5b1adb136a0343101f991c2305701b4",
|
||||
"repo_digests": [
|
||||
"informed-decision@sha256:c882d6c10ce61b29eed06594a0eb01abe5b1adb136a0343101f991c2305701b4"
|
||||
],
|
||||
"checks_passed": 11,
|
||||
"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",
|
||||
"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": "08669bc5e86f292e516045483d9889b05881d0278add97c92a905a58d077d69a",
|
||||
"native_identity_policy_audit_proven": false,
|
||||
"published": false,
|
||||
"deployed": false,
|
||||
"factory_attempts": 0,
|
||||
"paid_model_calls": 0,
|
||||
"cleanup_complete": true
|
||||
}
|
||||
|
|
@ -108,7 +108,9 @@ needed, so projected rotation does not require storing a token in configuration
|
|||
or SQLite. Runtime loading provisions nothing. Fixed internal `.svc` origins
|
||||
are supported explicitly; public cleartext origins and redirects are refused.
|
||||
The server remains one Waitress process, loopback port 8080, four threads.
|
||||
Multi-replica sessions/storage and deployment packaging require separate work.
|
||||
Multi-replica sessions/storage require separate work. Single-writer container
|
||||
packaging and the review-only deployment packet are now supplied in
|
||||
[deploy/README.md](../deploy/README.md); publication and native rollout remain open.
|
||||
|
||||
The audit thread ticks every 30 seconds, generates the declared per-class
|
||||
heartbeats, drains at most ten records per tick and writes a private
|
||||
|
|
|
|||
114
informed_decision/container.py
Normal file
114
informed_decision/container.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
"""Container boundary: private runtime config, local evidence and one writer.
|
||||
|
||||
Kubernetes projected configuration is root-owned and symlinked. Copy its bounded
|
||||
non-secret JSON into an owned ephemeral file; do not weaken Runtime's private
|
||||
file checks or copy projected bearer credentials into persistent storage.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from contextlib import contextmanager
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import signal
|
||||
import stat
|
||||
import tempfile
|
||||
|
||||
from .store import Store
|
||||
|
||||
|
||||
def private_directory(path):
|
||||
path = Path(path)
|
||||
if not path.is_absolute():
|
||||
raise ValueError("absolute private directory required")
|
||||
path.mkdir(mode=0o700, exist_ok=True)
|
||||
info = path.lstat()
|
||||
if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid()
|
||||
or stat.S_IMODE(info.st_mode) != 0o700):
|
||||
raise ValueError("private directory has unsafe ownership or mode")
|
||||
return path
|
||||
|
||||
|
||||
def prepare_configuration(source, *, data_root=Path('/data'), run_root=Path('/run/informed-decision')):
|
||||
with Path(source).open('rb') as handle:
|
||||
if not stat.S_ISREG(os.fstat(handle.fileno()).st_mode):
|
||||
raise ValueError('configuration must be a regular projected file')
|
||||
raw = handle.read(16385)
|
||||
if len(raw) > 16384:
|
||||
raise ValueError('configuration too large')
|
||||
data = json.loads(raw)
|
||||
evidence = Path(data_root) / 'private'
|
||||
if not isinstance(data, dict) or data.get('evidence_db') != str(evidence / 'review.sqlite'):
|
||||
raise ValueError('container evidence must use its private persistent volume')
|
||||
private_directory(evidence)
|
||||
destination = private_directory(Path(run_root) / 'private') / 'runtime.json'
|
||||
fd, temporary = tempfile.mkstemp(prefix='.runtime-', dir=destination.parent)
|
||||
try:
|
||||
with os.fdopen(fd, 'wb') as handle:
|
||||
handle.write(raw); handle.flush(); os.fsync(handle.fileno())
|
||||
os.replace(temporary, destination)
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
return destination, evidence
|
||||
|
||||
|
||||
@contextmanager
|
||||
def single_writer(evidence):
|
||||
directory = private_directory(evidence)
|
||||
fd = os.open(directory / 'serve.lock', os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
|
||||
try:
|
||||
info = os.fstat(fd)
|
||||
if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid()
|
||||
or info.st_nlink != 1 or stat.S_IMODE(info.st_mode) != 0o600):
|
||||
raise ValueError('unsafe service lock')
|
||||
try:
|
||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
raise ValueError('another review service holds this evidence volume') from None
|
||||
yield
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def main():
|
||||
from .web import main as serve
|
||||
def terminate(signum, frame):
|
||||
# PID 1 does not get ordinary default signal behavior. Let Waitress
|
||||
# drain its dispatcher and web.main stop the audit pump on SIGTERM.
|
||||
raise SystemExit(0)
|
||||
signal.signal(signal.SIGTERM, terminate)
|
||||
try:
|
||||
config, evidence = prepare_configuration(os.environ.get('INFD_CONTAINER_CONFIG', '/configuration/runtime.json'))
|
||||
with single_writer(evidence):
|
||||
os.environ['INFD_REVIEW_CONFIG'] = str(config)
|
||||
os.environ['INFD_LISTEN_HOST'] = '0.0.0.0'
|
||||
serve()
|
||||
except (OSError, ValueError, sqlite3.Error):
|
||||
# Configuration can be supplied by an operator. Do not echo its values
|
||||
# or an upstream response on startup failure.
|
||||
raise SystemExit('Container runtime could not start; inspect configuration and private volume admission.') from None
|
||||
|
||||
|
||||
def admin():
|
||||
parser = argparse.ArgumentParser(description='Local custody operations; no network or approval mutation.')
|
||||
commands = parser.add_subparsers(dest='command', required=True)
|
||||
backup = commands.add_parser('backup', help='SQLite-consistent backup to a new private file')
|
||||
backup.add_argument('--db', type=Path, required=True)
|
||||
backup.add_argument('--output', type=Path, required=True)
|
||||
inspect = commands.add_parser('inspect', help='Report delivery/submission counts, never private content')
|
||||
inspect.add_argument('--db', type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
if not args.db.is_file():
|
||||
parser.error('existing evidence database required')
|
||||
store = Store(args.db)
|
||||
if args.command == 'backup':
|
||||
store.backup(args.output)
|
||||
print(json.dumps({'status': 'backed_up', 'consistent_snapshot': True}))
|
||||
else:
|
||||
with store._connection() as db:
|
||||
print(json.dumps({'schema_version': db.execute('PRAGMA user_version').fetchone()[0],
|
||||
'outbox': {r[0]: r[1] for r in db.execute('SELECT state,COUNT(*) FROM outbox GROUP BY state')},
|
||||
'submissions': {r[0]: r[1] for r in db.execute('SELECT state,COUNT(*) FROM submissions GROUP BY state')}}))
|
||||
|
|
@ -213,6 +213,9 @@ class App:
|
|||
|
||||
def main():
|
||||
from waitress import serve
|
||||
host = os.environ.get("INFD_LISTEN_HOST", "127.0.0.1")
|
||||
if host not in ("127.0.0.1", "0.0.0.0"):
|
||||
raise ValueError("unsupported listener address")
|
||||
# Waitress does not log request targets; a proxy must also omit callback
|
||||
# query strings and cookies. No debug traceback middleware belongs here.
|
||||
login = KeyCapeLogin(os.environ["INFD_KEYCAPE_ISSUER"])
|
||||
|
|
@ -224,7 +227,7 @@ def main():
|
|||
app = App(login, runtime.controller if runtime else None,
|
||||
readiness=runtime.pump.ready if runtime else lambda: False)
|
||||
try:
|
||||
serve(app, host="127.0.0.1", port=8080, threads=4)
|
||||
serve(app, host=host, port=8080, threads=4)
|
||||
finally:
|
||||
if runtime:
|
||||
runtime.pump.stop()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ dependencies = ["PyJWT[crypto]>=2.10,<3", "waitress>=3,<4"]
|
|||
|
||||
[project.scripts]
|
||||
informed-decision-web = "informed_decision.web:main"
|
||||
informed-decision-container = "informed_decision.container:main"
|
||||
informed-decision-admin = "informed_decision.container:admin"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest>=8", "PyYAML>=6,<7"]
|
||||
|
|
|
|||
164
requirements.lock
Normal file
164
requirements.lock
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile pyproject.toml --python-version 3.12 --generate-hashes --output-file requirements.lock
|
||||
cffi==2.1.1 \
|
||||
--hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \
|
||||
--hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \
|
||||
--hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \
|
||||
--hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \
|
||||
--hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \
|
||||
--hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \
|
||||
--hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \
|
||||
--hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \
|
||||
--hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \
|
||||
--hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \
|
||||
--hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \
|
||||
--hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \
|
||||
--hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \
|
||||
--hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \
|
||||
--hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \
|
||||
--hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \
|
||||
--hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \
|
||||
--hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \
|
||||
--hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \
|
||||
--hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \
|
||||
--hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \
|
||||
--hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \
|
||||
--hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \
|
||||
--hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \
|
||||
--hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \
|
||||
--hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \
|
||||
--hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \
|
||||
--hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \
|
||||
--hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \
|
||||
--hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \
|
||||
--hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \
|
||||
--hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \
|
||||
--hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \
|
||||
--hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \
|
||||
--hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \
|
||||
--hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \
|
||||
--hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \
|
||||
--hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \
|
||||
--hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \
|
||||
--hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \
|
||||
--hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \
|
||||
--hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \
|
||||
--hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \
|
||||
--hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \
|
||||
--hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \
|
||||
--hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \
|
||||
--hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \
|
||||
--hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \
|
||||
--hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \
|
||||
--hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \
|
||||
--hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \
|
||||
--hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \
|
||||
--hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \
|
||||
--hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \
|
||||
--hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \
|
||||
--hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \
|
||||
--hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \
|
||||
--hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \
|
||||
--hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \
|
||||
--hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \
|
||||
--hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \
|
||||
--hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \
|
||||
--hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \
|
||||
--hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \
|
||||
--hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \
|
||||
--hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \
|
||||
--hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \
|
||||
--hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \
|
||||
--hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \
|
||||
--hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \
|
||||
--hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \
|
||||
--hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \
|
||||
--hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \
|
||||
--hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \
|
||||
--hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \
|
||||
--hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \
|
||||
--hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \
|
||||
--hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \
|
||||
--hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \
|
||||
--hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \
|
||||
--hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \
|
||||
--hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \
|
||||
--hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \
|
||||
--hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \
|
||||
--hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \
|
||||
--hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \
|
||||
--hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \
|
||||
--hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \
|
||||
--hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \
|
||||
--hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \
|
||||
--hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \
|
||||
--hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \
|
||||
--hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \
|
||||
--hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \
|
||||
--hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \
|
||||
--hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \
|
||||
--hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \
|
||||
--hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \
|
||||
--hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \
|
||||
--hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264
|
||||
# via cryptography
|
||||
cryptography==50.0.1 \
|
||||
--hash=sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71 \
|
||||
--hash=sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23 \
|
||||
--hash=sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6 \
|
||||
--hash=sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e \
|
||||
--hash=sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361 \
|
||||
--hash=sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054 \
|
||||
--hash=sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f \
|
||||
--hash=sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6 \
|
||||
--hash=sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49 \
|
||||
--hash=sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5 \
|
||||
--hash=sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149 \
|
||||
--hash=sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88 \
|
||||
--hash=sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad \
|
||||
--hash=sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a \
|
||||
--hash=sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f \
|
||||
--hash=sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2 \
|
||||
--hash=sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20 \
|
||||
--hash=sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45 \
|
||||
--hash=sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f \
|
||||
--hash=sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b \
|
||||
--hash=sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527 \
|
||||
--hash=sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3 \
|
||||
--hash=sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6 \
|
||||
--hash=sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367 \
|
||||
--hash=sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0 \
|
||||
--hash=sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94 \
|
||||
--hash=sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239 \
|
||||
--hash=sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b \
|
||||
--hash=sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a \
|
||||
--hash=sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9 \
|
||||
--hash=sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5 \
|
||||
--hash=sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc \
|
||||
--hash=sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648 \
|
||||
--hash=sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986 \
|
||||
--hash=sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959 \
|
||||
--hash=sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0 \
|
||||
--hash=sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17 \
|
||||
--hash=sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e \
|
||||
--hash=sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733 \
|
||||
--hash=sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f \
|
||||
--hash=sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8 \
|
||||
--hash=sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf \
|
||||
--hash=sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671 \
|
||||
--hash=sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80 \
|
||||
--hash=sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558 \
|
||||
--hash=sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef
|
||||
# via pyjwt
|
||||
pycparser==3.0 \
|
||||
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
|
||||
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
|
||||
# via cffi
|
||||
pyjwt==2.13.0 \
|
||||
--hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \
|
||||
--hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728
|
||||
# via informed-decision (pyproject.toml)
|
||||
waitress==3.0.2 \
|
||||
--hash=sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f \
|
||||
--hash=sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e
|
||||
# via informed-decision (pyproject.toml)
|
||||
103
tests/test_container_runtime.py
Normal file
103
tests/test_container_runtime.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import stat
|
||||
|
||||
import pytest
|
||||
|
||||
from informed_decision.container import prepare_configuration, private_directory, single_writer
|
||||
from informed_decision.runtime import Runtime
|
||||
from informed_decision.store import Store
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def projected(tmp_path):
|
||||
data=tmp_path/'data';data.mkdir()
|
||||
run=tmp_path/'run';run.mkdir()
|
||||
config={'schema':'informed-decision.review-runtime.v1','evidence_db':str(data/'private/review.sqlite'),
|
||||
'approval_origin':'http://approval-engine.approval-engine.svc.cluster.local:8080',
|
||||
'policy':{'origin':'http://flex-auth-review.flex-auth.svc.cluster.local:8080',
|
||||
'package':'fixture','version':'v1','package_digest':'sha256:'+'a'*64,
|
||||
'caller_token_file':str(tmp_path/'not-provisioned-caller')},
|
||||
'audit':{'origin':'http://audit-core.audit-core.svc.cluster.local:8080',
|
||||
'sender_token_file':str(tmp_path/'not-provisioned-sender')}}
|
||||
payload=tmp_path/'projected';payload.write_text(json.dumps(config));payload.chmod(0o444)
|
||||
link=tmp_path/'runtime.json';link.symlink_to(payload)
|
||||
return link,data,run,config
|
||||
|
||||
|
||||
def test_projected_config_becomes_private_without_reading_or_copying_credentials(projected):
|
||||
source,data,run,expected=projected
|
||||
result,evidence=prepare_configuration(source,data_root=data,run_root=run)
|
||||
assert not result.is_symlink() and stat.S_IMODE(result.stat().st_mode)==0o600
|
||||
assert result.stat().st_uid==os.getuid() and json.loads(result.read_text())==expected
|
||||
assert stat.S_IMODE(evidence.stat().st_mode)==0o700
|
||||
runtime=Runtime.from_file(result)
|
||||
assert not runtime.pump.ready()
|
||||
assert not Path(expected['policy']['caller_token_file']).exists()
|
||||
assert not Path(expected['audit']['sender_token_file']).exists()
|
||||
assert sorted(p.name for p in result.parent.iterdir())==['runtime.json']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('fault',['oversize','outside_db','unsafe_directory','symlink_directory'])
|
||||
def test_unsafe_container_storage_and_config_refused(projected,tmp_path,fault):
|
||||
source,data,run,config=projected
|
||||
source.resolve().chmod(0o644)
|
||||
if fault=='oversize':source.write_text('x'*16385)
|
||||
if fault=='outside_db':
|
||||
config['evidence_db']=str(tmp_path/'outside.sqlite');source.write_text(json.dumps(config))
|
||||
if fault=='unsafe_directory':(data/'private').mkdir(mode=0o755)
|
||||
if fault=='symlink_directory':(data/'private').symlink_to(run)
|
||||
with pytest.raises((ValueError,FileExistsError)):
|
||||
prepare_configuration(source,data_root=data,run_root=run)
|
||||
assert not (data/'private/review.sqlite').exists()
|
||||
|
||||
|
||||
def test_second_service_cannot_open_the_same_evidence_volume(tmp_path):
|
||||
private=private_directory(tmp_path/'private')
|
||||
with single_writer(private):
|
||||
with pytest.raises(ValueError,match='another review service'):
|
||||
with single_writer(private):pass
|
||||
with single_writer(private):pass # Process/holder release permits restart.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('fault',['symlink','hardlink','mode'])
|
||||
def test_unsafe_service_lock_refused(tmp_path,fault):
|
||||
private=private_directory(tmp_path/'private');lock=private/'serve.lock'
|
||||
if fault=='symlink':lock.symlink_to(tmp_path/'outside')
|
||||
else:
|
||||
lock.touch(mode=0o600)
|
||||
if fault=='hardlink':os.link(lock,private/'other')
|
||||
else:lock.chmod(0o644)
|
||||
with pytest.raises((ValueError,OSError)):
|
||||
with single_writer(private):pass
|
||||
|
||||
|
||||
def test_reconfiguration_preserves_persisted_evidence(projected):
|
||||
source,data,run,config=projected
|
||||
result,_=prepare_configuration(source,data_root=data,run_root=run)
|
||||
first=Runtime.from_file(result);digest=first.controller.store.put_document(b'private fixture content')
|
||||
source.resolve().chmod(0o644)
|
||||
config['policy']['version']='v2';source.write_text(json.dumps(config))
|
||||
result,_=prepare_configuration(source,data_root=data,run_root=run)
|
||||
second=Runtime.from_file(result)
|
||||
assert second.controller.policy.version=='v2'
|
||||
with second.controller.store._connection() as db:
|
||||
assert db.execute('SELECT content FROM documents WHERE digest=?',(digest,)).fetchone()[0]==b'private fixture content'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('host',[None,'0.0.0.0','localhost'])
|
||||
def test_listener_exposure_requires_explicit_supported_setting(monkeypatch,host):
|
||||
import waitress
|
||||
from informed_decision import web
|
||||
monkeypatch.delenv('INFD_REVIEW_CONFIG',raising=False)
|
||||
monkeypatch.setenv('INFD_KEYCAPE_ISSUER','https://keycape.test')
|
||||
monkeypatch.delenv('INFD_LISTEN_HOST',raising=False)
|
||||
if host:monkeypatch.setenv('INFD_LISTEN_HOST',host)
|
||||
observed=[]
|
||||
monkeypatch.setattr(waitress,'serve',lambda app,**kwargs:observed.append(kwargs))
|
||||
if host=='localhost':
|
||||
with pytest.raises(ValueError):web.main()
|
||||
assert not observed
|
||||
else:
|
||||
web.main();assert observed[0]['host']==(host or '127.0.0.1')
|
||||
77
tests/test_deployment_candidate.py
Normal file
77
tests/test_deployment_candidate.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import copy
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
spec=importlib.util.spec_from_file_location('render_deployment',Path(__file__).parents[1]/'tools/render_deployment.py')
|
||||
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
|
||||
|
||||
|
||||
def inputs():
|
||||
return {'image':'forgejo.coulomb.social/coulomb/informed-decision@sha256:'+'a'*64,
|
||||
'policy':{'origin':'http://flex-auth-review.flex-auth.svc.cluster.local:8080','package':'review.example',
|
||||
'version':'v1','package_digest':'sha256:'+'b'*64,'pod_name':'flex-auth-review'},
|
||||
'keycape_egress_ips':['92.205.62.239'],'storage_class':'local-path'}
|
||||
|
||||
|
||||
def test_candidate_does_not_create_custody_and_uses_one_private_writer():
|
||||
result=module.render(inputs());objects=result['items']
|
||||
assert not any(o['kind'] in {'Secret','Role','RoleBinding','ClusterRoleBinding','Ingress','Namespace'} for o in objects)
|
||||
deploy=next(o for o in objects if o['kind']=='Deployment')['spec']
|
||||
assert deploy['replicas']==1 and deploy['strategy']=={'type':'Recreate'}
|
||||
pod=deploy['template']['spec'];container=pod['containers'][0]
|
||||
assert pod['automountServiceAccountToken'] is False and pod['serviceAccountName']=='review'
|
||||
assert pod['securityContext']['runAsUser']==pod['securityContext']['fsGroup']==10001
|
||||
assert container['securityContext']['readOnlyRootFilesystem'] is True
|
||||
assert container['securityContext']['capabilities']['drop']==['ALL']
|
||||
caller=next(v for v in pod['volumes'] if v['name']=='caller')
|
||||
assert caller['projected']['sources']==[{'serviceAccountToken':{'path':'token','audience':'flex-auth','expirationSeconds':3600}}]
|
||||
# Healthy serving remains available for refusal/recovery; application itself
|
||||
# gates accept on audit /readyz, without a dependency-driven restart cycle.
|
||||
assert container['readinessProbe']['httpGet']['path']=='/healthz'
|
||||
assert container['livenessProbe']['httpGet']['path']=='/healthz'
|
||||
|
||||
|
||||
def test_policies_and_service_selectors_do_not_include_placeholder_or_whole_namespaces():
|
||||
objects=module.render(inputs())['items']
|
||||
service=next(o for o in objects if o['kind']=='Service')
|
||||
assert service['spec']['selector']['app.kubernetes.io/component']=='review'
|
||||
policy=next(o for o in objects if o['kind']=='NetworkPolicy')['spec']
|
||||
for rule in policy['ingress']+policy['egress']:
|
||||
for peer in rule.get('from',rule.get('to',[])):
|
||||
if 'namespaceSelector' in peer:assert 'podSelector' in peer
|
||||
assert policy['egress'][-1]['to']==[{'ipBlock':{'cidr':'92.205.62.239/32'}}]
|
||||
assert policy['egress'][-1]['ports']==[{'protocol':'TCP','port':443}]
|
||||
assert policy['egress'][-2]['to'][0]['podSelector']['matchLabels']=={'app.kubernetes.io/name':'traefik'}
|
||||
assert policy['egress'][-2]['ports']==[{'protocol':'TCP','port':8443}]
|
||||
peers=[o for o in objects if o['kind']=='NetworkPolicy' and o['metadata']['namespace']!='informed-decision']
|
||||
assert {o['metadata']['namespace'] for o in peers}=={'approval-engine','flex-auth'}
|
||||
for obj in peers:
|
||||
caller=obj['spec']['ingress'][0]['from'][0]
|
||||
assert caller['namespaceSelector']['matchLabels']=={'kubernetes.io/metadata.name':'informed-decision'}
|
||||
assert caller['podSelector']['matchLabels']['app.kubernetes.io/component']=='review'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('fault',['tag','other_image','missing_pin','external_pdp','wildcard_pod','unknown_field',
|
||||
'broad_egress','loopback','empty_ips','empty_storage','empty_package'])
|
||||
def test_incomplete_or_broad_deployment_inputs_refuse(fault):
|
||||
data=inputs()
|
||||
if fault=='tag':data['image']='forgejo.coulomb.social/coulomb/informed-decision:latest'
|
||||
if fault=='other_image':data['image']=data['image'].replace('informed-decision','other')
|
||||
if fault=='missing_pin':del data['policy']['package_digest']
|
||||
if fault=='external_pdp':data['policy']['origin']='https://elsewhere.test'
|
||||
if fault=='wildcard_pod':data['policy']['pod_name']='*'
|
||||
if fault=='unknown_field':data['token']='not-permitted-inline'
|
||||
if fault=='broad_egress':data['keycape_egress_ips']=['0.0.0.0/0']
|
||||
if fault=='loopback':data['keycape_egress_ips']=['127.0.0.1']
|
||||
if fault=='empty_ips':data['keycape_egress_ips']=[]
|
||||
if fault=='empty_storage':data['storage_class']=''
|
||||
if fault=='empty_package':data['policy']['package']=''
|
||||
with pytest.raises(ValueError):module.render(data)
|
||||
|
||||
|
||||
def test_policy_revision_changes_immutable_configuration_name():
|
||||
first=inputs();second=copy.deepcopy(first);second['policy']['version']='v2'
|
||||
names=lambda data:[o['metadata']['name'] for o in module.render(data)['items'] if o['kind']=='ConfigMap']
|
||||
assert names(first)!=names(second)
|
||||
131
tools/render_deployment.py
Normal file
131
tools/render_deployment.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
"""Render a review-only Kubernetes candidate. Never apply or provision custody."""
|
||||
|
||||
import argparse
|
||||
import ipaddress
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import hashlib
|
||||
|
||||
NAME = 'informed-decision'
|
||||
SHA = re.compile(r'sha256:[0-9a-f]{64}')
|
||||
LABEL = {'app.kubernetes.io/name': NAME}
|
||||
|
||||
|
||||
def render(inputs):
|
||||
if not isinstance(inputs,dict) or set(inputs)!={'image','policy','keycape_egress_ips','storage_class'}:
|
||||
raise ValueError('exact image, policy, KeyCape egress IPs and storage class inputs required')
|
||||
if not re.fullmatch(r'forgejo\.coulomb\.social/coulomb/informed-decision@sha256:[0-9a-f]{64}',inputs['image']):
|
||||
raise ValueError('immutable Informed Decision registry digest required')
|
||||
policy=inputs['policy']
|
||||
if not isinstance(policy,dict) or set(policy)!={'origin','package','version','package_digest','pod_name'}:
|
||||
raise ValueError('exact policy endpoint, package pins and pod selector required')
|
||||
if not re.fullmatch(r'http://[a-z0-9-]+\.flex-auth\.svc\.cluster\.local:8080',policy['origin']):
|
||||
raise ValueError('exact in-cluster Flex Auth endpoint required')
|
||||
if not re.fullmatch(r'[a-z0-9][a-z0-9.-]{0,62}',policy['pod_name']):
|
||||
raise ValueError('exact Flex Auth pod label required')
|
||||
if not SHA.fullmatch(policy['package_digest']) or any(
|
||||
not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]{0,127}',policy[key]) for key in ['package','version']):
|
||||
raise ValueError('exact policy package/version/digest required')
|
||||
if not re.fullmatch(r'[a-z0-9][a-z0-9.-]{0,62}',inputs['storage_class']):
|
||||
raise ValueError('explicit storage class required')
|
||||
ips=inputs['keycape_egress_ips']
|
||||
if not isinstance(ips,list) or not 1<=len(ips)<=4:
|
||||
raise ValueError('one to four exact admitted KeyCape egress addresses required')
|
||||
cidrs=[]
|
||||
for ip in ips:
|
||||
address=ipaddress.ip_address(ip)
|
||||
if not address.is_global:
|
||||
raise ValueError('KeyCape public origin requires exact global addresses')
|
||||
cidrs.append(str(address)+'/'+str(address.max_prefixlen))
|
||||
config={'schema':'informed-decision.review-runtime.v1','evidence_db':'/data/private/review.sqlite',
|
||||
'approval_origin':'http://approval-engine.approval-engine.svc.cluster.local:8080',
|
||||
'policy':{key:policy[key] for key in ['origin','package','version','package_digest']},
|
||||
'audit':{'origin':'http://audit-core.audit-core.svc.cluster.local:8080',
|
||||
'sender_token_file':'/var/run/secrets/informed-decision/audit/token'}}
|
||||
config['policy']['caller_token_file']='/var/run/secrets/informed-decision/policy/token'
|
||||
raw=json.dumps(config,sort_keys=True,separators=(',',':'))
|
||||
config_name=NAME+'-runtime-'+hashlib.sha256(raw.encode()).hexdigest()[:12]
|
||||
def obj(api,kind,name,**values):
|
||||
return {'apiVersion':api,'kind':kind,'metadata':{'name':name,'namespace':NAME,
|
||||
'annotations':{'informed-decision.coulomb.social/admission':'review-only; INFD-WP-0001-T08'}},**values}
|
||||
container_security={'allowPrivilegeEscalation':False,'readOnlyRootFilesystem':True,'capabilities':{'drop':['ALL']}}
|
||||
mount=lambda name,path:{'name':name,'mountPath':path}
|
||||
readonly=lambda name,path:{**mount(name,path),'readOnly':True}
|
||||
probe=lambda path:{'httpGet':{'path':path,'port':'http'},'periodSeconds':10,'timeoutSeconds':2}
|
||||
pod={'automountServiceAccountToken':False,'serviceAccountName':'review','terminationGracePeriodSeconds':45,
|
||||
'securityContext':{'runAsNonRoot':True,'runAsUser':10001,'runAsGroup':10001,'fsGroup':10001,
|
||||
'fsGroupChangePolicy':'OnRootMismatch','seccompProfile':{'type':'RuntimeDefault'}},
|
||||
'containers':[{'name':NAME,'image':inputs['image'],'securityContext':container_security,
|
||||
'env':[{'name':'INFD_KEYCAPE_ISSUER','value':'https://kc.coulomb.social'},
|
||||
{'name':'INFD_CONTAINER_CONFIG','value':'/configuration/runtime.json'}],
|
||||
'ports':[{'name':'http','containerPort':8080}],
|
||||
'startupProbe':{**probe('/healthz'),'failureThreshold':12},
|
||||
# Keep read/recovery pages reachable during an audit outage. The
|
||||
# application gates acceptance on /readyz internally; monitoring
|
||||
# must scrape it separately. An outage must not restart the writer.
|
||||
'readinessProbe':probe('/healthz'),'livenessProbe':probe('/healthz'),
|
||||
'resources':{'requests':{'cpu':'50m','memory':'64Mi'},'limits':{'cpu':'500m','memory':'256Mi'}},
|
||||
'volumeMounts':[mount('data','/data'),mount('runtime','/run/informed-decision'),mount('tmp','/tmp'),
|
||||
readonly('configuration','/configuration'),readonly('caller','/var/run/secrets/informed-decision/policy'),
|
||||
readonly('audit','/var/run/secrets/informed-decision/audit')]}],
|
||||
'volumes':[{'name':'data','persistentVolumeClaim':{'claimName':NAME+'-data'}},
|
||||
{'name':'runtime','emptyDir':{'medium':'Memory','sizeLimit':'16Mi'}},
|
||||
{'name':'tmp','emptyDir':{'medium':'Memory','sizeLimit':'16Mi'}},
|
||||
{'name':'configuration','configMap':{'name':config_name,'defaultMode':0o444}},
|
||||
{'name':'caller','projected':{'defaultMode':0o440,'sources':[{'serviceAccountToken':{
|
||||
'path':'token','audience':'flex-auth','expirationSeconds':3600}}]}},
|
||||
{'name':'audit','secret':{'secretName':NAME+'-audit','defaultMode':0o440,
|
||||
'items':[{'key':'token','path':'token'}]}}]}
|
||||
peer=lambda namespace,name:{'namespaceSelector':{'matchLabels':{'kubernetes.io/metadata.name':namespace}},
|
||||
'podSelector':{'matchLabels':{'app.kubernetes.io/name':name}}}
|
||||
http=[{'protocol':'TCP','port':8080}]
|
||||
objects=[
|
||||
obj('v1','ServiceAccount','review',automountServiceAccountToken=False),
|
||||
obj('v1','PersistentVolumeClaim',NAME+'-data',spec={'accessModes':['ReadWriteOnce'],
|
||||
'storageClassName':inputs['storage_class'],'resources':{'requests':{'storage':'1Gi'}}}),
|
||||
obj('v1','ConfigMap',config_name,immutable=True,data={'runtime.json':raw}),
|
||||
obj('apps/v1','Deployment',NAME,spec={'replicas':1,'strategy':{'type':'Recreate'},'progressDeadlineSeconds':600,
|
||||
'selector':{'matchLabels':LABEL},'template':{'metadata':{'labels':{**LABEL,'app.kubernetes.io/component':'review'}},'spec':pod}}),
|
||||
obj('v1','Service',NAME,spec={'type':'ClusterIP','selector':{**LABEL,'app.kubernetes.io/component':'review'},
|
||||
'ports':[{'name':'http','port':80,'targetPort':'http','protocol':'TCP'}]}),
|
||||
obj('networking.k8s.io/v1','NetworkPolicy',NAME+'-review',spec={'podSelector':{'matchLabels':{**LABEL,
|
||||
'app.kubernetes.io/component':'review'}},'policyTypes':['Ingress','Egress'],
|
||||
'ingress':[{'from':[peer('kube-system','traefik')],'ports':http}],
|
||||
'egress':[{'to':[{'namespaceSelector':{'matchLabels':{'kubernetes.io/metadata.name':'kube-system'}},
|
||||
'podSelector':{'matchLabels':{'k8s-app':'kube-dns'}}}],
|
||||
'ports':[{'protocol':'UDP','port':53},{'protocol':'TCP','port':53}]},
|
||||
{'to':[peer('approval-engine','approval-engine'),peer('audit-core','audit-core'),
|
||||
peer('flex-auth',policy['pod_name'])],'ports':http},
|
||||
# Traefik's observed websecure target is 8443. Include
|
||||
# its exact peer for CNI enforcement after Service DNAT.
|
||||
{'to':[peer('kube-system','traefik')],'ports':[{'protocol':'TCP','port':8443}]},
|
||||
{'to':[{'ipBlock':{'cidr':cidr}} for cidr in cidrs],'ports':[{'protocol':'TCP','port':443}]}]})]
|
||||
# Counterparty proposals travel in the review packet. Neither the existing
|
||||
# Approval Engine namespace gate nor other Flex callers admits this pod.
|
||||
# Audit Core already owns its prepared exact informed-decision ingress.
|
||||
for namespace,name,task in [('approval-engine','approval-engine','APPROVAL-WP-0002-T01'),
|
||||
('flex-auth',policy['pod_name'],'INFD-WP-0001-T08 policy-owner return')]:
|
||||
item=obj('networking.k8s.io/v1','NetworkPolicy',NAME+'-review-ingress',spec={
|
||||
'podSelector':{'matchLabels':{'app.kubernetes.io/name':name}},'policyTypes':['Ingress'],
|
||||
'ingress':[{'from':[{'namespaceSelector':{'matchLabels':{'kubernetes.io/metadata.name':NAME}},
|
||||
'podSelector':{'matchLabels':{**LABEL,'app.kubernetes.io/component':'review'}}}],'ports':http}]})
|
||||
item['metadata']['namespace']=namespace
|
||||
item['metadata']['annotations']['informed-decision.coulomb.social/owner-review']=task
|
||||
objects.append(item)
|
||||
return {'apiVersion':'v1','kind':'List','items':objects}
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--input',type=Path,required=True)
|
||||
parser.add_argument('--output',type=Path)
|
||||
args=parser.parse_args()
|
||||
try: result=render(json.loads(args.input.read_text()))
|
||||
except (ValueError,TypeError,KeyError,OSError) as exc:parser.error(str(exc))
|
||||
raw=json.dumps(result,indent=2)+'\n'
|
||||
if args.output:args.output.write_text(raw)
|
||||
else:print(raw,end='')
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
150
tools/smoke_container.py
Normal file
150
tools/smoke_container.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
"""Exercise a local image, private volumes and restore without external network.
|
||||
|
||||
Only synthetic records/tokens are created. No native issuer, policy, audit or
|
||||
Approval Engine is contacted. Every container and volume is created by this
|
||||
process, labeled, and removed in finally; the image is retained for inspection.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--image',required=True)
|
||||
parser.add_argument('--receipt',type=Path,required=True)
|
||||
args=parser.parse_args()
|
||||
prefix='infd-proof-'+uuid.uuid4().hex[:12]
|
||||
volumes=[];containers=[];checks=[]
|
||||
def docker(*words,check=True):
|
||||
return subprocess.run(['docker',*words],capture_output=True,text=True,check=check,timeout=60)
|
||||
def command(name,code):
|
||||
return docker('exec',name,'python','-c',code).stdout.strip()
|
||||
def admin(name,*args):
|
||||
return json.loads(docker('exec',name,'informed-decision-admin',*args).stdout)
|
||||
def stop(name):
|
||||
docker('stop','--time','15',name)
|
||||
state=json.loads(docker('inspect',name).stdout)[0]['State']
|
||||
assert state['ExitCode']==0, 'service did not stop gracefully: '+str(state['ExitCode'])
|
||||
def wait_health(name):
|
||||
for _ in range(40):
|
||||
result=docker('exec',name,'python','-c',
|
||||
"import urllib.request; assert urllib.request.urlopen('http://127.0.0.1:8080/healthz',timeout=1).status==200",check=False)
|
||||
if result.returncode==0:return
|
||||
state=json.loads(docker('inspect',name).stdout)[0]['State']
|
||||
if not state['Running']:raise AssertionError('container exited before health; '+docker('logs',name).stdout)
|
||||
time.sleep(.2)
|
||||
raise AssertionError('container did not become healthy')
|
||||
def volume(suffix):
|
||||
name=prefix+'-'+suffix
|
||||
docker('volume','create','--label','informed-decision.fixture='+prefix,name);volumes.append(name)
|
||||
docker('run','--rm','--network','none','--read-only','--user','0:0','--cap-drop','ALL','--cap-add','CHOWN',
|
||||
'--mount','type=volume,src='+name+',dst=/data','--entrypoint','python',args.image,'-c',
|
||||
"import os; os.chown('/data',0,10001); os.chmod('/data',0o2770)")
|
||||
return name
|
||||
def start(suffix,data,fixture,backup=None):
|
||||
name=prefix+'-'+suffix;containers.append(name)
|
||||
words=['run','-d','--name',name,'--label','informed-decision.fixture='+prefix,
|
||||
'--network','none','--read-only','--cap-drop','ALL','--security-opt','no-new-privileges',
|
||||
'--tmpfs','/run/informed-decision:rw,nosuid,nodev,noexec,uid=10001,gid=10001,mode=0700',
|
||||
'--tmpfs','/tmp:rw,nosuid,nodev,noexec,mode=1777',
|
||||
'--mount','type=volume,src='+data+',dst=/data',
|
||||
'--mount','type=bind,src='+str(fixture)+',dst=/configuration,readonly',
|
||||
'--env','INFD_KEYCAPE_ISSUER=https://kc.coulomb.social']
|
||||
if backup:words+=['--mount','type=volume,src='+backup+',dst=/backup']
|
||||
docker(*words,args.image);return name
|
||||
snapshot="""import hashlib,json
|
||||
from informed_decision.store import Store
|
||||
s=Store('/data/private/review.sqlite')
|
||||
with s._connection() as db:
|
||||
rows={t:[list(r) for r in db.execute('SELECT * FROM '+t+' ORDER BY rowid')] for t in ['memos','presentations','dispositions','submissions','evidence','documents']}
|
||||
rows['documents']=[[r[0],r[1],hashlib.sha256(r[2]).hexdigest()] for r in rows['documents']]
|
||||
print(hashlib.sha256(json.dumps(rows,sort_keys=True).encode()).hexdigest())
|
||||
"""
|
||||
seed="""from informed_decision.store import Store
|
||||
from informed_decision.memo import Memo,BindingSlice,Principal,Scope,BindingLevel,StepKind,PacketItem
|
||||
from informed_decision.provenance import Claim,Route
|
||||
from informed_decision.disposition import Actor,ActorKind,Verb
|
||||
s=Store('/data/private/review.sqlite');digest=s.put_document(b'Synthetic retained content; no real approval.')
|
||||
m=Memo(id='container-fixture',version=1,question='Synthetic custody exercise?',requested_act='deliver',binding_level=BindingLevel.ORGANIZATIONAL,brief='Fixture only',binding=BindingSlice(Principal('fixture-human','person','Fixture'),Scope('resource','fixture','Fixture')),step_kind=StepKind.APPROVE,packet=(PacketItem('doc','Fixture',digest),),approval_id='fixture',approval_binding_digest='sha256:'+'1'*64)
|
||||
s.save_memo(m)
|
||||
p=s.present(m.id,principal_sub='fixture-human',tenant=Claim('tenant:platform',Route.REGISTRATION),principal_type=Claim('human',Route.AUTHENTICATION))
|
||||
d=s.record_disposition(p.id,Actor('fixture-human',ActorKind.PERSON),Verb.ACCEPT,operation_id='container-fixture-op')
|
||||
attempt=s.begin_submission(d.id);s.finish_submission(d.id,attempt)
|
||||
assert s.submission(d.id)['state']=='unresolved'
|
||||
"""
|
||||
try:
|
||||
with tempfile.TemporaryDirectory(prefix=prefix+'-') as temp:
|
||||
fixture=Path(temp);fixture.chmod(0o755)
|
||||
(fixture/'audit-token').write_text('synthetic-audit-token');(fixture/'caller-token').write_text('synthetic-caller-token')
|
||||
config={'schema':'informed-decision.review-runtime.v1','evidence_db':'/data/private/review.sqlite',
|
||||
'approval_origin':'http://127.0.0.1:18082','policy':{'origin':'http://127.0.0.1:18083',
|
||||
'package':'container.fixture','version':'v1','package_digest':'sha256:'+'a'*64,
|
||||
'caller_token_file':'/configuration/caller-token'},
|
||||
'audit':{'origin':'http://127.0.0.1:18084','sender_token_file':'/configuration/audit-token'}}
|
||||
(fixture/'runtime.json').write_text(json.dumps(config))
|
||||
for file in fixture.iterdir():file.chmod(0o444)
|
||||
data=volume('data');backup=volume('backup')
|
||||
first=start('first',data,fixture,backup);wait_health(first)
|
||||
checks.append('installed container entrypoint serves with projected configuration')
|
||||
info=json.loads(docker('inspect',first).stdout)[0]
|
||||
assert info['Config']['User']=='10001:10001' and info['HostConfig']['ReadonlyRootfs']
|
||||
assert info['HostConfig']['NetworkMode']=='none' and not info['NetworkSettings']['Ports'].get('8080/tcp')
|
||||
checks.append('non-root read-only runtime has no external network or published port')
|
||||
command(first,"import os,stat,importlib.util; assert os.getuid()==10001; assert importlib.util.find_spec('pip') is None; assert stat.S_IMODE(os.stat('/data/private').st_mode)==0o700; assert stat.S_IMODE(os.stat('/data/private/review.sqlite').st_mode)==0o600; assert stat.S_IMODE(os.stat('/run/informed-decision/private/runtime.json').st_mode)==0o600")
|
||||
checks.append('private config/database modes enforced; package installer absent')
|
||||
command(first,"import urllib.request,urllib.error\nfor path,status in [('/readyz',503),('/review?memo_id=missing',401)]:\n try: urllib.request.urlopen('http://127.0.0.1:8080'+path)\n except urllib.error.HTTPError as e: assert e.code==status\n else: raise AssertionError(path)")
|
||||
checks.append('configured review refuses anonymous access and reports audit-unready status')
|
||||
command(first,"from informed_decision.container import single_writer\ntry:\n with single_writer('/data/private'): pass\nexcept ValueError: pass\nelse: raise AssertionError('second writer admitted')")
|
||||
checks.append('a second writer cannot acquire the active evidence volume')
|
||||
command(first,seed)
|
||||
state=admin(first,'inspect','--db','/data/private/review.sqlite')
|
||||
assert state['submissions']=={'unresolved':1} and state['outbox'].get('pending',0)>0
|
||||
original=command(first,snapshot)
|
||||
checks.append('synthetic uncertain intent and undelivered evidence remain explicit')
|
||||
command(first,"from informed_decision.container import private_directory; private_directory('/backup/private')")
|
||||
result=admin(first,'backup','--db','/data/private/review.sqlite','--output','/backup/private/review.sqlite')
|
||||
assert result['consistent_snapshot'] is True
|
||||
repeat=docker('exec',first,'informed-decision-admin','backup','--db','/data/private/review.sqlite','--output','/backup/private/review.sqlite',check=False)
|
||||
assert repeat.returncode!=0
|
||||
checks.append('consistent backup created while serving; overwrite refused')
|
||||
stop(first)
|
||||
checks.append('SIGTERM closes the serving writer without forced kill')
|
||||
docker('start',first);wait_health(first)
|
||||
assert command(first,snapshot)==original
|
||||
assert admin(first,'inspect','--db','/data/private/review.sqlite')['submissions']=={'unresolved':1}
|
||||
checks.append('restart preserves immutable content and unresolved submission without retry')
|
||||
stop(first)
|
||||
restored=start('restored',backup,fixture);wait_health(restored)
|
||||
assert command(restored,snapshot)==original
|
||||
assert admin(restored,'inspect','--db','/data/private/review.sqlite')['submissions']=={'unresolved':1}
|
||||
checks.append('separate restored volume preserves exact content and unresolved state')
|
||||
stop(restored)
|
||||
(fixture/'runtime.json').chmod(0o644);(fixture/'runtime.json').write_text('{}');(fixture/'runtime.json').chmod(0o444)
|
||||
invalid=start('invalid',data,fixture)
|
||||
code=int(docker('wait',invalid).stdout)
|
||||
assert code!=0
|
||||
checks.append('incomplete owner configuration exits before serving')
|
||||
image=json.loads(docker('image','inspect',args.image).stdout)[0]
|
||||
receipt={'status':'passed','image_id':image['Id'],'repo_digests':image['RepoDigests'],
|
||||
'checks_passed':len(checks),'checks':checks,'fixture_only':True,'external_network':'none',
|
||||
'content_snapshot_sha256':original,'native_identity_policy_audit_proven':False,
|
||||
'published':False,'deployed':False,'factory_attempts':0,'paid_model_calls':0}
|
||||
finally:
|
||||
for name in reversed(containers):docker('rm','-f',name,check=False)
|
||||
for name in reversed(volumes):docker('volume','rm',name,check=False)
|
||||
receipt['cleanup_complete']=all(docker('inspect',name,check=False).returncode!=0 for name in containers)
|
||||
receipt['cleanup_complete'] &= all(docker('volume','inspect',name,check=False).returncode!=0 for name in volumes)
|
||||
assert receipt['cleanup_complete']
|
||||
args.receipt.write_text(json.dumps(receipt,indent=2)+'\n')
|
||||
print(json.dumps({'status':'passed','checks_passed':len(checks),'cleanup_complete':True,'image_id':receipt['image_id']}))
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
|
|
@ -623,6 +623,50 @@ not be silently claimed by the bounded factory profile. The exact operational
|
|||
setup and remaining limits are in `docs/protected-browser-review.md`.
|
||||
T08 remains `progress`; no residual has been hidden by finishing the workplan.
|
||||
|
||||
2026-09-11 — **container and deployment candidate prepared and exercised.**
|
||||
`Containerfile` and hashed dependency lock build the installed wheel on the
|
||||
digest-pinned Alpine base. The runtime has no package installer, runs as 10001
|
||||
on a read-only root filesystem, and copies bounded projected configuration into
|
||||
owned ephemeral 0600 storage. It keeps private persistent evidence at 0700/0600
|
||||
and takes a process-lifetime lock before opening the serving runtime. This
|
||||
preserves the store's checks despite Kubernetes projected-file ownership and
|
||||
prevents a second service from sharing its evidence volume.
|
||||
|
||||
The installed custody CLI creates SQLite-consistent backups and reports only
|
||||
schema/delivery/submission counts. Eleven real-container checks passed with no
|
||||
external network or published port: restart and restore onto a second volume
|
||||
preserved exact content, undelivered evidence and a synthetic unresolved intent.
|
||||
All created containers/volumes were removed. The full suite passes 371 tests
|
||||
(27 added). Runtime files in the image match all 22 source modules; dependencies
|
||||
match the hashed lock. Trivy returned zero HIGH/CRITICAL findings; its Alpine
|
||||
lifecycle-list warning is retained in the receipt.
|
||||
|
||||
The final lifecycle check exposed a real PID-1 shutdown defect: the first image
|
||||
needed a forced kill (exit 137). An explicit SIGTERM handler now lets Waitress
|
||||
drain and the audit pump stop; the final image exits cleanly within 15 seconds.
|
||||
Recreate rollout and the 45-second pod termination window retain one writer.
|
||||
|
||||
The renderer prepares a Recreate Deployment, scoped projected caller token,
|
||||
immutable runtime config, PVC/Service and exact network rules. Kubernetes serving
|
||||
health remains separate from audit acceptance readiness, preserving refusal/
|
||||
recovery access during an audit outage. The source now includes exact ingress
|
||||
proposals for Approval Engine and the admitted PDP, because the existing caller
|
||||
rules do not automatically admit this pod. No namespace-wide caller label is
|
||||
added and no Secret, RBAC, policy assignment or Ingress is created.
|
||||
|
||||
Seven objects passed server dry-run in their exact namespaces. The eighth,
|
||||
Approval Engine ingress, found its namespace absent. Its unchanged policy shape
|
||||
passed in a representative existing namespace; that is schema validation only.
|
||||
Observed Traefik label/websecure port, KeyCape public IP and local-path storage
|
||||
were used to make the candidate concrete, not to claim CNI or custody proof.
|
||||
|
||||
Evidence: `docs/evidence/2026-09-11-container-candidate.json`; operating packet:
|
||||
`deploy/README.md`. The image is local and unpublished. T08 stays `progress` for
|
||||
the admitted native policy/caller/assignments, AUDIT-WP-0009-T11 custody, image
|
||||
publication, owner service/namespace and exact peer admission, registration and
|
||||
real human/deployed binding, platform backup/restore and product acceptance.
|
||||
No cluster apply, native secret read, factory attempt or paid call occurred.
|
||||
|
||||
## Known risks
|
||||
|
||||
- **T02 is a hard gate.** Writing the blueprint before the layer ruling risks
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue