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:
parent
f3b71b1521
commit
4f546ae3b1
41 changed files with 2598 additions and 52 deletions
|
|
@ -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"]
|
||||
|
|
|
|||
12
SCOPE.md
12
SCOPE.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
96
deploy/lifecycle/README.md
Normal file
96
deploy/lifecycle/README.md
Normal 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.
|
||||
52
deploy/lifecycle/admission.candidate.json
Normal file
52
deploy/lifecycle/admission.candidate.json
Normal 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
|
||||
}
|
||||
}
|
||||
12
deploy/lifecycle/intake-submitters.json
Normal file
12
deploy/lifecycle/intake-submitters.json
Normal 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
|
||||
}
|
||||
}
|
||||
5
deploy/lifecycle/lifecycle-callers.json
Normal file
5
deploy/lifecycle/lifecycle-callers.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"system:serviceaccount:state-hub:state-hub": {
|
||||
"tenant": "tenant:platform"
|
||||
}
|
||||
}
|
||||
189
deploy/lifecycle/policy.candidate.yaml
Normal file
189
deploy/lifecycle/policy.candidate.yaml
Normal 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
|
||||
16
deploy/lifecycle/registries.candidate.json
Normal file
16
deploy/lifecycle/registries.candidate.json
Normal 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"
|
||||
}
|
||||
}
|
||||
6
deploy/lifecycle/review-groups.json
Normal file
6
deploy/lifecycle/review-groups.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"net-kingdom-admins": {
|
||||
"subject": "uid=platform-root,ou=people,dc=netkingdom,dc=local",
|
||||
"display_name": "Platform reviewer"
|
||||
}
|
||||
}
|
||||
189
deploy/lifecycle/review-policy.candidate.yaml
Normal file
189
deploy/lifecycle/review-policy.candidate.yaml
Normal 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
|
||||
800
deploy/lifecycle/service.candidate.json
Normal file
800
deploy/lifecycle/service.candidate.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
32
docs/evidence/2026-10-04-lifecycle-release/container.json
Normal file
32
docs/evidence/2026-10-04-lifecycle-release/container.json
Normal 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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
19
docs/evidence/2026-10-04-lifecycle-release/manifest.json
Normal file
19
docs/evidence/2026-10-04-lifecycle-release/manifest.json
Normal 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"
|
||||
}
|
||||
}
|
||||
17
docs/evidence/2026-10-04-lifecycle-release/policy-scan.json
Normal file
17
docs/evidence/2026-10-04-lifecycle-release/policy-scan.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
17
docs/evidence/2026-10-04-lifecycle-release/service-scan.json
Normal file
17
docs/evidence/2026-10-04-lifecycle-release/service-scan.json
Normal 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"
|
||||
}
|
||||
83
docs/specs/LifecycleProductionRuntime.md
Normal file
83
docs/specs/LifecycleProductionRuntime.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
133
informed_decision/workload.py
Normal file
133
informed_decision/workload.py
Normal 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)
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
15
tests/test_dependency_pins.py
Normal file
15
tests/test_dependency_pins.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
56
tests/test_lifecycle_policy_component.py
Normal file
56
tests/test_lifecycle_policy_component.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
191
tests/test_workload_runtime.py
Normal file
191
tests/test_workload_runtime.py
Normal 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)
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
8
uv.lock
generated
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue