Prepare authenticated lifecycle runtime and production release candidate

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f
This commit is contained in:
tegwick 2026-10-04 21:40:19 +02:00
parent f3b71b1521
commit 4f546ae3b1
41 changed files with 2598 additions and 52 deletions

View file

@ -20,5 +20,5 @@ RUN rm -rf /usr/local/bin/pip* /usr/local/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
EXPOSE 8080 8081
ENTRYPOINT ["informed-decision-container"]

View file

@ -308,3 +308,15 @@ implementation, not a deployed intake listener. Attribute contracts, forwarding,
workload custody, private listener composition and live acceptance are tracked
as blocked tasks in INFD-WP-0005, with acceptance requirements in
`docs/intake-dependency-requirements.md`.
## Initial lifecycle production candidate — 2026-10-04
The private workload listener is now composed and tested with CA-verified
Kubernetes TokenReview, rotating projections and a shared single-writer runtime.
Initial audience is net-kingdom-admins humans plus the exact State Hub service
account. Attribute review now verifies native requester/human-control and receipt
pins before policy evaluation. FLEX-WP-0033 supplies separately pinned lifecycle
and v12 review policy candidates. Published immutable artifacts, security scans,
server dry-runs and recovery steps are retained in `deploy/lifecycle/README.md`.
These changes are not deployed. Requester custody, production State Hub forwarding
and native acceptance remain live tasks in INFD-WP-0005/0007 and RPF-WP-0051.

View file

@ -20,8 +20,8 @@ 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
dependency pins deliberately with `uv lock` followed by `uv export --no-dev --no-emit-project --format
requirements-txt --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.

View file

@ -0,0 +1,96 @@
# INFD-WP-0007 production candidate
Prepared 2026-10-04 for the founder-selected initial audience: net-kingdom-admins
humans and system:serviceaccount:state-hub:state-hub. **Not deployed.**
| Artifact | Immutable reference |
| --- | --- |
| Service | forgejo.coulomb.social/coulomb/informed-decision@sha256:2768bd1da3e651cc0be2d0f1ae6eea1af4eb057b31ef63ebb03b004c2ed76c3d |
| Access Engine | forgejo.coulomb.social/coulomb/flex-auth@sha256:f6ed8af69af766d7e9498465a23c292d91185b79352065736a2132216aac1d37 |
| Lifecycle package | informed-decision.lifecycle v1, sha256:cee6a79f74e81d0d389a2c2e2451a92be3434ea9dfcee4986e8c7e6cfdbb1827 |
| Human review package | informed-decision.compact-sitting v12, sha256:c5387f7d198dc39ab5967e0c218ce05cc779693565c8875e3035e45ec002a2db |
`admission.candidate.json` renders `service.candidate.json` using
`tools/render_deployment.py`. `registries.candidate.json` projects the three
sibling JSON registries. The policy YAML is Helm-rendered from Access Engine's
FLEX-WP-0033 candidate values; it is a review artifact, not a second deployment
owner. Existing Helm releases must be updated through their owning workflow.
All 25 proposed objects passed Kubernetes server dry-run against railiance01.
No Secret is included. Dry-run does not test reachability, credential delivery,
policy caller identity, backup restoration or a human disposition.
## Proposed live changes
- Replace the service's image and immutable runtime ConfigMap, retaining its
public Service/Ingress, single writer, Recreate strategy and existing PVC.
- Enable `/decisions` and lifecycle audit delivery; keep private API on 8081.
- Add the exact State Hub peer, API-server egress, CA/token projection and only
create-tokenreviews RBAC for informed-decision/review.
- Mount the proposed reloadable registries and Platform's exact read-only
requester Secret projection, without subPath.
- Add a separate lifecycle PDP and change the sitting PDP to the v12 package.
Review retains all v11 exact mandates and 15-minute KeyCape MFA; lifecycle
authoring uses a 12-hour MFA window. Agents cannot accept.
- Initial intake permits decision-record / net-kingdom-admins, 50 new attempts
per UTC day, with the existing platform-root recipient. Other admins may
collaborate; this does not silently make them the bound recipient.
The policy chart retains the owner's existing port-only API-server egress
(443/6443). The consumer's new API-server egress is address-specific. CARING is
advisory and consumer fact attribution is unsigned, as in the existing reviewed
contract; neither is presented as a newly enforced control.
## Pre-cutover gates
1. RPF-WP-0051 / CCR-2026-0032: complete the already prepared requester delivery
under its separate live go, with exact identity/path negative proofs and
mounted-file refresh/rotation evidence. Only the audit ExternalSecret was
present in the live namespace at inspection.
2. INFD-WP-0005-T08: State Hub owner supplies production forwarding and separate
lifecycle/intake audience token projections. Its current dev fixture is not
an activated forwarder. Confirm notification receiver reachability; keep
resolution write-back disabled until its owner admission.
3. Refresh live image/config pins and unresolved-entry counts; complete the
backup/restore steps below; obtain explicit approval of this concrete rollout.
INFD-WP-0007-T07 and CCR-2026-0032 require this distinct go.
## Cutover and recovery procedure
Before migrating, close new review traffic through the owning ingress workflow,
let active requests finish, and inspect the current schema-2 writer read-only.
The observed baseline was 24 confirmed submissions, 158 delivered outbox rows,
zero unresolved entries; these counts are not a future maintenance-window gate.
Retain the exact current Deployment, Service, runtime ConfigMap, Helm values and
all related policy pins. Do not include credentials in the evidence packet.
Use the existing running image's installed `informed-decision-admin backup` to
create a unique SQLite consistent snapshot in a private directory. Copy the
backup to independently protected custody, record its digest, and verify a
restore on a separate private volume before cutover. Close the old writer before
starting the schema-5 image. Local synthetic restore proof is retained in the
evidence directory; it does not replace this production data restoration check.
Deploy admitted owner policy/custody prerequisites, then the service candidate.
Verify `/healthz`, `/readyz`, pending/blocked counts, actual audience isolation,
wrong-caller refusals and public/private route separation. Exercise two new
records and a revision without changing image/config/package pins. Only a human
may finish review. Correlate the native Approval Engine entry and independent
Audit Core receipts; restart and retrieve the same immutable evidence.
If validation fails, stop the candidate and retain its database plus all new
receipts. **Never start the old schema-2 image on the schema-5 database.** Rollback
requires the verified pre-cutover copy on a separate volume and reconciliation
of every post-cutover external effect before resuming writes. An uncertain
submission stays unresolved; do not retry or discard it to enable rollback.
The prior image is in ../sitting-admission.json and remains a reference, not an
instruction to downgrade the live database.
## Validation evidence
`docs/evidence/2026-10-04-lifecycle-release/` retains container checks, bounded
scan summaries and policy validation. Service: 662 passed, 1 skipped with owner
components; 13 container checks passed, including restart and backup/restore.
Both artifact scans contain zero HIGH/CRITICAL findings. Actual evaluator tests
cover first-audience collaboration across revisions and verify unchanged package
pins; authentication custody in those tests is synthetic. Native activation and
broader browser packet/binding profiles remain explicit workplan tasks.

View file

@ -0,0 +1,52 @@
{
"image": "forgejo.coulomb.social/coulomb/informed-decision@sha256:2768bd1da3e651cc0be2d0f1ae6eea1af4eb057b31ef63ebb03b004c2ed76c3d",
"policy": {
"origin": "http://flex-auth-informed-decision-sitting.flex-auth.svc.cluster.local:8080",
"package": "informed-decision.compact-sitting",
"version": "v12",
"package_digest": "sha256:c5387f7d198dc39ab5967e0c218ce05cc779693565c8875e3035e45ec002a2db",
"pod_name": "flex-auth-informed-decision-sitting"
},
"keycape_egress_ips": [
"92.205.62.239"
],
"storage_class": "local-path",
"ordinary_approval_ids": [
"2ce76d7f-01c3-4b63-8476-8d1230679769",
"c2af4bcf-15b4-4305-aac1-acc7e4dcb099",
"dc22666a-d5d7-46bc-9490-ebe9fe09dc38"
],
"lifecycle_policy": {
"package": "informed-decision.lifecycle",
"version": "v1",
"package_digest": "sha256:cee6a79f74e81d0d389a2c2e2451a92be3434ea9dfcee4986e8c7e6cfdbb1827",
"origin": "http://flex-auth-informed-decision-lifecycle.flex-auth.svc.cluster.local:8080",
"pod_name": "flex-auth-informed-decision-lifecycle"
},
"lifecycle_delivery": true,
"intake_attributes": true,
"intake_delivery": {
"hub_origin": "http://state-hub.state-hub.svc.cluster.local:8000"
},
"workload_api": {
"registries_configmap": "informed-decision-workload-registry",
"peers": [
{
"namespace": "state-hub",
"app": "state-hub"
}
],
"api_egress": [
{
"ip": "10.43.0.1",
"port": 443
},
{
"ip": "92.205.62.239",
"port": 6443
}
],
"intake": true,
"lifecycle": true
}
}

View file

@ -0,0 +1,12 @@
{
"system:serviceaccount:state-hub:state-hub": {
"submitter_id": "state-hub",
"memo_classes": [
"decision-record"
],
"review_groups": [
"net-kingdom-admins"
],
"daily_limit": 50
}
}

View file

@ -0,0 +1,5 @@
{
"system:serviceaccount:state-hub:state-hub": {
"tenant": "tenant:platform"
}
}

View file

@ -0,0 +1,189 @@
---
# Source: flex-auth/templates/networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: flex-auth-informed-decision-lifecycle
labels:
app.kubernetes.io/name: flex-auth-informed-decision-lifecycle
app.kubernetes.io/instance: flex-auth-informed-decision-lifecycle
app.kubernetes.io/part-of: flex-auth
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: flex-auth-informed-decision-lifecycle
policyTypes:
- Ingress
- Egress
egress:
- ports:
- port: 443
protocol: TCP
- port: 6443
protocol: TCP
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: informed-decision
podSelector:
matchLabels:
app.kubernetes.io/name: informed-decision
ports:
- port: 8080
protocol: TCP
---
# Source: flex-auth/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: flex-auth-informed-decision-lifecycle
labels:
app.kubernetes.io/name: flex-auth-informed-decision-lifecycle
app.kubernetes.io/instance: flex-auth-informed-decision-lifecycle
app.kubernetes.io/part-of: flex-auth
automountServiceAccountToken: false
---
# Source: flex-auth/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: flex-auth-informed-decision-lifecycle-tokenreviewer
labels:
app.kubernetes.io/name: flex-auth-informed-decision-lifecycle
app.kubernetes.io/instance: flex-auth-informed-decision-lifecycle
app.kubernetes.io/part-of: flex-auth
rules:
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
---
# Source: flex-auth/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: flex-auth-informed-decision-lifecycle-tokenreviewer
labels:
app.kubernetes.io/name: flex-auth-informed-decision-lifecycle
app.kubernetes.io/instance: flex-auth-informed-decision-lifecycle
app.kubernetes.io/part-of: flex-auth
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: flex-auth-informed-decision-lifecycle-tokenreviewer
subjects:
- kind: ServiceAccount
name: flex-auth-informed-decision-lifecycle
namespace: flex-auth
---
# Source: flex-auth/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: flex-auth-informed-decision-lifecycle
labels:
app.kubernetes.io/name: flex-auth-informed-decision-lifecycle
app.kubernetes.io/instance: flex-auth-informed-decision-lifecycle
app.kubernetes.io/part-of: flex-auth
spec:
selector:
app.kubernetes.io/name: flex-auth-informed-decision-lifecycle
ports:
- name: http
port: 8080
targetPort: http
---
# Source: flex-auth/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: flex-auth-informed-decision-lifecycle
labels:
app.kubernetes.io/name: flex-auth-informed-decision-lifecycle
app.kubernetes.io/instance: flex-auth-informed-decision-lifecycle
app.kubernetes.io/part-of: flex-auth
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: flex-auth-informed-decision-lifecycle
template:
metadata:
labels:
app.kubernetes.io/name: flex-auth-informed-decision-lifecycle
app.kubernetes.io/instance: flex-auth-informed-decision-lifecycle
app.kubernetes.io/part-of: flex-auth
spec:
automountServiceAccountToken: false
serviceAccountName: flex-auth-informed-decision-lifecycle
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: flex-auth
image: forgejo.coulomb.social/coulomb/flex-auth@sha256:f6ed8af69af766d7e9498465a23c292d91185b79352065736a2132216aac1d37
imagePullPolicy: IfNotPresent
args:
- serve
- --addr
- 0.0.0.0:8080
- --registry
- /opt/flex-auth/examples/informed-decision-lifecycle/registry.json
- --policy
- /opt/flex-auth/examples/informed-decision-lifecycle/policy.md
- --caller-auth-mode
- "enforce"
- --caller-kubernetes-url
- "https://10.43.0.1"
- --caller-binding
- "informed-decision=system:serviceaccount:informed-decision:review"
ports:
- name: http
containerPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: http
periodSeconds: 20
readinessProbe:
httpGet:
path: /healthz
port: http
periodSeconds: 5
resources:
limits:
cpu: 300m
memory: 192Mi
requests:
cpu: 5m
memory: 32Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
volumeMounts:
- mountPath: /var/run/secrets/flex-auth-reviewer
name: flex-auth-reviewer
readOnly: true
volumes:
- name: flex-auth-reviewer
projected:
defaultMode: 0440
sources:
- serviceAccountToken:
expirationSeconds: 3600
path: token
- configMap:
name: kube-root-ca.crt
items:
- key: ca.crt
path: ca.crt

View file

@ -0,0 +1,16 @@
{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "informed-decision-workload-registry",
"namespace": "informed-decision",
"annotations": {
"informed-decision.coulomb.social/admission": "proposal; INFD-WP-0007 concrete rollout approval required"
}
},
"data": {
"lifecycle-callers.json": "{\n \"system:serviceaccount:state-hub:state-hub\": {\n \"tenant\": \"tenant:platform\"\n }\n}\n",
"intake-submitters.json": "{\n \"system:serviceaccount:state-hub:state-hub\": {\n \"submitter_id\": \"state-hub\",\n \"memo_classes\": [\n \"decision-record\"\n ],\n \"review_groups\": [\n \"net-kingdom-admins\"\n ],\n \"daily_limit\": 50\n }\n}\n",
"review-groups.json": "{\n \"net-kingdom-admins\": {\n \"subject\": \"uid=platform-root,ou=people,dc=netkingdom,dc=local\",\n \"display_name\": \"Platform reviewer\"\n }\n}\n"
}
}

View file

@ -0,0 +1,6 @@
{
"net-kingdom-admins": {
"subject": "uid=platform-root,ou=people,dc=netkingdom,dc=local",
"display_name": "Platform reviewer"
}
}

View file

@ -0,0 +1,189 @@
---
# Source: flex-auth/templates/networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: flex-auth-informed-decision-sitting
labels:
app.kubernetes.io/name: flex-auth-informed-decision-sitting
app.kubernetes.io/instance: flex-auth-informed-decision-sitting
app.kubernetes.io/part-of: flex-auth
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: flex-auth-informed-decision-sitting
policyTypes:
- Ingress
- Egress
egress:
- ports:
- port: 443
protocol: TCP
- port: 6443
protocol: TCP
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: informed-decision
podSelector:
matchLabels:
app.kubernetes.io/name: informed-decision
ports:
- port: 8080
protocol: TCP
---
# Source: flex-auth/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: flex-auth-informed-decision-sitting
labels:
app.kubernetes.io/name: flex-auth-informed-decision-sitting
app.kubernetes.io/instance: flex-auth-informed-decision-sitting
app.kubernetes.io/part-of: flex-auth
automountServiceAccountToken: false
---
# Source: flex-auth/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: flex-auth-informed-decision-sitting-tokenreviewer
labels:
app.kubernetes.io/name: flex-auth-informed-decision-sitting
app.kubernetes.io/instance: flex-auth-informed-decision-sitting
app.kubernetes.io/part-of: flex-auth
rules:
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
---
# Source: flex-auth/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: flex-auth-informed-decision-sitting-tokenreviewer
labels:
app.kubernetes.io/name: flex-auth-informed-decision-sitting
app.kubernetes.io/instance: flex-auth-informed-decision-sitting
app.kubernetes.io/part-of: flex-auth
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: flex-auth-informed-decision-sitting-tokenreviewer
subjects:
- kind: ServiceAccount
name: flex-auth-informed-decision-sitting
namespace: flex-auth
---
# Source: flex-auth/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: flex-auth-informed-decision-sitting
labels:
app.kubernetes.io/name: flex-auth-informed-decision-sitting
app.kubernetes.io/instance: flex-auth-informed-decision-sitting
app.kubernetes.io/part-of: flex-auth
spec:
selector:
app.kubernetes.io/name: flex-auth-informed-decision-sitting
ports:
- name: http
port: 8080
targetPort: http
---
# Source: flex-auth/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: flex-auth-informed-decision-sitting
labels:
app.kubernetes.io/name: flex-auth-informed-decision-sitting
app.kubernetes.io/instance: flex-auth-informed-decision-sitting
app.kubernetes.io/part-of: flex-auth
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: flex-auth-informed-decision-sitting
template:
metadata:
labels:
app.kubernetes.io/name: flex-auth-informed-decision-sitting
app.kubernetes.io/instance: flex-auth-informed-decision-sitting
app.kubernetes.io/part-of: flex-auth
spec:
automountServiceAccountToken: false
serviceAccountName: flex-auth-informed-decision-sitting
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: flex-auth
image: forgejo.coulomb.social/coulomb/flex-auth@sha256:f6ed8af69af766d7e9498465a23c292d91185b79352065736a2132216aac1d37
imagePullPolicy: IfNotPresent
args:
- serve
- --addr
- 0.0.0.0:8080
- --registry
- /opt/flex-auth/examples/informed-decision-lifecycle-review/registry.json
- --policy
- /opt/flex-auth/examples/informed-decision-lifecycle-review/policy.md
- --caller-auth-mode
- "enforce"
- --caller-kubernetes-url
- "https://10.43.0.1"
- --caller-binding
- "informed-decision=system:serviceaccount:informed-decision:review"
ports:
- name: http
containerPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: http
periodSeconds: 20
readinessProbe:
httpGet:
path: /healthz
port: http
periodSeconds: 5
resources:
limits:
cpu: 300m
memory: 192Mi
requests:
cpu: 5m
memory: 32Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
volumeMounts:
- mountPath: /var/run/secrets/flex-auth-reviewer
name: flex-auth-reviewer
readOnly: true
volumes:
- name: flex-auth-reviewer
projected:
defaultMode: 0440
sources:
- serviceAccountToken:
expirationSeconds: 3600
path: token
- configMap:
name: kube-root-ca.crt
items:
- key: ca.crt
path: ca.crt

View file

@ -0,0 +1,800 @@
{
"apiVersion": "v1",
"kind": "List",
"items": [
{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"name": "review",
"namespace": "informed-decision",
"annotations": {
"informed-decision.coulomb.social/admission": "review-only; INFD-WP-0001-T08"
}
},
"automountServiceAccountToken": false
},
{
"apiVersion": "v1",
"kind": "PersistentVolumeClaim",
"metadata": {
"name": "informed-decision-data",
"namespace": "informed-decision",
"annotations": {
"informed-decision.coulomb.social/admission": "review-only; INFD-WP-0001-T08"
}
},
"spec": {
"accessModes": [
"ReadWriteOnce"
],
"storageClassName": "local-path",
"resources": {
"requests": {
"storage": "1Gi"
}
}
}
},
{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "informed-decision-runtime-92d01402eb05",
"namespace": "informed-decision",
"annotations": {
"informed-decision.coulomb.social/admission": "review-only; INFD-WP-0001-T08"
}
},
"immutable": true,
"data": {
"runtime.json": "{\"approval_origin\":\"http://approval-engine.approval-engine.svc.cluster.local:8080\",\"audit\":{\"origin\":\"http://audit-core.audit-core.svc.cluster.local:8080\",\"sender_token_file\":\"/var/run/secrets/informed-decision/audit/token\"},\"evidence_db\":\"/data/private/review.sqlite\",\"intake_attributes\":true,\"intake_delivery\":{\"hub_origin\":\"http://state-hub.state-hub.svc.cluster.local:8000\"},\"lifecycle_delivery\":true,\"lifecycle_policy\":{\"caller_token_file\":\"/var/run/secrets/informed-decision/policy/token\",\"origin\":\"http://flex-auth-informed-decision-lifecycle.flex-auth.svc.cluster.local:8080\",\"package\":\"informed-decision.lifecycle\",\"package_digest\":\"sha256:cee6a79f74e81d0d389a2c2e2451a92be3434ea9dfcee4986e8c7e6cfdbb1827\",\"version\":\"v1\"},\"ordinary_approval_ids\":[\"2ce76d7f-01c3-4b63-8476-8d1230679769\",\"c2af4bcf-15b4-4305-aac1-acc7e4dcb099\",\"dc22666a-d5d7-46bc-9490-ebe9fe09dc38\"],\"policy\":{\"caller_token_file\":\"/var/run/secrets/informed-decision/policy/token\",\"origin\":\"http://flex-auth-informed-decision-sitting.flex-auth.svc.cluster.local:8080\",\"package\":\"informed-decision.compact-sitting\",\"package_digest\":\"sha256:c5387f7d198dc39ab5967e0c218ce05cc779693565c8875e3035e45ec002a2db\",\"version\":\"v12\"},\"schema\":\"informed-decision.review-runtime.v1\",\"workload_api\":{\"intake\":{\"groups_file\":\"/workload-registry/review-groups.json\",\"secret_file\":\"/var/run/secrets/informed-decision/requester/client-secret\",\"submitters_file\":\"/workload-registry/intake-submitters.json\"},\"lifecycle_callers_file\":\"/workload-registry/lifecycle-callers.json\",\"token_review\":{\"ca_file\":\"/var/run/secrets/informed-decision/kubernetes/ca.crt\",\"origin\":\"https://kubernetes.default.svc:443\",\"token_file\":\"/var/run/secrets/informed-decision/kubernetes/token\"}}}"
}
},
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "informed-decision",
"namespace": "informed-decision",
"annotations": {
"informed-decision.coulomb.social/admission": "review-only; INFD-WP-0001-T08"
}
},
"spec": {
"replicas": 1,
"strategy": {
"type": "Recreate"
},
"progressDeadlineSeconds": 600,
"selector": {
"matchLabels": {
"app.kubernetes.io/name": "informed-decision"
}
},
"template": {
"metadata": {
"labels": {
"app.kubernetes.io/name": "informed-decision",
"app.kubernetes.io/component": "review"
}
},
"spec": {
"automountServiceAccountToken": false,
"serviceAccountName": "review",
"terminationGracePeriodSeconds": 45,
"securityContext": {
"runAsNonRoot": true,
"runAsUser": 10001,
"runAsGroup": 10001,
"fsGroup": 10001,
"fsGroupChangePolicy": "OnRootMismatch",
"seccompProfile": {
"type": "RuntimeDefault"
}
},
"containers": [
{
"name": "informed-decision",
"image": "forgejo.coulomb.social/coulomb/informed-decision@sha256:2768bd1da3e651cc0be2d0f1ae6eea1af4eb057b31ef63ebb03b004c2ed76c3d",
"securityContext": {
"allowPrivilegeEscalation": false,
"readOnlyRootFilesystem": true,
"capabilities": {
"drop": [
"ALL"
]
}
},
"env": [
{
"name": "INFD_KEYCAPE_ISSUER",
"value": "https://kc.coulomb.social"
},
{
"name": "INFD_CONTAINER_CONFIG",
"value": "/configuration/runtime.json"
}
],
"ports": [
{
"name": "http",
"containerPort": 8080
},
{
"name": "workload",
"containerPort": 8081
}
],
"startupProbe": {
"httpGet": {
"path": "/healthz",
"port": "http"
},
"periodSeconds": 10,
"timeoutSeconds": 2,
"failureThreshold": 12
},
"readinessProbe": {
"httpGet": {
"path": "/healthz",
"port": "http"
},
"periodSeconds": 10,
"timeoutSeconds": 2
},
"livenessProbe": {
"httpGet": {
"path": "/healthz",
"port": "http"
},
"periodSeconds": 10,
"timeoutSeconds": 2
},
"resources": {
"requests": {
"cpu": "5m",
"memory": "64Mi"
},
"limits": {
"cpu": "500m",
"memory": "256Mi"
}
},
"volumeMounts": [
{
"name": "data",
"mountPath": "/data"
},
{
"name": "runtime",
"mountPath": "/run/informed-decision"
},
{
"name": "tmp",
"mountPath": "/tmp"
},
{
"name": "configuration",
"mountPath": "/configuration",
"readOnly": true
},
{
"name": "caller",
"mountPath": "/var/run/secrets/informed-decision/policy",
"readOnly": true
},
{
"name": "audit",
"mountPath": "/var/run/secrets/informed-decision/audit",
"readOnly": true
},
{
"name": "kubernetes-api",
"mountPath": "/var/run/secrets/informed-decision/kubernetes",
"readOnly": true
},
{
"name": "workload-registry",
"mountPath": "/workload-registry",
"readOnly": true
},
{
"name": "sitting-requester",
"mountPath": "/var/run/secrets/informed-decision/requester",
"readOnly": true
}
]
}
],
"volumes": [
{
"name": "data",
"persistentVolumeClaim": {
"claimName": "informed-decision-data"
}
},
{
"name": "runtime",
"emptyDir": {
"medium": "Memory",
"sizeLimit": "16Mi"
}
},
{
"name": "tmp",
"emptyDir": {
"medium": "Memory",
"sizeLimit": "16Mi"
}
},
{
"name": "configuration",
"configMap": {
"name": "informed-decision-runtime-92d01402eb05",
"defaultMode": 292
}
},
{
"name": "caller",
"projected": {
"defaultMode": 288,
"sources": [
{
"serviceAccountToken": {
"path": "token",
"audience": "flex-auth",
"expirationSeconds": 3600
}
}
]
}
},
{
"name": "audit",
"secret": {
"secretName": "informed-decision-audit",
"defaultMode": 288,
"items": [
{
"key": "token",
"path": "token"
}
]
}
},
{
"name": "kubernetes-api",
"projected": {
"defaultMode": 288,
"sources": [
{
"serviceAccountToken": {
"path": "token",
"expirationSeconds": 600
}
},
{
"configMap": {
"name": "kube-root-ca.crt",
"items": [
{
"key": "ca.crt",
"path": "ca.crt"
}
]
}
}
]
}
},
{
"name": "workload-registry",
"configMap": {
"name": "informed-decision-workload-registry",
"defaultMode": 292
}
},
{
"name": "sitting-requester",
"secret": {
"secretName": "informed-decision-sitting-requester",
"defaultMode": 288,
"optional": false,
"items": [
{
"key": "client-secret",
"path": "client-secret"
}
]
}
}
]
}
}
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "informed-decision",
"namespace": "informed-decision",
"annotations": {
"informed-decision.coulomb.social/admission": "review-only; INFD-WP-0001-T08"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"app.kubernetes.io/name": "informed-decision",
"app.kubernetes.io/component": "review"
},
"ports": [
{
"name": "http",
"port": 80,
"targetPort": "http",
"protocol": "TCP"
}
]
}
},
{
"apiVersion": "networking.k8s.io/v1",
"kind": "NetworkPolicy",
"metadata": {
"name": "informed-decision-review",
"namespace": "informed-decision",
"annotations": {
"informed-decision.coulomb.social/admission": "review-only; INFD-WP-0001-T08"
}
},
"spec": {
"podSelector": {
"matchLabels": {
"app.kubernetes.io/name": "informed-decision",
"app.kubernetes.io/component": "review"
}
},
"policyTypes": [
"Ingress",
"Egress"
],
"ingress": [
{
"from": [
{
"namespaceSelector": {
"matchLabels": {
"kubernetes.io/metadata.name": "kube-system"
}
},
"podSelector": {
"matchLabels": {
"app.kubernetes.io/name": "traefik"
}
}
}
],
"ports": [
{
"protocol": "TCP",
"port": 8080
}
]
},
{
"from": [
{
"namespaceSelector": {
"matchLabels": {
"kubernetes.io/metadata.name": "state-hub"
}
},
"podSelector": {
"matchLabels": {
"app.kubernetes.io/name": "state-hub"
}
}
}
],
"ports": [
{
"protocol": "TCP",
"port": 8081
}
]
}
],
"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": [
{
"namespaceSelector": {
"matchLabels": {
"kubernetes.io/metadata.name": "approval-engine"
}
},
"podSelector": {
"matchLabels": {
"app.kubernetes.io/name": "approval-engine"
}
}
},
{
"namespaceSelector": {
"matchLabels": {
"kubernetes.io/metadata.name": "audit-core"
}
},
"podSelector": {
"matchLabels": {
"app.kubernetes.io/name": "audit-core"
}
}
},
{
"namespaceSelector": {
"matchLabels": {
"kubernetes.io/metadata.name": "flex-auth"
}
},
"podSelector": {
"matchLabels": {
"app.kubernetes.io/name": "flex-auth-informed-decision-sitting"
}
}
},
{
"namespaceSelector": {
"matchLabels": {
"kubernetes.io/metadata.name": "flex-auth"
}
},
"podSelector": {
"matchLabels": {
"app.kubernetes.io/name": "flex-auth-informed-decision-lifecycle"
}
}
}
],
"ports": [
{
"protocol": "TCP",
"port": 8080
}
]
},
{
"to": [
{
"namespaceSelector": {
"matchLabels": {
"kubernetes.io/metadata.name": "kube-system"
}
},
"podSelector": {
"matchLabels": {
"app.kubernetes.io/name": "traefik"
}
}
}
],
"ports": [
{
"protocol": "TCP",
"port": 8443
}
]
},
{
"to": [
{
"ipBlock": {
"cidr": "92.205.62.239/32"
}
}
],
"ports": [
{
"protocol": "TCP",
"port": 443
}
]
},
{
"to": [
{
"namespaceSelector": {
"matchLabels": {
"kubernetes.io/metadata.name": "state-hub"
}
},
"podSelector": {
"matchLabels": {
"app.kubernetes.io/name": "state-hub"
}
}
}
],
"ports": [
{
"protocol": "TCP",
"port": 8000
}
]
},
{
"to": [
{
"ipBlock": {
"cidr": "10.43.0.1/32"
}
}
],
"ports": [
{
"protocol": "TCP",
"port": 443
}
]
},
{
"to": [
{
"ipBlock": {
"cidr": "92.205.62.239/32"
}
}
],
"ports": [
{
"protocol": "TCP",
"port": 6443
}
]
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "informed-decision-workload",
"namespace": "informed-decision",
"annotations": {
"informed-decision.coulomb.social/admission": "review-only; INFD-WP-0001-T08"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"app.kubernetes.io/name": "informed-decision",
"app.kubernetes.io/component": "review"
},
"ports": [
{
"name": "workload",
"port": 8081,
"targetPort": "workload",
"protocol": "TCP"
}
]
}
},
{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRole",
"metadata": {
"name": "informed-decision-tokenreview",
"annotations": {
"informed-decision.coulomb.social/admission": "proposal; INFD-WP-0007/INFD-WP-0005 owner review required"
}
},
"rules": [
{
"apiGroups": [
"authentication.k8s.io"
],
"resources": [
"tokenreviews"
],
"verbs": [
"create"
]
}
]
},
{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRoleBinding",
"metadata": {
"name": "informed-decision-tokenreview",
"annotations": {
"informed-decision.coulomb.social/admission": "proposal; INFD-WP-0007/INFD-WP-0005 owner review required"
}
},
"roleRef": {
"apiGroup": "rbac.authorization.k8s.io",
"kind": "ClusterRole",
"name": "informed-decision-tokenreview"
},
"subjects": [
{
"kind": "ServiceAccount",
"name": "review",
"namespace": "informed-decision"
}
]
},
{
"apiVersion": "networking.k8s.io/v1",
"kind": "NetworkPolicy",
"metadata": {
"name": "informed-decision-review-ingress",
"namespace": "approval-engine",
"annotations": {
"informed-decision.coulomb.social/admission": "review-only; INFD-WP-0001-T08",
"informed-decision.coulomb.social/owner-review": "APPROVAL-WP-0002-T01"
}
},
"spec": {
"podSelector": {
"matchLabels": {
"app.kubernetes.io/name": "approval-engine"
}
},
"policyTypes": [
"Ingress"
],
"ingress": [
{
"from": [
{
"namespaceSelector": {
"matchLabels": {
"kubernetes.io/metadata.name": "informed-decision"
}
},
"podSelector": {
"matchLabels": {
"app.kubernetes.io/name": "informed-decision",
"app.kubernetes.io/component": "review"
}
}
}
],
"ports": [
{
"protocol": "TCP",
"port": 8080
}
]
}
]
}
},
{
"apiVersion": "networking.k8s.io/v1",
"kind": "NetworkPolicy",
"metadata": {
"name": "informed-decision-review-ingress",
"namespace": "flex-auth",
"annotations": {
"informed-decision.coulomb.social/admission": "review-only; INFD-WP-0001-T08",
"informed-decision.coulomb.social/owner-review": "INFD-WP-0001-T08 policy-owner return"
}
},
"spec": {
"podSelector": {
"matchLabels": {
"app.kubernetes.io/name": "flex-auth-informed-decision-sitting"
}
},
"policyTypes": [
"Ingress"
],
"ingress": [
{
"from": [
{
"namespaceSelector": {
"matchLabels": {
"kubernetes.io/metadata.name": "informed-decision"
}
},
"podSelector": {
"matchLabels": {
"app.kubernetes.io/name": "informed-decision",
"app.kubernetes.io/component": "review"
}
}
}
],
"ports": [
{
"protocol": "TCP",
"port": 8080
}
]
}
]
}
},
{
"apiVersion": "networking.k8s.io/v1",
"kind": "NetworkPolicy",
"metadata": {
"name": "informed-decision-lifecycle-ingress",
"namespace": "flex-auth",
"annotations": {
"informed-decision.coulomb.social/admission": "review-only; INFD-WP-0001-T08",
"informed-decision.coulomb.social/owner-review": "INFD-WP-0007 lifecycle policy-owner return"
}
},
"spec": {
"podSelector": {
"matchLabels": {
"app.kubernetes.io/name": "flex-auth-informed-decision-lifecycle"
}
},
"policyTypes": [
"Ingress"
],
"ingress": [
{
"from": [
{
"namespaceSelector": {
"matchLabels": {
"kubernetes.io/metadata.name": "informed-decision"
}
},
"podSelector": {
"matchLabels": {
"app.kubernetes.io/name": "informed-decision",
"app.kubernetes.io/component": "review"
}
}
}
],
"ports": [
{
"protocol": "TCP",
"port": 8080
}
]
}
]
}
}
]
}

View file

@ -0,0 +1,32 @@
{
"status": "passed",
"image_id": "sha256:2768bd1da3e651cc0be2d0f1ae6eea1af4eb057b31ef63ebb03b004c2ed76c3d",
"repo_digests": [
"informed-decision@sha256:2768bd1da3e651cc0be2d0f1ae6eea1af4eb057b31ef63ebb03b004c2ed76c3d"
],
"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": "7bcbffb5652f7784de477241c4fcc464f8f5fa26288c44876db87beec964a904",
"native_identity_policy_audit_proven": false,
"published": false,
"deployed": false,
"factory_attempts": 0,
"paid_model_calls": 0,
"cleanup_complete": true
}

View file

@ -0,0 +1,88 @@
{
"valid": true,
"caring_findings": [
{
"code": "CARING-POLICY-MISSING-DIMENSION",
"severity": "warning",
"message": "policy package should declare governed canonical roles",
"fields": [
"caring.canonical_roles"
]
},
{
"code": "CARING-POLICY-MISSING-DIMENSION",
"severity": "warning",
"message": "policy package should declare governed organization relations",
"fields": [
"caring.organization_relations"
]
},
{
"code": "CARING-POLICY-MISSING-DIMENSION",
"severity": "warning",
"message": "policy package should declare governed scopes",
"fields": [
"caring.scopes"
]
},
{
"code": "CARING-POLICY-MISSING-DIMENSION",
"severity": "warning",
"message": "policy package should declare governed planes",
"fields": [
"caring.planes"
]
},
{
"code": "CARING-POLICY-MISSING-DIMENSION",
"severity": "warning",
"message": "policy package should declare governed capabilities",
"fields": [
"caring.capabilities"
]
},
{
"code": "CARING-POLICY-MISSING-DIMENSION",
"severity": "warning",
"message": "policy package should declare governed exposure modes",
"fields": [
"caring.exposure_modes"
]
},
{
"code": "CARING-POLICY-MISSING-DIMENSION",
"severity": "warning",
"message": "policy package should declare governed conditions",
"fields": [
"caring.conditions"
]
},
{
"code": "CARING-POLICY-MISSING-DIMENSION",
"severity": "warning",
"message": "policy package should declare governed restrictions",
"fields": [
"caring.restrictions"
]
}
],
"tests": [
{
"name": "test_empty_denied",
"passed": true
}
],
"fixtures": [
{
"id": "unknown-request-denied",
"passed": true,
"expected": {
"effect": "deny"
},
"actual": {
"effect": "deny",
"reason": "lifecycle_scope_or_identity_refused"
}
}
]
}

View file

@ -0,0 +1,19 @@
{
"date": "2026-10-04",
"status": "prepared-not-deployed",
"server_dry_run": {
"cluster": "railiance01",
"objects": 25,
"passed": true
},
"sha256": {
"deploy/lifecycle/admission.candidate.json": "e3054aebb2338146b912121198eba0232973574b9f045710abc802e524fe43f6",
"deploy/lifecycle/intake-submitters.json": "58ba5ef87ecfd40bc7f0eaf2beb5e50f0c1f0e47315e79dbfa9e06411a17eba9",
"deploy/lifecycle/lifecycle-callers.json": "0b66382a64bdf81ee75d14a70ba3b19290084922a1e0bc8a873af0f039dc7041",
"deploy/lifecycle/policy.candidate.yaml": "e08fcb494d8d29f45b5fbce949246ce2de28e73a56efd2064f42dbfee8f3d11f",
"deploy/lifecycle/registries.candidate.json": "9bad6db5fbc2178d31b04120016a41830c3552fbb9e2517b718f9bbe87cf649e",
"deploy/lifecycle/review-groups.json": "1d61d2b82230981130936651a7fc4230e06ecaa51416a8a1be2bb2eb6ec4c353",
"deploy/lifecycle/review-policy.candidate.yaml": "6c49a9d520a553778935555351c70b3823b72dd0f1ed3a5383d5a4fc0e5e7998",
"deploy/lifecycle/service.candidate.json": "bf36ecb0160bf893a60fe059a3baf27a63668e2bb7b9f17c9582d6de8e842545"
}
}

View file

@ -0,0 +1,17 @@
{
"scanner": "aquasec/trivy@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969",
"scanner_version": "0.74.0",
"scanned_at": "2026-10-04T19:35:45.454792771Z",
"artifact": "flex-auth:wp0007-candidate",
"image_id": "sha256:f6ed8af69af766d7e9498465a23c292d91185b79352065736a2132216aac1d37",
"severities": [
"HIGH",
"CRITICAL"
],
"vulnerabilities": 0,
"full_report_sha256": "e41d19c961f0e0d3a4051362c6769edc3069b4286612239861818d403c16ee6a",
"warnings": [
"vendor severity selection"
],
"scope": "candidate artifact scan; not a general security attestation"
}

View file

@ -0,0 +1,232 @@
{
"valid": true,
"caring_findings": [
{
"code": "CARING-POLICY-MISSING-DIMENSION",
"severity": "warning",
"message": "policy package should declare governed canonical roles",
"fields": [
"caring.canonical_roles"
]
},
{
"code": "CARING-POLICY-MISSING-DIMENSION",
"severity": "warning",
"message": "policy package should declare governed organization relations",
"fields": [
"caring.organization_relations"
]
},
{
"code": "CARING-POLICY-MISSING-DIMENSION",
"severity": "warning",
"message": "policy package should declare governed scopes",
"fields": [
"caring.scopes"
]
},
{
"code": "CARING-POLICY-MISSING-DIMENSION",
"severity": "warning",
"message": "policy package should declare governed planes",
"fields": [
"caring.planes"
]
},
{
"code": "CARING-POLICY-MISSING-DIMENSION",
"severity": "warning",
"message": "policy package should declare governed capabilities",
"fields": [
"caring.capabilities"
]
},
{
"code": "CARING-POLICY-MISSING-DIMENSION",
"severity": "warning",
"message": "policy package should declare governed exposure modes",
"fields": [
"caring.exposure_modes"
]
},
{
"code": "CARING-POLICY-MISSING-DIMENSION",
"severity": "warning",
"message": "policy package should declare governed conditions",
"fields": [
"caring.conditions"
]
},
{
"code": "CARING-POLICY-MISSING-DIMENSION",
"severity": "warning",
"message": "policy package should declare governed restrictions",
"fields": [
"caring.restrictions"
]
}
],
"tests": [
{
"name": "test_allow_does_not_prove_the_pin",
"passed": true
},
{
"name": "test_approval_id_malformed",
"passed": true
},
{
"name": "test_attribute_acts_are_eligible_only_when_sets_admit",
"passed": true
},
{
"name": "test_bad_memo_id_refused",
"passed": true
},
{
"name": "test_digest_mismatch",
"passed": true
},
{
"name": "test_exact_pin_mismatch_is_not_rescued",
"passed": true
},
{
"name": "test_exact_record_ignores_attribute_context",
"passed": true
},
{
"name": "test_future_mfa_is_identity_refused",
"passed": true
},
{
"name": "test_human_control_false",
"passed": true
},
{
"name": "test_initial_release_allows_only_decision_record_class",
"passed": true
},
{
"name": "test_initial_release_sets",
"passed": true
},
{
"name": "test_list_does_not_use_attribute_admission",
"passed": true
},
{
"name": "test_listable_is_exact_union",
"passed": true
},
{
"name": "test_memo_review_group_is_not_hardcoded",
"passed": true
},
{
"name": "test_memo_version_refused",
"passed": true
},
{
"name": "test_requester_mismatch",
"passed": true
},
{
"name": "test_review_group_must_be_verified",
"passed": true
},
{
"name": "test_service_subject_is_identity_refused",
"passed": true
},
{
"name": "test_six_exact_metered_memos_allow_human_review",
"passed": true
},
{
"name": "test_stale_mfa_on_attribute_branch",
"passed": true
},
{
"name": "test_t03_records_not_in_act_scope",
"passed": true
},
{
"name": "test_tool_proof_exact_record_allowed",
"passed": true
},
{
"name": "test_tool_proof_without_mfa_denied",
"passed": true
},
{
"name": "test_tool_proof_wrong_digest_denied",
"passed": true
},
{
"name": "test_tool_proof_wrong_native_id_denied",
"passed": true
},
{
"name": "test_unknown_class",
"passed": true
},
{
"name": "test_unknown_request_denied",
"passed": true
},
{
"name": "test_unknown_submitter",
"passed": true
}
],
"fixtures": [
{
"id": "unknown-request-denied",
"passed": true,
"expected": {
"effect": "deny"
},
"actual": {
"effect": "deny",
"reason": "compact_sitting_scope_or_identity_refused"
}
},
{
"id": "attribute-non-human-refused",
"passed": true,
"expected": {
"effect": "deny",
"reason": "identity_refused"
},
"actual": {
"effect": "deny",
"reason": "identity_refused"
}
},
{
"id": "attribute-stale-mfa",
"passed": true,
"expected": {
"effect": "deny",
"reason": "stale_mfa"
},
"actual": {
"effect": "deny",
"reason": "stale_mfa"
}
},
{
"id": "attribute-future-mfa",
"passed": true,
"expected": {
"effect": "deny",
"reason": "identity_refused"
},
"actual": {
"effect": "deny",
"reason": "identity_refused"
}
}
]
}

View file

@ -0,0 +1,17 @@
{
"scanner": "aquasec/trivy@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969",
"scanner_version": "0.74.0",
"scanned_at": "2026-10-04T19:30:26.955785485Z",
"artifact": "informed-decision:wp0007-candidate",
"image_id": "sha256:2768bd1da3e651cc0be2d0f1ae6eea1af4eb057b31ef63ebb03b004c2ed76c3d",
"severities": [
"HIGH",
"CRITICAL"
],
"vulnerabilities": 0,
"full_report_sha256": "86543b4d8850008ada40e10f99f3b26c0564a77cab5932f61115e574e21a2ee4",
"warnings": [
"Alpine 3.24 absent from scanner EOL list"
],
"scope": "candidate artifact scan; not a general security attestation"
}

View file

@ -0,0 +1,83 @@
# Initial production runtime candidate
INFD-WP-0007 / INFD-WP-0005. Founder selected net-kingdom-admins humans and
system:serviceaccount:state-hub:state-hub on 2026-10-04. This is a preparation
contract; deployment still requires the concrete rollout approval.
## Identity and listeners
The same process/store serves browser routes on 8080 and private workload routes
on 8081. Only the public listener is behind decisions.coulomb.social. A separate
ClusterIP and exact State Hub namespace AND app-label ingress expose 8081.
`/v1/decisions` uses a projected token with audience informed-decision-lifecycle;
`/v1/memos` uses a different token with audience informed-decision-intake. Neither
route accepts a browser cookie. Browser authoring retains KeyCape sessions,
CSRF and origin checking. The private listener exposes no browser routes.
Every workload request performs Kubernetes TokenReview over CA- and
hostname-verified HTTPS with a freshly read rotating reviewer credential. The
review service needs only create on tokenreviews, not token creation or Secret
read. The API-server audience is used for its reviewer token. Workload UID and
subject come from TokenReview; tenant comes from the reloadable exact-account
registry. JWT expiry is read only after TokenReview verifies that same token.
Callers expire within 30 seconds or token expiry, whichever is earlier.
The registry is identity registration, not an action permit. Every operation
still requires a fresh external PDP decision. The lifecycle PDP has a distinct
package, version and digest from human review. Only humans can bind via the
existing protected review flow. Access Engine keeps the authorization decision;
Approval Engine keeps authoritative approval state.
## Initial proposal data
`deploy/lifecycle/lifecycle-callers.json` names the exact State Hub account and
platform tenant. The intake registry permits only decision-record class and
net-kingdom-admins, with a proposed cap of 50 new attempts per UTC day. Replays
do not spend a second attempt; uncertain creation never automatically reposts.
The cap and routing are configuration, not per-decision deployments.
The review-group recipient is the existing platform-root KeyCape subject found
in previously bound review records. This is the initial single recipient; group
membership alone does not turn every admin into that bound principal. Expanding
to multiple human recipients needs a reviewed binding contract. The default
legacy registries remain empty; only the reviewed release ConfigMap mounts the
initial proposal. Directory group verification remains independent of this map.
Registry volumes use no subPath and reload on each request. Requester custody is
read-only at `/var/run/secrets/informed-decision/requester/client-secret`, matching
CCR-2026-0032. No secret value is copied into release files or SQLite. Kubernetes
Secret objects are not emitted by the renderer.
## Evidence and compatibility
Runtime options enable lifecycle audit delivery, intake audit/notification
delivery and verified intake metadata for human review. The latter reloads the
live Approval Engine object, checks its binding pin, requester informed-decision
and human_control=true before the PDP call. Store guards recheck retained intake
metadata at the mutation boundary. A valid-looking UUID/digest alone is not proof.
State Hub forwarding currently exists as a dev fixture, not a production worker.
The selected identity still needs projected lifecycle/intake tokens and an owner
runtime integration. State Hub notifications/write-back require admitted receiver
routes; `INFD_HUB_WRITEBACK` stays off in this candidate. Pending rows remain
explicit. These are live INFD-WP-0005 dependencies, not claimed release evidence.
The image migrates schema 2 to schema 5 on open. Existing records, presentations,
dispositions, submissions and outbox evidence must survive. Rollback to the old
image against the migrated database is forbidden: stop the new writer, retain all
new evidence, and restore a verified pre-cutover copy on a separate private volume
only under the reviewed recovery plan. An in-flight/unresolved human submission
must be reconciled, never retried or discarded to make rollback convenient.
## Remaining live gates
- CCR-2026-0032 requester projection: current namespace has only the audit
ExternalSecret; requester custody, native identity/path refusals and rotation
evidence remain with RPF-WP-0051.
- FLEX-WP-0033: publish scanned policy artifact; approve exact policy/RBAC/network
objects and native caller/subject allow/refusal tests.
- INFD-WP-0005: production State Hub forwarding/receiver and token projections.
- INFD-WP-0007-T06/T07: backup/restore, exact rollout approval, native human review
and independent audit receipts across two decisions and a revision with unchanged
release/configuration pins. Browser packet authoring and broader binding profiles
remain on T05; the supported review handoff profile is documented separately.

View file

@ -102,3 +102,9 @@ HTTP schemas with canonical URLs and request limits; service admission and
publication audience contracts; lifecycle audit receiver admission and readiness
accounting. Test fixtures exercise the real PolicyClient against synthetic policy
transport. They are not evidence of a deployed lifecycle policy grant.
Production candidate record facts also include `revision_by`, the immutable actor
of the requested revision. This lets its editor retain access to an unpublished
successor after refining a published decision. Original authorship remains
`registered_by`; both facts participate in the same transaction/read race guard.

View file

@ -7,7 +7,7 @@ import ipaddress
from http.client import HTTPException
from urllib.error import HTTPError, URLError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener
from urllib.request import HTTPRedirectHandler, HTTPSHandler, ProxyHandler, Request, build_opener
class TransportError(Exception):
@ -41,9 +41,9 @@ class _NoRedirect(HTTPRedirectHandler):
class JSONTransport:
"""No redirects or ambient proxy credentials; five-second request timeout."""
def __init__(self, *, allow_internal_http=False) -> None:
def __init__(self, *, allow_internal_http=False, ssl_context=None) -> None:
self.allow_internal_http = allow_internal_http
self._opener = build_opener(ProxyHandler({}), _NoRedirect())
self._opener = build_opener(ProxyHandler({}), _NoRedirect(), HTTPSHandler(context=ssl_context))
def request(self, method: str, url: str, *, headers=None, body=None) -> tuple[int, dict]:
parsed = urlsplit(url)

View file

@ -215,7 +215,11 @@ class LifecycleRepository:
published = db.execute("SELECT body FROM lifecycle_events WHERE tenant=? AND memo_id=? "
"AND version=? AND kind='published' ORDER BY sequence DESC LIMIT 1",
(tenant, memo_id, version if version is not None else row["current_version"])).fetchone()
revision = db.execute("SELECT actor FROM lifecycle_events WHERE tenant=? AND memo_id=? "
"AND version=? AND kind IN ('registered','revised') ORDER BY sequence DESC LIMIT 1",
(tenant, memo_id, version if version is not None else row["current_version"])).fetchone()
return {"registered": True, "registered_by": json.loads(row["actor"]),
"revision_by": json.loads(revision["actor"]) if revision else None,
"current_version": row["current_version"],
"published_audience": json.loads(published["body"])["audience"] if published else None}

View file

@ -57,9 +57,12 @@ def submitted_digest(request):
return "sha256:" + hashlib.sha256(encoded.encode()).hexdigest()
def build_request(session: HumanSession, memo, action, policy_version):
def build_request(session: HumanSession, memo, action, policy_version, *, intake_context=None):
if action not in ACTIONS:
raise ValueError("unsupported review action")
extra = {} if intake_context is None else dict(intake_context)
if extra and set(extra) != {"review_group", "memo_class", "intake_submitter", "approval_requester", "approval_human_control"}:
raise ValueError("exact intake policy context required")
return {"id": str(uuid.uuid4()), "tenant": "tenant:platform",
"subject": {"id": session.subject, "type": "human", "tenant": session.tenant.value,
"attributes": {"tenant_source": session.tenant.route.value,
@ -70,7 +73,7 @@ def build_request(session: HumanSession, memo, action, policy_version):
"resource": {"id": "memo:" + memo.id, "type": "decision-memo",
"system": "informed-decision", "tenant": "tenant:platform"},
"context": {"memo_version": memo.version, "approval_id": memo.approval_id,
"approval_binding_digest": memo.approval_binding_digest},
"approval_binding_digest": memo.approval_binding_digest, **extra},
"policy_version": policy_version}

View file

@ -53,9 +53,10 @@ class OverviewRow:
class ReviewController:
def __init__(self, store, policy, approval_factory, *, clock=time.time):
def __init__(self, store, policy, approval_factory, *, clock=time.time, intake_attributes=False):
self.store, self.policy, self.approval_factory = store, policy, approval_factory
self.clock = clock
self.intake_attributes = intake_attributes
def _session(self, session):
if not isinstance(session, HumanSession) or session.expires_at <= self.clock():
@ -80,7 +81,13 @@ class ReviewController:
def _authorize(self, session, memo, action):
self._memo(session, memo)
request = build_request(session, memo, action, self.policy.version)
context = self.store.intake_context(memo) if self.intake_attributes and action != 'list' else None
if context is not None:
current = self._approval(session, memo)
if (current['binding'].get('actor') != context['approval_requester']
or current['binding'].get('human_control') is not True):
raise ReviewError(409, 'binding_changed')
request = build_request(session, memo, action, self.policy.version, intake_context=context)
observation = self.policy.check(request)
policy_id = self.store.observe_policy(observation)
try:

View file

@ -119,6 +119,7 @@ class Runtime:
controller: ReviewController
pump: AuditPump
lifecycle: object | None = None
workload: object | None = None
@classmethod
def from_file(cls, filename):
@ -129,7 +130,7 @@ class Runtime:
or info.st_size > 16384):
raise ValueError("runtime configuration must be an owned private 0600 file")
data = json.loads(path.read_text())
if ((set(data) - {"ordinary_approval_ids", "intake_delivery", "lifecycle_policy", "lifecycle_delivery"}) != {"schema", "evidence_db", "policy", "approval_origin", "audit"}
if ((set(data) - {"ordinary_approval_ids", "intake_delivery", "lifecycle_policy", "lifecycle_delivery", "workload_api", "intake_attributes"}) != {"schema", "evidence_db", "policy", "approval_origin", "audit"}
or data["schema"] != "informed-decision.review-runtime.v1"
or set(data["policy"]) != {"origin", "package", "version", "package_digest", "caller_token_file"}
or set(data["audit"]) != {"origin", "sender_token_file"}):
@ -137,6 +138,9 @@ class Runtime:
lifecycle_delivery = data.get("lifecycle_delivery", False)
if type(lifecycle_delivery) is not bool:
raise ValueError("invalid lifecycle delivery setting")
intake_attributes = data.get('intake_attributes', False)
if type(intake_attributes) is not bool:
raise ValueError('invalid intake attributes setting')
ordinary = ordinary_review_ids(data.get("ordinary_approval_ids", []))
if not Path(data["evidence_db"]).is_absolute():
raise ValueError("absolute evidence database path required")
@ -168,7 +172,8 @@ class Runtime:
package_digest=configuration["package_digest"], allow_internal_http=True)
store = Store(data["evidence_db"])
controller = ReviewController(store, client, lambda session: ApprovalHTTPClient(
approval_origin, session, allow_internal_http=True, ordinary_approval_ids=tuple(ordinary)))
approval_origin, session, allow_internal_http=True, ordinary_approval_ids=tuple(ordinary)),
intake_attributes=intake_attributes)
worker = None
if hub:
from .intake_delivery import IntakeDeliveryWorker
@ -179,5 +184,9 @@ class Runtime:
from .lifecycle import LifecycleRepository
from .lifecycle_service import LifecycleService
lifecycle = LifecycleService(LifecycleRepository(store), lifecycle_policy)
workload = None
if 'workload_api' in data:
from .workload import compose_workload
workload = compose_workload(data['workload_api'], store, lifecycle, approval_origin, token_file)
return cls(controller, AuditPump(store, sink, intake_worker=worker,
lifecycle_delivery=lifecycle_delivery), lifecycle)
lifecycle_delivery=lifecycle_delivery), lifecycle, workload)

View file

@ -259,12 +259,36 @@ class Store:
if row is None or row["outcome"] != "allow" or row["usable_until"] <= time.time():
raise Conflict("a current recorded policy observation is required")
request = json.loads(row["request"])
expected = {"memo_version": memo.version, "approval_id": memo.approval_id,
"approval_binding_digest": memo.approval_binding_digest}
if set(request['context']) - expected.keys():
context = self._intake_context(db, memo)
if context is None:
raise Conflict("intake policy observation has no ready receipt")
expected.update(context)
if (request["action"] != action or request["subject"]["id"] != subject
or request["resource"]["id"] != "memo:" + memo.id
or request["context"] != {"memo_version": memo.version, "approval_id": memo.approval_id,
"approval_binding_digest": memo.approval_binding_digest}):
or request["context"] != expected):
raise Conflict("policy observation does not name this act")
@staticmethod
def _intake_context(db, memo):
row = db.execute("SELECT submitter,metadata,receipt FROM intake_ledger WHERE memo_id=? AND state='ready'",
(memo.id,)).fetchone()
if row is None:
return None
receipt, metadata = json.loads(row['receipt']), json.loads(row['metadata'])
if (receipt['approval_id'] != memo.approval_id
or receipt['approval_binding_digest'] != memo.approval_binding_digest):
raise Conflict('intake receipt does not match memo')
return {'review_group': metadata['review_group'], 'memo_class': metadata['memo_class'],
'intake_submitter': row['submitter'], 'approval_requester': 'informed-decision',
'approval_human_control': True}
def intake_context(self, memo):
with self._connection() as db:
return self._intake_context(db, memo)
def _presentation(self, db, presentation_id):
row = db.execute("SELECT body FROM presentations WHERE id=?", (presentation_id,)).fetchone()
if row is None:

View file

@ -263,7 +263,25 @@ def main():
readiness=runtime.pump.ready if runtime else lambda: False,
lifecycle=runtime.lifecycle if runtime else None)
try:
serve(app, host=host, port=8080, threads=4)
if runtime and runtime.workload:
serve_with_workload(app, runtime.workload, host=host)
else:
serve(app, host=host, port=8080, threads=4)
finally:
if runtime:
runtime.pump.stop()
def serve_with_workload(public, private, *, host, public_port=8080, private_port=8081):
"""Two sockets, one writer/process; the private app has no browser routes."""
from waitress import create_server
servers, socket_map = [], {}
try:
servers.append(create_server(public, host=host, port=public_port, threads=4, map=socket_map))
servers.append(create_server(private, host=host, port=private_port, threads=4, map=socket_map,
max_request_body_size=1024 * 1024))
servers[0].run()
finally:
for server in servers:
server.close()
server.task_dispatcher.shutdown()

View file

@ -0,0 +1,133 @@
"""Private workload composition. Kubernetes verifies identity; PDP grants actions."""
import json
import math
from pathlib import Path
import re
import ssl
import time
import jwt
from .disposition import Actor, ActorKind
from .http_transport import JSONTransport, https_origin
from .intake_service import IntakeError
from .lifecycle import Attribution
from .lifecycle_http import LifecycleAPI
from .lifecycle_service import Caller
from .records import dumps
LIFECYCLE_AUDIENCE = 'informed-decision-lifecycle'
SA = re.compile(r'system:serviceaccount:[a-z0-9-]+:[a-z0-9-]+')
def registry(path):
with Path(path).open('rb') as handle:
raw = handle.read(65537)
if len(raw) > 65536:
raise ValueError('registry too large')
result = json.loads(raw)
if (not isinstance(result, dict) or any(not SA.fullmatch(name) or
not isinstance(row, dict) or set(row) != {'tenant'} or row['tenant'] != 'tenant:platform'
for name, row in result.items())):
raise ValueError('invalid workload registry')
return result
class KubernetesTokenReview:
def __init__(self, origin, ca_file, token_provider, *, transport_factory=JSONTransport):
self.origin = https_origin(origin)
self.ca_file = Path(ca_file)
if not self.ca_file.is_absolute():
raise ValueError('absolute Kubernetes CA path required')
self.token_provider = token_provider
self.transport_factory = transport_factory
def __call__(self, body):
try:
# Reopen projected CA/caller custody; require TLS peer and hostname validation.
context = ssl.create_default_context(cafile=str(self.ca_file))
transport = self.transport_factory(ssl_context=context)
status, result = transport.request('POST', self.origin + '/apis/authentication.k8s.io/v1/tokenreviews',
headers={'Authorization': 'Bearer ' + self.token_provider(), 'Content-Type': 'application/json'},
body=dumps(body).encode())
if status not in (200, 201):
raise ValueError()
return result
except Exception:
raise IntakeError(503, 'workload_identity_unavailable') from None
class LifecycleWorkloadIdentity:
def __init__(self, review, callers_file, *, clock=time.time):
self.review, self.callers_file, self.clock = review, Path(callers_file), clock
if not self.callers_file.is_absolute():
raise ValueError('absolute workload registry path required')
def __call__(self, token):
if not isinstance(token, str) or not 1 <= len(token) <= 16384:
raise ValueError('invalid workload token')
result = self.review({'apiVersion': 'authentication.k8s.io/v1', 'kind': 'TokenReview',
'spec': {'token': token, 'audiences': [LIFECYCLE_AUDIENCE]}})
status = result['status']
user = status['user']
if (status.get('authenticated') is not True or status.get('error')
or LIFECYCLE_AUDIENCE not in status.get('audiences', [])
or not isinstance(user.get('username'), str) or not SA.fullmatch(user['username'])
or not isinstance(user.get('uid'), str) or not user['uid']):
raise ValueError('workload authentication refused')
admitted = registry(self.callers_file).get(user['username'])
if admitted is None:
raise ValueError('workload not admitted')
# The API server has verified this Kubernetes JWT. Read only its expiry;
# subject/UID come from TokenReview and tenant from reviewed registration.
claims = jwt.decode(token, options={'verify_signature': False})
expiry = claims.get('exp')
if type(expiry) is not int or not math.isfinite(expiry) or expiry <= self.clock():
raise ValueError('workload token expired')
return Caller(Attribution(admitted['tenant'], Actor(user['username'], ActorKind.SERVICE)),
min(expiry, self.clock() + 30), dumps({
'principal_type_source': 'authentication-derived',
'tenant_source': 'registration-supplied',
'workload_uid': user['uid'], 'authentication_audience': LIFECYCLE_AUDIENCE}))
class PrivateWorkloadApp:
def __init__(self, *, intake=None, lifecycle=None):
self.intake, self.lifecycle = intake, lifecycle
def __call__(self, environ, start_response):
path = environ.get('PATH_INFO', '')
if path == '/v1/memos' and self.intake is not None:
return self.intake(environ, start_response)
if (path == '/v1/decisions' or path.startswith('/v1/decisions/')) and self.lifecycle is not None:
return self.lifecycle(environ, start_response)
raw = b'{"error":"route_not_found"}'
start_response('404 Not Found', [('Content-Type', 'application/json'), ('Cache-Control', 'no-store'),
('Content-Length', str(len(raw)))])
return [raw]
def compose_workload(data, store, lifecycle, approval_origin, token_reader):
if (not isinstance(data, dict) or set(data) - {'token_review', 'intake', 'lifecycle_callers_file'}
or 'token_review' not in data or not ({'intake', 'lifecycle_callers_file'} & data.keys())):
raise ValueError('invalid private workload configuration')
review = data['token_review']
if not isinstance(review, dict) or set(review) != {'origin', 'ca_file', 'token_file'}:
raise ValueError('exact TokenReview configuration required')
verifier = KubernetesTokenReview(review['origin'], review['ca_file'], token_reader(review['token_file']))
intake, api = None, None
if 'intake' in data:
from .requester import compose_intake
configuration = data['intake']
if not isinstance(configuration, dict) or set(configuration) != {'secret_file', 'submitters_file', 'groups_file'}:
raise ValueError('invalid private intake configuration')
if any(not isinstance(p, str) or not Path(p).is_absolute() for p in configuration.values()):
raise ValueError('absolute intake projection paths required')
intake = compose_intake(store, verifier, issuer='https://kc.coulomb.social',
secret_file=configuration['secret_file'], approval_origin=approval_origin,
submitters_path=configuration['submitters_file'], groups_path=configuration['groups_file'])
if 'lifecycle_callers_file' in data:
if lifecycle is None:
raise ValueError('private lifecycle API requires a separately pinned policy')
api = LifecycleAPI(lifecycle, LifecycleWorkloadIdentity(verifier, data['lifecycle_callers_file']))
return PrivateWorkloadApp(intake=intake, lifecycle=api)

View file

@ -3,7 +3,7 @@ name = "informed-decision"
version = "0.2.0"
description = "Presentation and binding surface for decisions — the Decision Memo and its evidence."
requires-python = ">=3.11"
dependencies = ["PyJWT[crypto]>=2.10,<3", "waitress>=3,<4"]
dependencies = ["PyJWT[crypto]>=2.14,<3", "waitress>=3,<4"]
[project.scripts]
informed-decision-web = "informed_decision.web:main"

View file

@ -1,15 +1,13 @@
# 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 \
# uv export --no-dev --no-emit-project --format requirements-txt --output-file requirements.lock
cffi==2.1.1 ; platform_python_implementation != 'PyPy' \
--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 \
@ -25,9 +23,7 @@ cffi==2.1.1 \
--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 \
@ -38,8 +34,6 @@ cffi==2.1.1 \
--hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \
--hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \
--hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \
--hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \
--hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \
--hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \
--hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \
--hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \
@ -50,20 +44,16 @@ cffi==2.1.1 \
--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 \
@ -74,13 +64,11 @@ cffi==2.1.1 \
--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 \
@ -101,7 +89,6 @@ cffi==2.1.1 \
--hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \
--hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \
--hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264
# via cryptography
cryptography==50.0.1 \
--hash=sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71 \
--hash=sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23 \
@ -149,16 +136,12 @@ cryptography==50.0.1 \
--hash=sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80 \
--hash=sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558 \
--hash=sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef
# via pyjwt
pycparser==3.0 \
pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' \
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
# via cffi
pyjwt==2.13.0 \
--hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \
--hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728
# via informed-decision (pyproject.toml)
pyjwt==2.15.1 \
--hash=sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193 \
--hash=sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8
waitress==3.0.2 \
--hash=sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f \
--hash=sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e
# via informed-decision (pyproject.toml)

View file

@ -26,7 +26,7 @@ FIXTURE = Path(__file__).parent / 'fixtures/review-policy'
@contextmanager
def flex_component(binary, directory, signing_key):
def flex_component(binary, directory, signing_key, *, fixture=FIXTURE):
tokens={'synthetic-workload-token':'system:serviceaccount:informed-decision:review',
'synthetic-wrong-caller':'system:serviceaccount:other:other'}
calls=[]
@ -55,7 +55,7 @@ def flex_component(binary, directory, signing_key):
listener.bind(('127.0.0.1',0));port=listener.getsockname()[1]
origin=f'http://127.0.0.1:{port}'
process=subprocess.Popen([str(binary),'serve','--addr',f'127.0.0.1:{port}',
'--registry',str(FIXTURE/'registry.json'),'--policy',str(FIXTURE/'policy.md'),
'--registry',str(fixture/'registry.json'),'--policy',str(fixture/'policy.md'),
'--caller-auth-mode','enforce','--caller-binding','informed-decision=system:serviceaccount:informed-decision:review',
'--caller-kubernetes-url',f'http://127.0.0.1:{server.server_port}',
'--caller-reviewer-token-file',str(reviewer),'--caller-ca-file',str(ca)],stdout=subprocess.DEVNULL,stderr=subprocess.PIPE)
@ -69,7 +69,7 @@ def flex_component(binary, directory, signing_key):
except TransportError:pass
time.sleep(.05)
else:raise RuntimeError('fixture evaluator did not start')
pin=json.loads((FIXTURE/'pin.json').read_text());pin.pop('fixture_only')
pin=json.loads((fixture/'pin.json').read_text());pin.pop('fixture_only', None)
client=PolicyClient(origin,lambda:'synthetic-workload-token',allow_internal_http=True,**pin)
yield client,calls
finally:

View file

@ -0,0 +1,15 @@
"""Prevent a tested dependency and its shipped container pin from diverging."""
from pathlib import Path
import re
import tomllib
def test_container_dependencies_match_project_lock():
root = Path(__file__).resolve().parents[1]
lock = tomllib.loads((root/'uv.lock').read_text())
locked = {p['name'].lower(): p['version'] for p in lock['package']}
requirements = dict(re.findall(r'^([a-zA-Z0-9_-]+)==([^ ;\\\n]+)',
(root/'requirements.lock').read_text(), re.MULTILINE))
assert requirements
for name, version in requirements.items():
assert locked[name.lower()] == version, name

View file

@ -97,3 +97,53 @@ def test_optional_workspace_requires_distinct_pins_and_exact_policy_network_peer
data['lifecycle_policy']['package'] = data['policy']['package']
with pytest.raises(ValueError):
module.render(data)
def workload_inputs():
data = inputs()
data['lifecycle_policy'] = {**data['policy'], 'package': 'informed-decision.lifecycle', 'pod_name': 'flex-auth-lifecycle'}
data['lifecycle_delivery'] = True
data['intake_attributes'] = True
data['intake_delivery'] = {'hub_origin': 'http://state-hub.state-hub.svc.cluster.local:8000'}
data['workload_api'] = {'intake': True, 'lifecycle': True,
'registries_configmap': 'informed-decision-workload-registry',
'peers': [{'namespace': 'state-hub', 'app': 'state-hub'}],
'api_egress': [{'ip': '10.43.0.1', 'port': 443}, {'ip': '92.205.62.239', 'port': 6443}]}
return data
def test_private_workload_candidate_has_exact_rbac_peers_and_projection():
objects = module.render(workload_inputs())['items']
services = [o for o in objects if o['kind'] == 'Service']
assert len(services) == 2
assert services[0]['spec']['ports'][0]['port'] == 80
assert services[1]['spec']['ports'][0]['port'] == 8081
assert all(o['kind'] not in ('Secret', 'Ingress') for o in objects)
role = next(o for o in objects if o['kind'] == 'ClusterRole')
assert role['rules'] == [{'apiGroups':['authentication.k8s.io'],'resources':['tokenreviews'],'verbs':['create']}]
binding = next(o for o in objects if o['kind'] == 'ClusterRoleBinding')
assert binding['subjects'] == [{'kind':'ServiceAccount','name':'review','namespace':'informed-decision'}]
policy = next(o for o in objects if o['kind']=='NetworkPolicy' and o['metadata']['name']=='informed-decision-review')
rule = policy['spec']['ingress'][-1]
assert rule['ports'] == [{'protocol':'TCP','port':8081}]
assert rule['from'][0]['namespaceSelector']['matchLabels'] == {'kubernetes.io/metadata.name':'state-hub'}
pod = next(o for o in objects if o['kind']=='Deployment')['spec']['template']['spec']
registry = next(m for m in pod['containers'][0]['volumeMounts'] if m['name']=='workload-registry')
assert registry['readOnly'] and 'subPath' not in registry
requester = next(v for v in pod['volumes'] if v['name']=='sitting-requester')
assert requester['secret']['secretName'] == 'informed-decision-sitting-requester'
assert requester['secret']['defaultMode'] == 0o440
config = json.loads(next(o for o in objects if o['kind']=='ConfigMap')['data']['runtime.json'])
assert config['workload_api']['token_review']['origin'] == 'https://kubernetes.default.svc:443'
@pytest.mark.parametrize('fault', ['wildcard','missing_delivery','no_policy','broad_api','false_type','empty_peers'])
def test_private_workload_candidate_refuses_incomplete_or_broad_configuration(fault):
data = workload_inputs()
if fault == 'wildcard': data['workload_api']['peers'][0]['app']='*'
if fault == 'missing_delivery': del data['intake_delivery']
if fault == 'no_policy': del data['lifecycle_policy']
if fault == 'broad_api': data['workload_api']['api_egress'][0]['ip']='10.0.0.0/8'
if fault == 'false_type': data['workload_api']['intake']='true'
if fault == 'empty_peers': data['workload_api']['peers']=[]
with pytest.raises(ValueError): module.render(data)

View file

@ -0,0 +1,56 @@
"""Real candidate Access Engine policy; authentication custody remains synthetic."""
from dataclasses import replace
import os
from pathlib import Path
import time
import pytest
from informed_decision.disposition import Actor, ActorKind
from informed_decision.lifecycle import Attribution, LifecycleRepository
from informed_decision.lifecycle_service import Caller, LifecycleError, LifecycleService
from informed_decision.records import dumps
from informed_decision.store import Store
from review_components import flex_component
from test_browser_auth import signing_key
from test_lifecycle import workspace
from test_policy_client import human
def test_initial_audience_collaborates_across_revisions_with_real_pdp(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'
pins = (fixture / 'pin.json').read_bytes()
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'}))
person = Caller.from_human_session(human())
app.register(hub, memo, key='first')
with pytest.raises(LifecycleError):
app.retrieve(person, memo.id)
app.publish(hub, memo.id, 1, audience={'groups': ['net-kingdom-admins'], 'subjects': []}, key='publish')
app.annotate(person, memo.id, 1, kind='comment', body={'text': 'Consider the alternative'}, key='comment')
app.revise(person, memo.next_version(brief='Alternative assessed'), expected_version=1, summary='Refined', key='refine')
# The successor is unpublished; its editor and original author retain access.
assert app.retrieve(person, memo.id).version == 2
assert app.retrieve(hub, memo.id).version == 2
other = Caller.from_human_session(replace(human(), subject='another-admin'))
with pytest.raises(LifecycleError):
app.retrieve(other, memo.id)
app.publish(person, memo.id, 2, audience={'groups': ['net-kingdom-admins'], 'subjects': []}, key='republish')
assert app.retrieve(other, memo.id).version == 2
app.register(hub, replace(memo, id='second-decision'), key='second')
reopened = LifecycleService(LifecycleRepository(Store(repo.store.path)), pdp)
assert reopened.retrieve(person, memo.id).version == 2
foreign = replace(hub, attribution=Attribution('tenant:platform', Actor('system:serviceaccount:other:other', ActorKind.SERVICE)))
with pytest.raises(LifecycleError):
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

View file

@ -186,8 +186,11 @@ def test_actual_engine_human_entry_and_independent_audit_link_to_source_revision
status, loaded = request(intake, receipt['envelope'])
assert status == 201, loaded
controller = ReviewController(app.repository.store, policy(),
lambda session: ApprovalHTTPClient('https://approval.test', session, transport=transport))
lambda session: ApprovalHTTPClient('https://approval.test', session, transport=transport), intake_attributes=True)
page = controller.open(human, receipt['review_memo_id'])
context = controller.policy.transport.calls[-1][3]['context']
assert context['intake_submitter'] == 'state-hub'
assert context['approval_requester'] == 'informed-decision' and context['approval_human_control'] is True
controller.acknowledge(human, page.presentation.id, memo.required_ack_ids)
controller.act(human, page.presentation.id, Verb.ACCEPT, operation_id='human-accept')
result = app.outcomes(actor, memo.id, 2)
@ -278,3 +281,41 @@ def test_schema_four_upgrade_preserves_evidence_and_fences_old_readers(handoff):
assert app.events(actor, memo.id) == before
receipt = prepare(handoff)
assert prepare(handoff) == receipt
def test_attribute_review_checks_live_requester_before_policy(handoff):
from informed_decision.review import ReviewController, ReviewError
from test_policy_client import human
app, actor, memo, options, intake, calls = handoff
receipt = prepare(handoff)
assert request(intake, receipt['envelope'])[0] == 201
stored = app.repository.store.memo(receipt['review_memo_id'])
session = replace(human(), subject=stored.binding.principal.id)
class Engine:
def get_approval(self, approval_id):
return {'binding': {'digest': stored.approval_binding_digest, 'actor': 'wrong-requester', 'human_control': True}}
controller = ReviewController(app.repository.store, policy(), lambda session: Engine(), intake_attributes=True)
with pytest.raises(ReviewError, match='binding_changed'):
controller.open(session, stored.id)
assert controller.policy.transport.calls == []
def test_intake_policy_guard_rechecks_retained_metadata(handoff):
from informed_decision.policy import build_request
from test_policy_client import human
app, actor, memo, options, intake, calls = handoff
receipt = prepare(handoff)
assert request(intake, receipt['envelope'])[0] == 201
store = app.repository.store
review = store.memo(receipt['review_memo_id'])
session = replace(human(), subject=review.binding.principal.id)
request_body = build_request(session, review, 'read', 'v1', intake_context=store.intake_context(review))
observation = policy().check(request_body)
policy_id = store.observe_policy(observation)
with store._transaction() as db:
row = db.execute('SELECT metadata FROM intake_ledger WHERE memo_id=?', (review.id,)).fetchone()
metadata = json.loads(row['metadata']); metadata['review_group'] = 'changed'
db.execute('UPDATE intake_ledger SET metadata=? WHERE memo_id=?', (json.dumps(metadata), review.id))
with pytest.raises(Conflict, match='policy observation'):
store.present(review.id, principal_sub=session.subject, tenant=session.tenant,
principal_type=session.principal_type, policy_id=policy_id)

View file

@ -0,0 +1,191 @@
import io
import json
import ssl
import time
import jwt
import pytest
from informed_decision.disposition import ActorKind
from informed_decision.intake_service import IntakeError, AUDIENCE
from informed_decision.workload import KubernetesTokenReview, LifecycleWorkloadIdentity, LIFECYCLE_AUDIENCE, PrivateWorkloadApp
from informed_decision.runtime import Runtime
from test_review_runtime import config
SA = 'system:serviceaccount:state-hub:state-hub'
def verified(body):
return {'status': {'authenticated': True, 'audiences': body['spec']['audiences'],
'user': {'username': SA, 'uid': 'actual-sa-uid'}}}
def token(exp=None):
return jwt.encode({'exp': int(time.time())+300 if exp is None else exp}, 'synthetic-test-key-with-at-least-thirty-two-bytes', algorithm='HS256')
def test_workload_identity_uses_verified_subject_and_separate_audience(tmp_path):
path = tmp_path/'callers.json'
path.write_text(json.dumps({SA: {'tenant': 'tenant:platform'}}))
requests = []
def review(body):
requests.append(body)
return verified(body)
identity = LifecycleWorkloadIdentity(review, path)
caller = identity(token())
assert requests[0]['spec']['audiences'] == [LIFECYCLE_AUDIENCE]
assert caller.attribution.actor.sub == SA and caller.attribution.actor.kind is ActorKind.SERVICE
assert caller.expires_at <= time.time()+30
assert json.loads(caller.attributes_json)['workload_uid'] == 'actual-sa-uid'
assert 'token' not in caller.attributes_json
path.write_text('{}')
with pytest.raises(ValueError):
identity(token())
@pytest.mark.parametrize('fault', ['wrong_audience', 'unauthenticated', 'human', 'missing_uid', 'expired', 'no_expiry', 'tenant'])
def test_workload_identity_fails_closed(tmp_path, fault):
path = tmp_path/'callers.json'
path.write_text(json.dumps({SA: {'tenant': 'tenant:other' if fault == 'tenant' else 'tenant:platform'}}))
def review(body):
result = verified(body)
if fault == 'wrong_audience': result['status']['audiences'] = [AUDIENCE]
if fault == 'unauthenticated': result['status']['authenticated'] = False
if fault == 'human': result['status']['user']['username'] = 'human'
if fault == 'missing_uid': result['status']['user']['uid'] = ''
return result
value = token(int(time.time())-1) if fault == 'expired' else token()
if fault == 'no_expiry': value = jwt.encode({}, 'synthetic-test-key-with-at-least-thirty-two-bytes', algorithm='HS256')
with pytest.raises(ValueError):
LifecycleWorkloadIdentity(review, path)(value)
def test_tokenreview_uses_owned_ca_rotating_caller_and_bounded_transport(tmp_path, monkeypatch):
seen = []
sentinel = ssl.create_default_context()
def context(*, cafile):
assert cafile == str(tmp_path/'ca.crt')
return sentinel
monkeypatch.setattr(ssl, 'create_default_context', context)
current = ['synthetic-caller-one']
class Transport:
def __init__(self, **kwargs):
assert kwargs['ssl_context'] is sentinel
def request(self, method, url, **kwargs):
seen.append((method, url, kwargs))
return 201, verified(json.loads(kwargs['body']))
review = KubernetesTokenReview('https://kubernetes.default.svc:443', tmp_path/'ca.crt',
lambda: current[0], transport_factory=Transport)
body = {'spec': {'token': 'synthetic-submitter', 'audiences': [AUDIENCE]}}
assert review(body)['status']['authenticated']
current[0] = 'synthetic-caller-two'
review(body)
assert seen[0][2]['headers']['Authorization'] != seen[1][2]['headers']['Authorization']
assert seen[0][1] == 'https://kubernetes.default.svc:443/apis/authentication.k8s.io/v1/tokenreviews'
def fail(**kwargs): raise OSError('private-path-or-response')
monkeypatch.setattr(ssl, 'create_default_context', fail)
with pytest.raises(IntakeError, match='workload_identity_unavailable') as error:
review(body)
assert 'private-path' not in str(error.value)
def test_explicit_runtime_configuration_does_not_read_custody_or_start_listener(tmp_path):
path, data = config(tmp_path)
data['lifecycle_policy'] = {**data['policy'], 'package': 'informed-decision.lifecycle'}
data['workload_api'] = {'token_review': {'origin': 'https://kubernetes.default.svc:443',
'ca_file': str(tmp_path/'absent-ca'), 'token_file': str(tmp_path/'absent-token')},
'lifecycle_callers_file': str(tmp_path/'absent-callers'),
'intake': {'secret_file': str(tmp_path/'absent-secret'), 'submitters_file': str(tmp_path/'absent-submitters'),
'groups_file': str(tmp_path/'absent-groups')}}
data['intake_attributes'] = True
path.write_text(json.dumps(data))
runtime = Runtime.from_file(path)
assert isinstance(runtime.workload, PrivateWorkloadApp)
assert runtime.workload.intake.store is runtime.controller.store
assert runtime.workload.lifecycle.service is runtime.lifecycle
assert runtime.controller.intake_attributes
del data['lifecycle_policy']
path.write_text(json.dumps(data))
with pytest.raises(ValueError): Runtime.from_file(path)
def test_private_router_does_not_expose_browser_or_unknown_routes():
calls = []
def app(environ, start):
calls.append(environ['PATH_INFO']); start('200 OK', []); return [b'ok']
router = PrivateWorkloadApp(intake=app, lifecycle=app)
for path in ('/v1/memos', '/v1/decisions', '/v1/decisions/example/events'):
assert b''.join(router({'PATH_INFO': path}, lambda s, h: None)) == b'ok'
for path in ('/', '/auth/start', '/review', '/v1/decisionsevil'):
statuses = []
router({'PATH_INFO': path}, lambda s, h: statuses.append(s))
assert statuses == ['404 Not Found']
assert len(calls) == 3
def test_two_listener_startup_failure_closes_first_socket_and_dispatcher(monkeypatch):
import waitress
from informed_decision.web import serve_with_workload
calls = []
class Server:
def close(self): calls.append('close')
def shutdown(self): calls.append('shutdown')
@property
def task_dispatcher(self): return self
def create(app, **kwargs):
calls.append(kwargs)
if kwargs['port'] == 8081: raise OSError('occupied')
return Server()
monkeypatch.setattr(waitress, 'create_server', create)
with pytest.raises(OSError): serve_with_workload(object(), object(), host='127.0.0.1')
assert calls[0]['map'] is calls[1]['map']
assert calls[-2:] == ['close', 'shutdown']
def test_real_tls_tokenreview_requires_ca_and_hostname(tmp_path):
from datetime import datetime, timedelta, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import ipaddress
import threading
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
now = datetime.now(timezone.utc)
name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, 'synthetic-api')])
certificate = (x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key())
.serial_number(x509.random_serial_number()).not_valid_before(now-timedelta(minutes=1))
.not_valid_after(now+timedelta(hours=1))
.add_extension(x509.SubjectAlternativeName([x509.IPAddress(ipaddress.ip_address('127.0.0.1'))]), critical=False)
.sign(key, hashes.SHA256()))
ca, private_key = tmp_path/'ca.pem', tmp_path/'key.pem'
ca.write_bytes(certificate.public_bytes(serialization.Encoding.PEM))
private_key.write_bytes(key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8,
serialization.NoEncryption()))
received = []
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args): pass
def do_POST(self):
received.append(self.path)
assert self.headers['Authorization'] == 'Bearer synthetic-reviewer'
body = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
raw = json.dumps(verified(body)).encode()
self.send_response(201); self.end_headers(); self.wfile.write(raw)
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
tls = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
tls.load_cert_chain(ca, private_key)
server.socket = tls.wrap_socket(server.socket, server_side=True)
thread = threading.Thread(target=server.serve_forever, daemon=True); thread.start()
body = {'spec': {'token': 'synthetic-caller', 'audiences': [LIFECYCLE_AUDIENCE]}}
try:
origin = f'https://127.0.0.1:{server.server_port}'
review = KubernetesTokenReview(origin, ca, lambda: 'synthetic-reviewer')
assert review(body)['status']['authenticated']
wrong_host = KubernetesTokenReview(origin.replace('127.0.0.1', 'localhost'), ca, lambda: 'synthetic-reviewer')
with pytest.raises(IntakeError): wrong_host(body)
untrusted = KubernetesTokenReview(origin, '/etc/ssl/certs/ca-certificates.crt', lambda: 'synthetic-reviewer')
with pytest.raises(IntakeError): untrusted(body)
assert len(received) == 1
finally:
server.shutdown(); server.server_close(); thread.join(timeout=5)

View file

@ -24,8 +24,65 @@ def validate_policy(policy):
raise ValueError('exact policy package/version/digest required')
def validate_workload(value):
if (not isinstance(value,dict) or set(value)!={'registries_configmap','peers','api_egress','intake','lifecycle'}
or any(type(value[k]) is not bool for k in ('intake','lifecycle'))
or not (value['intake'] or value['lifecycle'])
or not re.fullmatch(r'[a-z0-9][a-z0-9.-]{0,62}',value['registries_configmap'])):
raise ValueError('exact workload listener configuration required')
for key in ('peers','api_egress'):
if not isinstance(value[key],list) or not 1<=len(value[key])<=16:
raise ValueError('bounded exact workload peers required')
for item in value['peers']:
if (not isinstance(item,dict) or set(item)!={'namespace','app'}
or any(not re.fullmatch(r'[a-z0-9][a-z0-9.-]{0,62}',x) for x in item.values())):
raise ValueError('exact workload namespace and pod label required')
for item in value['api_egress']:
if not isinstance(item,dict) or set(item)!={'ip','port'} or type(item['port']) is not int or item['port'] not in (443,6443):
raise ValueError('exact Kubernetes API endpoint required')
address=ipaddress.ip_address(item['ip'])
if address.is_loopback or address.is_unspecified or address.is_multicast:
raise ValueError('routable API address required')
def add_workload_objects(objects,pod,value,obj,readonly,peer):
container=pod['containers'][0]
container['ports'].append({'name':'workload','containerPort':8081})
container['volumeMounts'].extend([
readonly('kubernetes-api','/var/run/secrets/informed-decision/kubernetes'),
readonly('workload-registry','/workload-registry')])
pod['volumes'].extend([
{'name':'kubernetes-api','projected':{'defaultMode':0o440,'sources':[
{'serviceAccountToken':{'path':'token','expirationSeconds':600}},
{'configMap':{'name':'kube-root-ca.crt','items':[{'key':'ca.crt','path':'ca.crt'}]}}]}},
{'name':'workload-registry','configMap':{'name':value['registries_configmap'],'defaultMode':0o444}}])
if value['intake']:
container['volumeMounts'].append(readonly('sitting-requester','/var/run/secrets/informed-decision/requester'))
pod['volumes'].append({'name':'sitting-requester','secret':{
'secretName':'informed-decision-sitting-requester','defaultMode':0o440,'optional':False,
'items':[{'key':'client-secret','path':'client-secret'}]}})
objects.append(obj('v1','Service',NAME+'-workload',spec={'type':'ClusterIP',
'selector':{**LABEL,'app.kubernetes.io/component':'review'},
'ports':[{'name':'workload','port':8081,'targetPort':'workload','protocol':'TCP'}]}))
objects[5]['spec']['ingress'].append({'from':[peer(p['namespace'],p['app']) for p in value['peers']],
'ports':[{'protocol':'TCP','port':8081}]})
for endpoint in value['api_egress']:
address=ipaddress.ip_address(endpoint['ip'])
objects[5]['spec']['egress'].append({'to':[{'ipBlock':{'cidr':str(address)+'/'+str(address.max_prefixlen)}}],
'ports':[{'protocol':'TCP','port':endpoint['port']}]})
role=obj('rbac.authorization.k8s.io/v1','ClusterRole',NAME+'-tokenreview',rules=[
{'apiGroups':['authentication.k8s.io'],'resources':['tokenreviews'],'verbs':['create']}])
binding=obj('rbac.authorization.k8s.io/v1','ClusterRoleBinding',NAME+'-tokenreview',
roleRef={'apiGroup':'rbac.authorization.k8s.io','kind':'ClusterRole','name':NAME+'-tokenreview'},
subjects=[{'kind':'ServiceAccount','name':'review','namespace':NAME}])
for item in (role,binding):
item['metadata'].pop('namespace')
item['metadata']['annotations']['informed-decision.coulomb.social/admission']='proposal; INFD-WP-0007/INFD-WP-0005 owner review required'
objects.append(item)
def render(inputs):
if not isinstance(inputs,dict) or (set(inputs)-{'ordinary_approval_ids','lifecycle_policy','lifecycle_delivery'})!={'image','policy','keycape_egress_ips','storage_class'}:
if not isinstance(inputs,dict) or (set(inputs)-{'ordinary_approval_ids','lifecycle_policy','lifecycle_delivery','workload_api','intake_attributes','intake_delivery'})!={'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')
@ -67,6 +124,32 @@ def render(inputs):
raise ValueError('lifecycle delivery must be boolean')
config['lifecycle_delivery']=inputs['lifecycle_delivery']
if 'intake_attributes' in inputs:
if type(inputs['intake_attributes']) is not bool:
raise ValueError('intake attributes must be boolean')
config['intake_attributes']=inputs['intake_attributes']
if 'intake_delivery' in inputs:
if inputs['intake_delivery'] != {'hub_origin':'http://state-hub.state-hub.svc.cluster.local:8000'}:
raise ValueError('exact State Hub delivery origin required')
config['intake_delivery']=inputs['intake_delivery']
if 'workload_api' in inputs:
workload=inputs['workload_api']
validate_workload(workload)
if workload['lifecycle'] and (not lifecycle or inputs.get('lifecycle_delivery') is not True):
raise ValueError('workload lifecycle needs policy and evidence delivery')
if workload['intake'] and (inputs.get('intake_attributes') is not True or 'intake_delivery' not in inputs):
raise ValueError('private intake needs attribute and delivery composition')
config['workload_api']={'token_review':{'origin':'https://kubernetes.default.svc:443',
'ca_file':'/var/run/secrets/informed-decision/kubernetes/ca.crt',
'token_file':'/var/run/secrets/informed-decision/kubernetes/token'}}
if workload['lifecycle']:
config['workload_api']['lifecycle_callers_file']='/workload-registry/lifecycle-callers.json'
if workload['intake']:
config['workload_api']['intake']={
'secret_file':'/var/run/secrets/informed-decision/requester/client-secret',
'submitters_file':'/workload-registry/intake-submitters.json',
'groups_file':'/workload-registry/review-groups.json'}
raw=json.dumps(config,sort_keys=True,separators=(',',':'))
config_name=NAME+'-runtime-'+hashlib.sha256(raw.encode()).hexdigest()[:12]
def obj(api,kind,name,**values):
@ -124,6 +207,11 @@ def render(inputs):
# 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}]}]})]
if 'intake_delivery' in inputs:
objects[5]['spec']['egress'].append({'to':[peer('state-hub','state-hub')],
'ports':[{'protocol':'TCP','port':8000}]})
if 'workload_api' in inputs:
add_workload_objects(objects, pod, inputs['workload_api'], obj, readonly, peer)
# 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.

View file

@ -20,6 +20,7 @@ def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--image',required=True)
parser.add_argument('--receipt',type=Path,required=True)
parser.add_argument('--workload',action='store_true',help='exercise the separate private workload listener')
args=parser.parse_args()
prefix='infd-proof-'+uuid.uuid4().hex[:12]
volumes=[];containers=[];checks=[]
@ -89,6 +90,17 @@ assert s.submission(d.id)['state']=='unresolved'
'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'}}
if args.workload:
config['lifecycle_policy']={**config['policy'],'package':'container.lifecycle.fixture'}
config['lifecycle_delivery']=True
config['workload_api']={'token_review':{'origin':'https://127.0.0.1:18443',
'ca_file':'/configuration/absent-ca','token_file':'/configuration/caller-token'},
'lifecycle_callers_file':'/configuration/lifecycle-callers.json',
'intake':{'secret_file':'/configuration/absent-requester',
'submitters_file':'/configuration/intake-submitters.json',
'groups_file':'/configuration/review-groups.json'}}
for name in ('lifecycle-callers.json','intake-submitters.json','review-groups.json'):
(fixture/name).write_text('{}')
(fixture/'runtime.json').write_text(json.dumps(config))
for file in fixture.iterdir():file.chmod(0o444)
data=volume('data');backup=volume('backup')
@ -102,6 +114,10 @@ assert s.submission(d.id)['state']=='unresolved'
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')
if args.workload:
command(first,"import urllib.request,urllib.error\nfor port,path,status in [(8081,'/v1/decisions',401),(8081,'/v1/memos',405),(8081,'/review',404),(8080,'/v1/decisions',404)]:\n try: urllib.request.urlopen('http://127.0.0.1:'+str(port)+path)\n except urllib.error.HTTPError as e: assert e.code==status\n else: raise AssertionError(path)")
checks.append('private listener refuses anonymous lifecycle access and exposes no browser routes')
checks.append('public listener exposes no private workload route')
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)

8
uv.lock generated
View file

@ -181,7 +181,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "pyjwt", extras = ["crypto"], specifier = ">=2.10,<3" },
{ name = "pyjwt", extras = ["crypto"], specifier = ">=2.14,<3" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8" },
{ name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6,<7" },
{ name = "waitress", specifier = ">=3,<4" },
@ -234,11 +234,11 @@ wheels = [
[[package]]
name = "pyjwt"
version = "2.14.0"
version = "2.15.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/af/c3/8a3b59c25070cc61dc517fbdfa5dc0904670c96f605cc69759dc09166b99/pyjwt-2.14.0.tar.gz", hash = "sha256:77283c83fb56ecf566a886c757a714bc83668e38156de2cce8263302f42e0b86", size = 113177 }
sdist = { url = "https://files.pythonhosted.org/packages/43/ea/5194e52748b0da83d71e082d75496eaec6e58f419f5e184786ded517e6a9/pyjwt-2.15.1.tar.gz", hash = "sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8", size = 121252 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/97/672cb32ce0dfea44b740cb7b4f97038463b9cf7c0ead1aacf595572851d6/pyjwt-2.14.0-py3-none-any.whl", hash = "sha256:ad0cef71c756a56e74863c2919cf0985f72decbcfcb550ee2f422e7c62b5eedc", size = 32896 },
{ url = "https://files.pythonhosted.org/packages/50/ca/44de4e75f8aadc457f0634be3b542815078ded46dca30efb960edeecad6e/pyjwt-2.15.1-py3-none-any.whl", hash = "sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193", size = 33860 },
]
[package.optional-dependencies]

View file

@ -113,7 +113,7 @@ status: wait
priority: high
blocked_on: message-from:railiance-platform
needs_human: true
blocking_reason: "Founder-approved ESO binding/mount accepted for source integration; private listener and CA-verified TokenReview runtime composition, immutable image/config, native custody proof and separate live go remain"
blocking_reason: "Founder-approved ESO binding/mount accepted for source integration; private listener and CA-verified TokenReview runtime composed and tested; native custody proof and separate live go remain"
state_hub_task_id: "3b0f536c-e8ee-5493-b663-2f60f5f429ad"
decision_id: "0d6792b6-59e4-4fa0-887a-c4e51ea8e131"
```
@ -130,7 +130,7 @@ id: INFD-WP-0005-T06
status: wait
priority: high
blocked_on: message-from:access-engine
blocking_reason: "Attribute admission branch and generated submitter/class sets are access-engine's change (msg 306bcdd2); deploy needs founder go"
blocking_reason: "Consumer implemented; FLEX-WP-0033 prepares v12 State Hub / decision-record admission; native verification and concrete rollout approval remain"
state_hub_task_id: "c03016da-a795-5c5c-b86e-0ce9afc67be6"
```
@ -167,7 +167,7 @@ id: INFD-WP-0005-T08
status: wait
priority: medium
blocked_on: message-from:state-hub
blocking_reason: "state-hub must choose the actual SA and supply reviewed grant/runtime forwarding (msg e30dea31), alongside T05/T06"
blocking_reason: "Founder selected system:serviceaccount:state-hub:state-hub; production token projection and runtime forwarding remain, alongside T05/T06"
state_hub_task_id: "ce2b36b4-ef82-53b7-aee7-e0063855370b"
```
@ -259,3 +259,15 @@ private listener, CA-verified TokenReview transport, ESO TokenRequest check,
immutable owner image/config and native rotation/revocation proof remain.
T05 stays wait and the wider human decision stays open for the concrete live
packet. No live action, credential retrieval or issuer mutation occurred.
## Production runtime preparation — 2026-10-04
Private intake/lifecycle listeners, CA-verified TokenReview, rotating requester
projection composition and attribute-admission consumer checks are implemented.
Founder selected net-kingdom-admins humans and the State Hub workload. Initial
registries are review candidates under deploy/lifecycle; the empty legacy
registries and live runtime remain unchanged. T05/T06 remain waiting on native
custody and policy deployment; T08 still needs the State Hub production forwarder,
not merely its dev fixture. See docs/specs/LifecycleProductionRuntime.md and
FLEX-WP-0033 for exact boundaries.

View file

@ -249,3 +249,28 @@ Validation for this continuation: full cross-repository suite **634 passed,
5 skipped**; `git diff --check` passed. Schema-4 upgrade preserves existing
records and marks schema 5 to fence older readers. Implementation decision logged
as State Hub `ccb2e110-6032-4259-a402-989415a8d9fe` (not a rollout approval).
## First production audience and runtime candidate — 2026-10-04
Founder selected net-kingdom-admins humans and the State Hub workload. Added the
private 8081 composition with fresh CA-verified TokenReview and exact workload
registration, shared-store public/private serving and guarded attribute review.
A revision's editor now remains authorized to its unpublished successor through
immutable revision_by facts. Actual Access Engine HTTP integration proves this
with the separately pinned lifecycle policy (FLEX-WP-0033).
Published immutable service/policy candidates and retained the reviewable packet
at deploy/lifecycle/README.md. Both scans have zero HIGH/CRITICAL findings after
fixing service JWT pins and inherited Access Engine Go/OPA/telemetry dependencies.
All 25 Kubernetes objects passed server dry-run; 662 service/component tests pass
(1 skipped), as do 13 installed-container checks. Access Engine full regression
and candidate package validations pass. Tests are not native custody proof.
No live change or human binding was performed. T02–T06 remain in progress for
native contracts, State Hub production forwarding, browser packet/binding
profiles and complete live acceptance. T07 still requires concrete rollout go.
CCR-2026-0032 / RPF-WP-0051 owns requester delivery; INFD-WP-0005-T08 owns State
Hub forwarding/token projections. The exact chosen identity is no longer an open
question. Rollback must retain new evidence and must not run a schema-2 reader
on the migrated schema-5 database.