From dd15d8cf3f383515982370c852e9fb7dd6f5eca3 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 11 Oct 2026 22:53:54 +0200 Subject: [PATCH] Prepare renamed Access Engine runtime twins with exact reviewer scope Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade --- charts/access-engine/Chart.yaml | 6 + charts/access-engine/templates/_helpers.tpl | 35 ++ .../access-engine/templates/deployment.yaml | 167 +++++++ .../templates/networkpolicy.yaml | 68 +++ .../access-engine/templates/outbox-pvc.yaml | 20 + charts/access-engine/templates/rbac.yaml | 30 ++ charts/access-engine/templates/service.yaml | 14 + .../templates/serviceaccount.yaml | 10 + charts/access-engine/values.yaml | 87 ++++ cmd/access-engine/readback_pin_test.go | 55 ++- deploy/access-engine-namespace.yaml | 8 + ...ess-runtime-twins-prepared-receipt-v2.json | 439 ++++++++++++++++++ ...access-runtime-twins-prepared-receipt.json | 190 ++++++++ tests/stage1.sh | 2 + tests/test_runtime_twins.py | 69 +++ tools/gen_runtime_twins.py | 49 ++ .../access-engine/application-readback.yaml | 34 ++ .../informed-decision-lifecycle.yaml | 30 ++ .../informed-decision-sitting.yaml | 30 ++ .../access-engine/informed-decision-t03.yaml | 30 ++ values/access-engine/ops-warden-fetch.yaml | 23 + values/access-engine/ops-warden.yaml | 21 + values/access-engine/secrets-engine.yaml | 23 + values/access-engine/tenant-engine.yaml | 23 + values/access-engine/user-engine.yaml | 23 + ...EX-WP-0036-runtime-rename-access-engine.md | 20 +- 26 files changed, 1480 insertions(+), 26 deletions(-) create mode 100644 charts/access-engine/Chart.yaml create mode 100644 charts/access-engine/templates/_helpers.tpl create mode 100644 charts/access-engine/templates/deployment.yaml create mode 100644 charts/access-engine/templates/networkpolicy.yaml create mode 100644 charts/access-engine/templates/outbox-pvc.yaml create mode 100644 charts/access-engine/templates/rbac.yaml create mode 100644 charts/access-engine/templates/service.yaml create mode 100644 charts/access-engine/templates/serviceaccount.yaml create mode 100644 charts/access-engine/values.yaml create mode 100644 deploy/access-engine-namespace.yaml create mode 100644 docs/evidence/2026-10-11-runtime-twins/access-runtime-twins-prepared-receipt-v2.json create mode 100644 docs/evidence/2026-10-11-runtime-twins/access-runtime-twins-prepared-receipt.json create mode 100644 tests/test_runtime_twins.py create mode 100644 tools/gen_runtime_twins.py create mode 100644 values/access-engine/application-readback.yaml create mode 100644 values/access-engine/informed-decision-lifecycle.yaml create mode 100644 values/access-engine/informed-decision-sitting.yaml create mode 100644 values/access-engine/informed-decision-t03.yaml create mode 100644 values/access-engine/ops-warden-fetch.yaml create mode 100644 values/access-engine/ops-warden.yaml create mode 100644 values/access-engine/secrets-engine.yaml create mode 100644 values/access-engine/tenant-engine.yaml create mode 100644 values/access-engine/user-engine.yaml diff --git a/charts/access-engine/Chart.yaml b/charts/access-engine/Chart.yaml new file mode 100644 index 0000000..7040921 --- /dev/null +++ b/charts/access-engine/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: access-engine +description: Independently rollable access-engine policy-decision Deployment (one consumer per release). +type: application +version: 0.2.0 +appVersion: "0.1.0" diff --git a/charts/access-engine/templates/_helpers.tpl b/charts/access-engine/templates/_helpers.tpl new file mode 100644 index 0000000..b9786b9 --- /dev/null +++ b/charts/access-engine/templates/_helpers.tpl @@ -0,0 +1,35 @@ +{{- define "access-engine.image" -}} +{{- if not .Values.image.digest }} +{{- fail "image.digest is required (digest_policy=required); do not deploy by tag" }} +{{- end }} +{{- printf "%s@%s" .Values.image.repository .Values.image.digest -}} +{{- end -}} + +{{- define "access-engine.name" -}} +{{- required "name is required" .Values.name -}} +{{- end -}} + +{{- define "access-engine.selectorLabels" -}} +app.kubernetes.io/name: {{ include "access-engine.name" . }} +{{- end -}} + +{{- define "access-engine.labels" -}} +{{ include "access-engine.selectorLabels" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/part-of: access-engine +{{- end -}} + +{{- define "access-engine.callerAuth.mode" -}} +{{- $mode := "disabled" -}} +{{- if and (hasKey .Values "callerAuth") .Values.callerAuth (hasKey .Values.callerAuth "mode") .Values.callerAuth.mode -}} +{{- $mode = .Values.callerAuth.mode -}} +{{- end -}} +{{- if not (has $mode (list "disabled" "warn" "enforce")) -}} +{{- fail (printf "callerAuth.mode must be disabled, warn, or enforce; got %q" $mode) -}} +{{- end -}} +{{- $mode -}} +{{- end -}} + +{{- define "access-engine.callerAuth.enabled" -}} +{{- if has (include "access-engine.callerAuth.mode" .) (list "warn" "enforce") -}}true{{- else -}}false{{- end -}} +{{- end -}} diff --git a/charts/access-engine/templates/deployment.yaml b/charts/access-engine/templates/deployment.yaml new file mode 100644 index 0000000..644b2fe --- /dev/null +++ b/charts/access-engine/templates/deployment.yaml @@ -0,0 +1,167 @@ +{{- if .Values.auditDelivery.enabled }} +{{- if ne (first .Values.args) "serve" }} +{{- fail "auditDelivery currently supports serve decision pins only" }} +{{- end }} +{{- if not .Values.outbox.enabled }} +{{- fail "auditDelivery requires enabled durable outbox" }} +{{- end }} +{{- if not (regexMatch "^https?://[^/?#@]+/v1/events$" .Values.auditDelivery.url) }} +{{- fail "auditDelivery.url must be an explicit http(s) /v1/events endpoint" }} +{{- end }} +{{- if or (not (hasPrefix "/" .Values.auditDelivery.mountPath)) (contains ".." .Values.auditDelivery.mountPath) }} +{{- fail "auditDelivery.mountPath must be an absolute normalized directory" }} +{{- end }} +{{- end }} +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: {{ .Release.Namespace }} + name: {{ include "access-engine.name" . }} + labels: + {{- include "access-engine.labels" . | nindent 4 }} +spec: + {{- if .Values.outbox.enabled }} + {{- if ne (int .Values.replicaCount) 1 }} + {{- fail "outbox requires replicaCount=1: one writer per source and PVC" }} + {{- end }} + strategy: + type: Recreate + {{- end }} + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "access-engine.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "access-engine.labels" . | nindent 8 }} + spec: + automountServiceAccountToken: false + {{- if eq (include "access-engine.callerAuth.enabled" .) "true" }} + serviceAccountName: {{ include "access-engine.name" . }} + {{- end }} + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: access-engine + image: {{ include "access-engine.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + {{- toYaml .Values.args | nindent 12 }} + {{- if .Values.outbox.enabled }} + - --outbox-dir + - {{ .Values.outbox.mountPath | quote }} + - --emission-source + - {{ required "outbox.source is required when outbox.enabled=true" .Values.outbox.source | quote }} + {{- end }} + {{- if .Values.auditDelivery.enabled }} + - --audit-url + - {{ .Values.auditDelivery.url | quote }} + - --audit-token-file + - {{ printf "%s/token" .Values.auditDelivery.mountPath | quote }} + {{- end }} + {{- if eq (include "access-engine.callerAuth.enabled" .) "true" }} + {{- $binding := "" }} + {{- if not (and .Values.readback .Values.readback.enabled) }} + {{- $binding = required "callerAuth.binding is required when callerAuth.mode is warn or enforce" .Values.callerAuth.binding }} + {{- end }} + - --caller-audience + - {{ required "callerAuth.audience is required" .Values.callerAuth.audience | quote }} + - --caller-reviewer-token-file + - {{ printf "%s/token" .Values.callerAuth.reviewer.mountPath | quote }} + - --caller-ca-file + - {{ printf "%s/ca.crt" .Values.callerAuth.reviewer.mountPath | quote }} + - --caller-auth-mode + - {{ include "access-engine.callerAuth.mode" . | quote }} + - --caller-kubernetes-url + - {{ required "callerAuth.kubernetesURL is required when callerAuth.mode is warn or enforce" .Values.callerAuth.kubernetesURL | quote }} + {{- if and .Values.readback .Values.readback.enabled }} + {{- /* serve-readback admits one readback caller and no /v1/check binding (FLEX-DEC-2026-019). */}} + - --readback-caller + - {{ required "readback.caller is required when readback.enabled=true" .Values.readback.caller | quote }} + {{- else }} + - --caller-binding + - {{ $binding | quote }} + {{- range .Values.callerAuth.extraBindings }} + - --caller-binding + - {{ . | quote }} + {{- end }} + {{- end }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + livenessProbe: + httpGet: + path: {{ .Values.health.path }} + port: http + periodSeconds: 20 + readinessProbe: + httpGet: + path: {{ .Values.health.path }} + port: http + periodSeconds: 5 + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + {{- if or .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "access-engine.callerAuth.enabled" .) "true") }} + volumeMounts: + {{- if .Values.auditDelivery.enabled }} + - mountPath: {{ .Values.auditDelivery.mountPath | quote }} + name: access-engine-audit + readOnly: true + {{- end }} + {{- if .Values.outbox.enabled }} + - mountPath: {{ .Values.outbox.mountPath | quote }} + name: access-engine-outbox + {{- end }} + {{- if eq (include "access-engine.callerAuth.enabled" .) "true" }} + - mountPath: {{ .Values.callerAuth.reviewer.mountPath }} + name: access-engine-reviewer + readOnly: true + {{- end }} + {{- end }} + {{- if or .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "access-engine.callerAuth.enabled" .) "true") }} + volumes: + {{- if .Values.auditDelivery.enabled }} + - name: access-engine-audit + secret: + secretName: {{ required "auditDelivery.secretName must name an admitted existing Secret" .Values.auditDelivery.secretName | quote }} + defaultMode: 0440 + items: + - key: {{ required "auditDelivery.secretKey is required" .Values.auditDelivery.secretKey | quote }} + path: token + {{- end }} + {{- if .Values.outbox.enabled }} + - name: access-engine-outbox + persistentVolumeClaim: + claimName: {{ default (printf "%s-outbox" (include "access-engine.name" .)) .Values.outbox.existingClaim | quote }} + {{- end }} + {{- if eq (include "access-engine.callerAuth.enabled" .) "true" }} + - name: access-engine-reviewer + projected: + defaultMode: 0440 + sources: + - serviceAccountToken: + {{- if .Values.callerAuth.reviewer.audience }} + audience: {{ .Values.callerAuth.reviewer.audience | quote }} + {{- end }} + expirationSeconds: {{ .Values.callerAuth.reviewer.expirationSeconds }} + path: token + - configMap: + name: kube-root-ca.crt + items: + - key: ca.crt + path: ca.crt + {{- end }} + {{- end }} diff --git a/charts/access-engine/templates/networkpolicy.yaml b/charts/access-engine/templates/networkpolicy.yaml new file mode 100644 index 0000000..7e33de4 --- /dev/null +++ b/charts/access-engine/templates/networkpolicy.yaml @@ -0,0 +1,68 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + namespace: {{ .Release.Namespace }} + name: {{ include "access-engine.name" . }} + labels: + {{- include "access-engine.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "access-engine.selectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + - Egress + {{- if or .Values.auditDelivery.enabled (eq (include "access-engine.callerAuth.enabled" .) "true") }} + egress: + {{- if eq (include "access-engine.callerAuth.enabled" .) "true" }} + - ports: + - port: 443 + protocol: TCP + - port: 6443 + protocol: TCP + {{- end }} + {{- if .Values.auditDelivery.enabled }} + {{- if or (not (regexMatch "^[1-9][0-9]*$" (toString .Values.auditDelivery.receiverPort))) (lt (int .Values.auditDelivery.receiverPort) 1) (gt (int .Values.auditDelivery.receiverPort) 65535) }} + {{- fail "auditDelivery.receiverPort must be explicitly set to a valid receiver port" }} + {{- end }} + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ required "auditDelivery.receiverNamespace is required" .Values.auditDelivery.receiverNamespace | quote }} + podSelector: + matchLabels: + app.kubernetes.io/name: {{ required "auditDelivery.receiverPodName is required" .Values.auditDelivery.receiverPodName | quote }} + ports: + - port: {{ .Values.auditDelivery.receiverPort }} + protocol: TCP + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ required "auditDelivery.dnsNamespace is required" .Values.auditDelivery.dnsNamespace | quote }} + podSelector: + matchLabels: + k8s-app: {{ required "auditDelivery.dnsPodLabel is required" .Values.auditDelivery.dnsPodLabel | quote }} + ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + {{- end }} + {{- else }} + egress: [] + {{- end }} + {{- if .Values.consumer.isolated }} + ingress: [] + {{- else }} + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ required "consumer.namespace is required when not isolated" .Values.consumer.namespace }} + podSelector: + matchLabels: + app.kubernetes.io/name: {{ required "consumer.podName is required when not isolated" .Values.consumer.podName }} + ports: + - port: {{ .Values.service.port }} + protocol: TCP + {{- end }} diff --git a/charts/access-engine/templates/outbox-pvc.yaml b/charts/access-engine/templates/outbox-pvc.yaml new file mode 100644 index 0000000..2eae8f6 --- /dev/null +++ b/charts/access-engine/templates/outbox-pvc.yaml @@ -0,0 +1,20 @@ +{{- if and .Values.outbox.enabled (not .Values.outbox.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + namespace: {{ .Release.Namespace }} + name: {{ include "access-engine.name" . }}-outbox + annotations: + helm.sh/resource-policy: keep + labels: + {{- include "access-engine.labels" . | nindent 4 }} +spec: + accessModes: + - ReadWriteOnce + {{- if .Values.outbox.storageClassName }} + storageClassName: {{ .Values.outbox.storageClassName | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.outbox.size | quote }} +{{- end }} diff --git a/charts/access-engine/templates/rbac.yaml b/charts/access-engine/templates/rbac.yaml new file mode 100644 index 0000000..52dc62c --- /dev/null +++ b/charts/access-engine/templates/rbac.yaml @@ -0,0 +1,30 @@ +{{- if eq (include "access-engine.callerAuth.enabled" .) "true" }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "access-engine.name" . }}-tokenreviewer + labels: + {{- include "access-engine.labels" . | nindent 4 }} +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "access-engine.name" . }}-tokenreviewer + labels: + {{- include "access-engine.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "access-engine.name" . }}-tokenreviewer +subjects: + - kind: ServiceAccount + name: {{ include "access-engine.name" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/charts/access-engine/templates/service.yaml b/charts/access-engine/templates/service.yaml new file mode 100644 index 0000000..ecc4402 --- /dev/null +++ b/charts/access-engine/templates/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + namespace: {{ .Release.Namespace }} + name: {{ include "access-engine.name" . }} + labels: + {{- include "access-engine.labels" . | nindent 4 }} +spec: + selector: + {{- include "access-engine.selectorLabels" . | nindent 4 }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http diff --git a/charts/access-engine/templates/serviceaccount.yaml b/charts/access-engine/templates/serviceaccount.yaml new file mode 100644 index 0000000..82b0c74 --- /dev/null +++ b/charts/access-engine/templates/serviceaccount.yaml @@ -0,0 +1,10 @@ +{{- if eq (include "access-engine.callerAuth.enabled" .) "true" }} +apiVersion: v1 +kind: ServiceAccount +metadata: + namespace: {{ .Release.Namespace }} + name: {{ include "access-engine.name" . }} + labels: + {{- include "access-engine.labels" . | nindent 4 }} +automountServiceAccountToken: false +{{- end }} diff --git a/charts/access-engine/values.yaml b/charts/access-engine/values.yaml new file mode 100644 index 0000000..20989a7 --- /dev/null +++ b/charts/access-engine/values.yaml @@ -0,0 +1,87 @@ +# Default chart values. Real pins live in values/*.yaml — never promote a tag. +name: access-engine +namespace: access-engine + +image: + repository: forgejo.coulomb.social/coulomb/access-engine + digest: "" + pullPolicy: IfNotPresent + +replicaCount: 1 + +# Inert until enabled for an approved pin. The outbox is single-writer. +outbox: + enabled: false + source: "" + mountPath: /var/lib/access-engine/outbox + size: 1Gi + storageClassName: "" + existingClaim: "" + +# Inert preparation: caller provides reviewed existing Secret and exact receiver. +# The chart never creates/mints sender credentials or registers a sender. +auditDelivery: + enabled: false + url: "" + secretName: "" + secretKey: "" + mountPath: /var/run/secrets/access-engine-audit + receiverNamespace: "" + receiverPodName: "" + receiverPort: 0 + dnsNamespace: "" + dnsPodLabel: "" + +args: + - serve + - --addr + - 0.0.0.0:8080 + - --registry + - /opt/access-engine/examples/tenant-engine/registry_snapshot.json + - --policy + - /opt/access-engine/examples/tenant-engine/policy_package.md + +service: + port: 8080 + +health: + path: /healthz + +consumer: + isolated: false + namespace: "" + podName: "" + +# disabled keeps the image bootable without TokenReview (isolated canary). +# Production pins set warn, then enforce, independently per consumer. +# serve-readback pins (FLEX-WP-0035) admit one readback caller via +# --readback-caller instead of --caller-binding. +readback: + enabled: false + caller: "" + +callerAuth: + mode: disabled + audience: access-engine + kubernetesURL: https://kubernetes.default.svc + binding: "" + # Further resource.system=principal pairs, each rendered as its own + # --caller-binding (e.g. ops-warden-fetch binds every catalog owner system + # to the ops-warden ServiceAccount). + extraBindings: [] + reviewer: + mountPath: /var/run/secrets/access-engine-reviewer + # Empty: kubelet mints the API server's default audiences so TokenReview + # authenticates. A custom audience (e.g. https://kubernetes.default.svc) + # 401s on this k3s cluster, whose API audiences are + # https://kubernetes.default.svc.cluster.local and k3s. + audience: "" + expirationSeconds: 3600 + +resources: + requests: + cpu: 25m + memory: 32Mi + limits: + cpu: 300m + memory: 192Mi diff --git a/cmd/access-engine/readback_pin_test.go b/cmd/access-engine/readback_pin_test.go index f23c6c2..c39256a 100644 --- a/cmd/access-engine/readback_pin_test.go +++ b/cmd/access-engine/readback_pin_test.go @@ -43,30 +43,35 @@ func argAfter(args []string, flag string) []string { // package at the same digest its own pin runs. The package is baked into the // image, so same image digest and same path is the same package digest. func TestReadbackPinMatchesParticipantPins(t *testing.T) { - readback := loadPinValues(t, "application-readback.yaml") - if len(readback.Args) == 0 || readback.Args[0] != "serve-readback" { - t.Fatal("readback pin must run serve-readback, which mounts no /v1/check") - } - participants := map[string]string{"informed-decision": "informed-decision-lifecycle.yaml"} - packages := argAfter(readback.Args, "--package") - if len(packages) != len(participants) { - t.Fatalf("readback packages %v, want one per participant %v", packages, participants) - } - for _, pkg := range packages { - system, path, _ := strings.Cut(pkg, "=") - file, ok := participants[system] - if !ok { - t.Fatalf("readback package for %q has no participant pin", system) - } - own := loadPinValues(t, file) - if own.Image.Digest != readback.Image.Digest { - t.Errorf("%s: readback image %s, own pin %s", system, readback.Image.Digest, own.Image.Digest) - } - if policies := argAfter(own.Args, "--policy"); len(policies) != 1 || policies[0] != path { - t.Errorf("%s: readback package %s, own pin %v", system, path, policies) - } - if !strings.Contains(strings.Join(argAfter(readback.Args, "--registry"), " "), strings.Join(argAfter(own.Args, "--registry"), " ")) { - t.Errorf("%s: readback pin does not load its registry", system) - } + for _, prefix := range []string{"", "access-engine/"} { + t.Run(prefix, func(t *testing.T) { + readback := loadPinValues(t, prefix+"application-readback.yaml") + if len(readback.Args) == 0 || readback.Args[0] != "serve-readback" { + t.Fatal("readback pin must run serve-readback, which mounts no /v1/check") + } + participants := map[string]string{"informed-decision": "informed-decision-lifecycle.yaml"} + packages := argAfter(readback.Args, "--package") + if len(packages) != len(participants) { + t.Fatalf("readback packages %v, want one per participant %v", packages, participants) + } + for _, pkg := range packages { + system, path, _ := strings.Cut(pkg, "=") + file, ok := participants[system] + if !ok { + t.Fatalf("readback package for %q has no participant pin", system) + } + own := loadPinValues(t, prefix+file) + if own.Image.Digest != readback.Image.Digest { + t.Errorf("%s: readback image %s, own pin %s", system, readback.Image.Digest, own.Image.Digest) + } + if policies := argAfter(own.Args, "--policy"); len(policies) != 1 || policies[0] != path { + t.Errorf("%s: readback package %s, own pin %v", system, path, policies) + } + if !strings.Contains(strings.Join(argAfter(readback.Args, "--registry"), " "), strings.Join(argAfter(own.Args, "--registry"), " ")) { + t.Errorf("%s: readback pin does not load its registry", system) + } + } + + }) } } diff --git a/deploy/access-engine-namespace.yaml b/deploy/access-engine-namespace.yaml new file mode 100644 index 0000000..aa2f6bd --- /dev/null +++ b/deploy/access-engine-namespace.yaml @@ -0,0 +1,8 @@ +# FLEX-WP-0036-T05; creation remains gated on ordered predecessor live proof. +apiVersion: v1 +kind: Namespace +metadata: + name: access-engine + labels: + kubernetes.io/metadata.name: access-engine + app.kubernetes.io/part-of: access-engine diff --git a/docs/evidence/2026-10-11-runtime-twins/access-runtime-twins-prepared-receipt-v2.json b/docs/evidence/2026-10-11-runtime-twins/access-runtime-twins-prepared-receipt-v2.json new file mode 100644 index 0000000..1698f67 --- /dev/null +++ b/docs/evidence/2026-10-11-runtime-twins/access-runtime-twins-prepared-receipt-v2.json @@ -0,0 +1,439 @@ +{ + "schema": "build-mode-prepared-change-v2", + "workplan": "FLEX-WP-0036", + "task": "FLEX-WP-0036-T05", + "status": "PREPARED_SOURCE_ONLY_NATIVE_RED_BATCH_WAIT", + "target": "access-engine namespace and nine side-by-side releases", + "prerequisites": [ + "Single daily red batch disposition authorizes exact nine new TokenReview ServiceAccount bindings", + "Ordered preceding critical-path live evidence by coordinator", + "T03 image compatibility rollout verified on existing pins", + "T04 exact artifact availability and same-digest twin provenance verified", + "Old T05 declared external dependency qualified by coordinator; worker did not inspect wait task", + "Actual live pin inventory/digest and CPU headroom reconciled before server dry-run" + ], + "expected_result": "Nine renamed releases Ready; /healthz200; anonymous protected routes401; exact twin image/policy and caller/network scope unchanged", + "abort_owner": "Custodian coordinator acting as Access runtime change owner", + "abort_conditions": [ + "Any source/live twin digest or package argv mismatch", + "Any unproven preceding critical-path gate", + "Insufficient headroom or rollout stall", + "New pin cannot TokenReview or cannot match old decision behavior", + "Any changed caller principal, ingress/egress grant or shared durable writer state", + "Any missing exact red-batch authorization for new API principals or recovery revocation scope" + ], + "verification": [ + "Fresh helm template + server dry-run for exact source revision, namespace and pin manifests", + "Reconcile live nine-pin inventory and image digests against source", + "Ready, healthz200 and anonymous401 each new pin", + "Live authenticated owner allow/deny correspondence with old twin; no consumer switch in T05", + "Read back exact nine bound ServiceAccount subjects and TokenReview-only ClusterRole rules; prove no additional subjects/resources/verbs were admitted" + ], + "rollback": "Before execution: no native grants exist to revoke. If the exact red batch is authorized and executed, suspend newly introduced GitOps reconciliation and scale only new access-engine Deployments to zero to stop workloads; this does NOT revoke their TokenReview authority. Rollback must additionally revoke exactly the nine new ClusterRoleBindings through the authorized red-batch recovery scope, retaining prior flex-auth bindings and all namespaces/data. Do not claim scale-zero alone revokes credentials or API authority. Destructive namespace/data deletion remains outside this item.", + "scope_limits": [ + "No consumer endpoint/egress flip", + "No live mutation executed", + "No dual repository publication or unproven new image paths claimed", + "No durable writer twins generated", + "Nine new Kubernetes API TokenReview caller grants are explicitly proposed in red batch; no new protected-consumer caller mappings, secret grants or native writes executed" + ], + "pins": [ + { + "name": "access-engine-application-readback", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:e8c57c18a4a9d5583be099146984f7e9cd881446218b65aeefd7619b18aa9229", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "audience": "access-engine" + }, + "consumer": { + "isolated": false, + "namespace": "user-engine", + "podName": "user-engine" + }, + "request_cpu": "5m", + "request_memory": "32Mi" + }, + { + "name": "access-engine-informed-decision-lifecycle", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:e8c57c18a4a9d5583be099146984f7e9cd881446218b65aeefd7619b18aa9229", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "binding": "informed-decision=system:serviceaccount:informed-decision:review", + "audience": "access-engine" + }, + "consumer": { + "isolated": false, + "namespace": "informed-decision", + "podName": "informed-decision" + }, + "request_cpu": "5m", + "request_memory": "32Mi" + }, + { + "name": "access-engine-informed-decision-sitting", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:bdac62bc3f089cba0df7c35eeb8d0d9b3af2d4bae5e4ea4767516c1ed20b1e99", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "binding": "informed-decision=system:serviceaccount:informed-decision:review", + "audience": "access-engine" + }, + "consumer": { + "isolated": false, + "namespace": "informed-decision", + "podName": "informed-decision" + }, + "request_cpu": "5m", + "request_memory": "32Mi" + }, + { + "name": "access-engine-informed-decision-t03", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:6600c7a62c36dfb4fbb42590722613827d07ff49875b1f0f4b5de63d8351658d", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "binding": "informed-decision=system:serviceaccount:informed-decision:review", + "audience": "access-engine" + }, + "consumer": { + "isolated": false, + "namespace": "informed-decision", + "podName": "informed-decision" + }, + "request_cpu": "5m", + "request_memory": "32Mi" + }, + { + "name": "access-engine-ops-warden-fetch", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:419bc37420d0d0a5304a8587448f1fdbf87117fb041fb43081b0449b4a7bb13b", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "binding": "key-cape=system:serviceaccount:ops-warden:ops-warden", + "extraBindings": [ + "railiance-platform=system:serviceaccount:ops-warden:ops-warden" + ], + "audience": "access-engine" + }, + "consumer": { + "isolated": true + }, + "request_cpu": "25m", + "request_memory": "32Mi" + }, + { + "name": "access-engine-ops-warden", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:138aa3471c46bca6e814691fa1e6520aedda3dffd743e6b09141ab433afdb64b", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "binding": "ops-warden=system:serviceaccount:ops-warden:ops-warden", + "audience": "access-engine" + }, + "consumer": { + "isolated": true + }, + "request_cpu": "25m", + "request_memory": "32Mi" + }, + { + "name": "access-engine-secrets-engine", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "binding": "secrets-engine=system:serviceaccount:secrets-engine:secrets-engine", + "audience": "access-engine" + }, + "consumer": { + "isolated": false, + "namespace": "secrets-engine", + "podName": "secrets-engine" + }, + "request_cpu": "25m", + "request_memory": "32Mi" + }, + { + "name": "access-engine-tenant-engine", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "binding": "tenant-engine=system:serviceaccount:tenant-engine:tenant-engine", + "audience": "access-engine" + }, + "consumer": { + "isolated": false, + "namespace": "tenant-engine", + "podName": "tenant-engine" + }, + "request_cpu": "25m", + "request_memory": "32Mi" + }, + { + "name": "access-engine-user-engine", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:138aa3471c46bca6e814691fa1e6520aedda3dffd743e6b09141ab433afdb64b", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "binding": "user-engine=system:serviceaccount:user-engine:user-engine", + "audience": "access-engine" + }, + "consumer": { + "isolated": false, + "namespace": "user-engine", + "podName": "user-engine" + }, + "request_cpu": "25m", + "request_memory": "32Mi" + } + ], + "classification": { + "source_preparation": "blue reversible local source work", + "native_namespace_and_pins": "red access-scope expansion: new ServiceAccounts receive Kubernetes TokenReview API create authority", + "reason": "Equality of verb sets does not establish equality of principals; nine new bound subjects can now create tokenreviews.", + "approval_route": "one item in the single daily red batch; no per-item founder request" + }, + "supersedes": { + "path": "/tmp/access-runtime-twins-prepared-receipt.json", + "sha256": "ddf0d366afa3330ab753079dc6e4c122559b3bb2b9c6e38faf72d23f8a09127d", + "reason": "First prepared receipt incorrectly classified new TokenReview principals as blue based on role-verb parity." + }, + "new_access_scope": [ + { + "subject": { + "kind": "ServiceAccount", + "namespace": "access-engine", + "name": "access-engine-application-readback", + "authenticated_username": "system:serviceaccount:access-engine:access-engine-application-readback" + }, + "binding": "access-engine-application-readback-tokenreviewer", + "role": { + "kind": "ClusterRole", + "name": "access-engine-application-readback-tokenreviewer" + }, + "capability": { + "apiGroups": [ + "authentication.k8s.io" + ], + "resources": [ + "tokenreviews" + ], + "verbs": [ + "create" + ] + }, + "scope": "cluster-scoped TokenReview API create; not a secrets-read grant or protected-consumer impersonation grant" + }, + { + "subject": { + "kind": "ServiceAccount", + "namespace": "access-engine", + "name": "access-engine-informed-decision-lifecycle", + "authenticated_username": "system:serviceaccount:access-engine:access-engine-informed-decision-lifecycle" + }, + "binding": "access-engine-informed-decision-lifecycle-tokenreviewer", + "role": { + "kind": "ClusterRole", + "name": "access-engine-informed-decision-lifecycle-tokenreviewer" + }, + "capability": { + "apiGroups": [ + "authentication.k8s.io" + ], + "resources": [ + "tokenreviews" + ], + "verbs": [ + "create" + ] + }, + "scope": "cluster-scoped TokenReview API create; not a secrets-read grant or protected-consumer impersonation grant" + }, + { + "subject": { + "kind": "ServiceAccount", + "namespace": "access-engine", + "name": "access-engine-informed-decision-sitting", + "authenticated_username": "system:serviceaccount:access-engine:access-engine-informed-decision-sitting" + }, + "binding": "access-engine-informed-decision-sitting-tokenreviewer", + "role": { + "kind": "ClusterRole", + "name": "access-engine-informed-decision-sitting-tokenreviewer" + }, + "capability": { + "apiGroups": [ + "authentication.k8s.io" + ], + "resources": [ + "tokenreviews" + ], + "verbs": [ + "create" + ] + }, + "scope": "cluster-scoped TokenReview API create; not a secrets-read grant or protected-consumer impersonation grant" + }, + { + "subject": { + "kind": "ServiceAccount", + "namespace": "access-engine", + "name": "access-engine-informed-decision-t03", + "authenticated_username": "system:serviceaccount:access-engine:access-engine-informed-decision-t03" + }, + "binding": "access-engine-informed-decision-t03-tokenreviewer", + "role": { + "kind": "ClusterRole", + "name": "access-engine-informed-decision-t03-tokenreviewer" + }, + "capability": { + "apiGroups": [ + "authentication.k8s.io" + ], + "resources": [ + "tokenreviews" + ], + "verbs": [ + "create" + ] + }, + "scope": "cluster-scoped TokenReview API create; not a secrets-read grant or protected-consumer impersonation grant" + }, + { + "subject": { + "kind": "ServiceAccount", + "namespace": "access-engine", + "name": "access-engine-ops-warden-fetch", + "authenticated_username": "system:serviceaccount:access-engine:access-engine-ops-warden-fetch" + }, + "binding": "access-engine-ops-warden-fetch-tokenreviewer", + "role": { + "kind": "ClusterRole", + "name": "access-engine-ops-warden-fetch-tokenreviewer" + }, + "capability": { + "apiGroups": [ + "authentication.k8s.io" + ], + "resources": [ + "tokenreviews" + ], + "verbs": [ + "create" + ] + }, + "scope": "cluster-scoped TokenReview API create; not a secrets-read grant or protected-consumer impersonation grant" + }, + { + "subject": { + "kind": "ServiceAccount", + "namespace": "access-engine", + "name": "access-engine-ops-warden", + "authenticated_username": "system:serviceaccount:access-engine:access-engine-ops-warden" + }, + "binding": "access-engine-ops-warden-tokenreviewer", + "role": { + "kind": "ClusterRole", + "name": "access-engine-ops-warden-tokenreviewer" + }, + "capability": { + "apiGroups": [ + "authentication.k8s.io" + ], + "resources": [ + "tokenreviews" + ], + "verbs": [ + "create" + ] + }, + "scope": "cluster-scoped TokenReview API create; not a secrets-read grant or protected-consumer impersonation grant" + }, + { + "subject": { + "kind": "ServiceAccount", + "namespace": "access-engine", + "name": "access-engine-secrets-engine", + "authenticated_username": "system:serviceaccount:access-engine:access-engine-secrets-engine" + }, + "binding": "access-engine-secrets-engine-tokenreviewer", + "role": { + "kind": "ClusterRole", + "name": "access-engine-secrets-engine-tokenreviewer" + }, + "capability": { + "apiGroups": [ + "authentication.k8s.io" + ], + "resources": [ + "tokenreviews" + ], + "verbs": [ + "create" + ] + }, + "scope": "cluster-scoped TokenReview API create; not a secrets-read grant or protected-consumer impersonation grant" + }, + { + "subject": { + "kind": "ServiceAccount", + "namespace": "access-engine", + "name": "access-engine-tenant-engine", + "authenticated_username": "system:serviceaccount:access-engine:access-engine-tenant-engine" + }, + "binding": "access-engine-tenant-engine-tokenreviewer", + "role": { + "kind": "ClusterRole", + "name": "access-engine-tenant-engine-tokenreviewer" + }, + "capability": { + "apiGroups": [ + "authentication.k8s.io" + ], + "resources": [ + "tokenreviews" + ], + "verbs": [ + "create" + ] + }, + "scope": "cluster-scoped TokenReview API create; not a secrets-read grant or protected-consumer impersonation grant" + }, + { + "subject": { + "kind": "ServiceAccount", + "namespace": "access-engine", + "name": "access-engine-user-engine", + "authenticated_username": "system:serviceaccount:access-engine:access-engine-user-engine" + }, + "binding": "access-engine-user-engine-tokenreviewer", + "role": { + "kind": "ClusterRole", + "name": "access-engine-user-engine-tokenreviewer" + }, + "capability": { + "apiGroups": [ + "authentication.k8s.io" + ], + "resources": [ + "tokenreviews" + ], + "verbs": [ + "create" + ] + }, + "scope": "cluster-scoped TokenReview API create; not a secrets-read grant or protected-consumer impersonation grant" + } + ], + "daily_red_batch_item": { + "id": "FLEX-WP-0036-T05-tokenreview-twins", + "request": "Authorize exact nine access-engine ServiceAccount TokenReview create bindings, together with exact binding revocation recovery; dependent namespace/pin rollout stays gated on T03/T04 and ordered predecessor evidence.", + "new_subject_count": 9, + "scope_list_field": "new_access_scope", + "execution_state": "not executed", + "scope_limits": "No secrets-read grants, protected-consumer mapping expansion, namespace/data deletion or consumer endpoint change." + } +} diff --git a/docs/evidence/2026-10-11-runtime-twins/access-runtime-twins-prepared-receipt.json b/docs/evidence/2026-10-11-runtime-twins/access-runtime-twins-prepared-receipt.json new file mode 100644 index 0000000..083827a --- /dev/null +++ b/docs/evidence/2026-10-11-runtime-twins/access-runtime-twins-prepared-receipt.json @@ -0,0 +1,190 @@ +{ + "schema": "build-mode-prepared-blue-change-v1", + "workplan": "FLEX-WP-0036", + "task": "FLEX-WP-0036-T05", + "status": "PREPARED_SOURCE_ONLY_NATIVE_WAIT", + "target": "access-engine namespace and nine side-by-side releases", + "prerequisites": [ + "Ordered preceding critical-path live evidence by coordinator", + "T03 image compatibility rollout verified on existing pins", + "T04 exact artifact availability and same-digest twin provenance verified", + "Old T05 declared external dependency qualified by coordinator; worker did not inspect wait task", + "Actual live pin inventory/digest and CPU headroom reconciled before server dry-run" + ], + "expected_result": "Nine renamed releases Ready; /healthz200; anonymous protected routes401; exact twin image/policy and caller/network scope unchanged", + "abort_owner": "Custodian coordinator acting as Access runtime change owner", + "abort_conditions": [ + "Any source/live twin digest or package argv mismatch", + "Any unproven preceding critical-path gate", + "Insufficient headroom or rollout stall", + "New pin cannot TokenReview or cannot match old decision behavior", + "Any changed caller principal, ingress/egress grant or shared durable writer state" + ], + "verification": [ + "Fresh helm template + server dry-run for exact source revision, namespace and pin manifests", + "Reconcile live nine-pin inventory and image digests against source", + "Ready, healthz200 and anonymous401 each new pin", + "Live authenticated owner allow/deny correspondence with old twin; no consumer switch in T05" + ], + "rollback": "Suspend newly introduced GitOps reconciliation and scale only newly created access-engine Deployments to zero. Leave flex-auth releases, namespace, data and caller configuration intact. Do not delete old/new namespaces, RBAC or durable storage in this blue scope; destructive cleanup goes to the red batch.", + "scope_limits": [ + "No consumer endpoint/egress flip", + "No live mutation executed", + "No new caller grants or secrets", + "No dual repository publication or unproven new image paths claimed", + "No durable writer twins generated" + ], + "pins": [ + { + "name": "access-engine-application-readback", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:e8c57c18a4a9d5583be099146984f7e9cd881446218b65aeefd7619b18aa9229", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "audience": "access-engine" + }, + "consumer": { + "isolated": false, + "namespace": "user-engine", + "podName": "user-engine" + }, + "request_cpu": "5m", + "request_memory": "32Mi" + }, + { + "name": "access-engine-informed-decision-lifecycle", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:e8c57c18a4a9d5583be099146984f7e9cd881446218b65aeefd7619b18aa9229", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "binding": "informed-decision=system:serviceaccount:informed-decision:review", + "audience": "access-engine" + }, + "consumer": { + "isolated": false, + "namespace": "informed-decision", + "podName": "informed-decision" + }, + "request_cpu": "5m", + "request_memory": "32Mi" + }, + { + "name": "access-engine-informed-decision-sitting", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:bdac62bc3f089cba0df7c35eeb8d0d9b3af2d4bae5e4ea4767516c1ed20b1e99", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "binding": "informed-decision=system:serviceaccount:informed-decision:review", + "audience": "access-engine" + }, + "consumer": { + "isolated": false, + "namespace": "informed-decision", + "podName": "informed-decision" + }, + "request_cpu": "5m", + "request_memory": "32Mi" + }, + { + "name": "access-engine-informed-decision-t03", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:6600c7a62c36dfb4fbb42590722613827d07ff49875b1f0f4b5de63d8351658d", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "binding": "informed-decision=system:serviceaccount:informed-decision:review", + "audience": "access-engine" + }, + "consumer": { + "isolated": false, + "namespace": "informed-decision", + "podName": "informed-decision" + }, + "request_cpu": "5m", + "request_memory": "32Mi" + }, + { + "name": "access-engine-ops-warden-fetch", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:419bc37420d0d0a5304a8587448f1fdbf87117fb041fb43081b0449b4a7bb13b", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "binding": "key-cape=system:serviceaccount:ops-warden:ops-warden", + "extraBindings": [ + "railiance-platform=system:serviceaccount:ops-warden:ops-warden" + ], + "audience": "access-engine" + }, + "consumer": { + "isolated": true + }, + "request_cpu": "25m", + "request_memory": "32Mi" + }, + { + "name": "access-engine-ops-warden", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:138aa3471c46bca6e814691fa1e6520aedda3dffd743e6b09141ab433afdb64b", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "binding": "ops-warden=system:serviceaccount:ops-warden:ops-warden", + "audience": "access-engine" + }, + "consumer": { + "isolated": true + }, + "request_cpu": "25m", + "request_memory": "32Mi" + }, + { + "name": "access-engine-secrets-engine", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "binding": "secrets-engine=system:serviceaccount:secrets-engine:secrets-engine", + "audience": "access-engine" + }, + "consumer": { + "isolated": false, + "namespace": "secrets-engine", + "podName": "secrets-engine" + }, + "request_cpu": "25m", + "request_memory": "32Mi" + }, + { + "name": "access-engine-tenant-engine", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "binding": "tenant-engine=system:serviceaccount:tenant-engine:tenant-engine", + "audience": "access-engine" + }, + "consumer": { + "isolated": false, + "namespace": "tenant-engine", + "podName": "tenant-engine" + }, + "request_cpu": "25m", + "request_memory": "32Mi" + }, + { + "name": "access-engine-user-engine", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:138aa3471c46bca6e814691fa1e6520aedda3dffd743e6b09141ab433afdb64b", + "caller_auth": { + "mode": "enforce", + "kubernetesURL": "https://10.43.0.1", + "binding": "user-engine=system:serviceaccount:user-engine:user-engine", + "audience": "access-engine" + }, + "consumer": { + "isolated": false, + "namespace": "user-engine", + "podName": "user-engine" + }, + "request_cpu": "25m", + "request_memory": "32Mi" + } + ] +} diff --git a/tests/stage1.sh b/tests/stage1.sh index 0531d95..cec1ee2 100755 --- a/tests/stage1.sh +++ b/tests/stage1.sh @@ -6,6 +6,8 @@ cd "$(dirname "${BASH_SOURCE[0]}")/.." python3 tests/validate_app_toml.py python3 tests/test_outbox_chart.py +python3 tools/gen_runtime_twins.py --check +python3 tests/test_runtime_twins.py for values in values/stage1.yaml values/stage2-canary.yaml values/stage3-production.yaml \ values/user-engine.yaml values/tenant-engine.yaml values/ops-warden.yaml; do diff --git a/tests/test_runtime_twins.py b/tests/test_runtime_twins.py new file mode 100644 index 0000000..4071469 --- /dev/null +++ b/tests/test_runtime_twins.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Source render integration tests; never contacts the cluster.""" +import copy +import importlib.util +from pathlib import Path +import subprocess +import unittest +import yaml + +ROOT = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location("twins", ROOT / "tools/gen_runtime_twins.py") +twins = importlib.util.module_from_spec(spec) +spec.loader.exec_module(twins) + +def render(chart, values, namespace, name): + output = subprocess.check_output(["helm", "template", name, str(ROOT / "charts" / chart), "-f", str(ROOT / "values" / values), "--namespace", namespace], text=True) + return {obj["kind"]: obj for obj in yaml.safe_load_all(output) if obj} + +def after(args, flag): + return [args[i + 1] for i, item in enumerate(args[:-1]) if item == flag] + +class RuntimeTwinTests(unittest.TestCase): + def test_actual_nine_twin_render_security_and_content(self): + for pin in twins.PINS: + with self.subTest(pin=pin): + source = yaml.safe_load((ROOT / "values" / (pin + ".yaml")).read_text()) + new = yaml.safe_load((ROOT / "values/access-engine" / (pin + ".yaml")).read_text()) + self.assertEqual(new, twins.twin(source)) + old_objects = render("flex-auth", pin + ".yaml", "flex-auth", source["name"]) + objects = render("access-engine", "access-engine/" + pin + ".yaml", "access-engine", new["name"]) + self.assertEqual(set(objects), set(old_objects)) + for kind in ("Deployment", "Service", "ServiceAccount", "NetworkPolicy"): + self.assertEqual(objects[kind]["metadata"]["namespace"], "access-engine") + deployment = objects["Deployment"] + pod = deployment["spec"]["template"]["spec"] + container = pod["containers"][0] + old_container = old_objects["Deployment"]["spec"]["template"]["spec"]["containers"][0] + self.assertEqual(container["image"], old_container["image"]) + self.assertEqual(container["args"][:len(source["args"])], source["args"]) + self.assertEqual(after(container["args"], "--caller-audience"), ["access-engine"]) + self.assertEqual(after(container["args"], "--caller-binding"), after(old_container["args"], "--caller-binding")) + self.assertEqual(after(container["args"], "--readback-caller"), after(old_container["args"], "--readback-caller")) + self.assertEqual(pod["serviceAccountName"], new["name"]) + mount = next(m["mountPath"] for m in container["volumeMounts"] if m["name"] == "access-engine-reviewer") + self.assertEqual(after(container["args"], "--caller-reviewer-token-file"), [mount + "/token"]) + self.assertEqual(after(container["args"], "--caller-ca-file"), [mount + "/ca.crt"]) + self.assertFalse(pod["automountServiceAccountToken"]) + self.assertEqual(objects["ClusterRole"]["rules"], old_objects["ClusterRole"]["rules"]) + binding = objects["ClusterRoleBinding"] + self.assertEqual(binding["subjects"], [{"kind": "ServiceAccount", "name": new["name"], "namespace": "access-engine"}]) + self.assertEqual(binding["roleRef"]["name"], new["name"] + "-tokenreviewer") + self.assertEqual(objects["NetworkPolicy"]["spec"]["ingress"], old_objects["NetworkPolicy"]["spec"]["ingress"]) + self.assertEqual(objects["NetworkPolicy"]["spec"]["egress"], old_objects["NetworkPolicy"]["spec"]["egress"]) + self.assertEqual(objects["Service"]["spec"]["selector"], deployment["spec"]["selector"]["matchLabels"]) + self.assertEqual(deployment["spec"]["selector"]["matchLabels"], {"app.kubernetes.io/name": new["name"]}) + self.assertTrue(container["securityContext"]["readOnlyRootFilesystem"]) + self.assertFalse(container["securityContext"]["allowPrivilegeEscalation"]) + self.assertEqual(container["livenessProbe"], old_container["livenessProbe"]) + self.assertEqual(container["readinessProbe"], old_container["readinessProbe"]) + + def test_refuse_unpinned_unenforced_and_shared_writer(self): + source = yaml.safe_load((ROOT / "values/user-engine.yaml").read_text()) + for field, value in (("image", {"digest": "latest"}), ("callerAuth", {"mode": "disabled"}), ("outbox", {"enabled": True}), ("auditDelivery", {"enabled": True}), ("name", "other-pin")): + with self.subTest(field=field): + bad = copy.deepcopy(source); bad[field] = value + with self.assertRaises(ValueError): twins.twin(bad) + self.assertNotIn("namespace", source) + +if __name__ == "__main__": unittest.main() diff --git a/tools/gen_runtime_twins.py b/tools/gen_runtime_twins.py new file mode 100644 index 0000000..7cfb26d --- /dev/null +++ b/tools/gen_runtime_twins.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Prepare exact-content runtime twins. Local files only; never applies to Kubernetes. + +Namespace/release/chart/ServiceAccount and caller audience move to access-engine. +Image repository+digest and policy/registry argv remain exact existing-twin values +until publication/path migration is separately proven. +""" +from pathlib import Path +import argparse +import copy +import re +import yaml + +ROOT = Path(__file__).resolve().parents[1] +PINS = ("ops-warden", "ops-warden-fetch", "secrets-engine", "user-engine", "tenant-engine", "informed-decision-lifecycle", "informed-decision-sitting", "informed-decision-t03", "application-readback") + +def twin(source): + result = copy.deepcopy(source) + if not re.fullmatch(r"sha256:[0-9a-f]{64}", source.get("image", {}).get("digest", "")): + raise ValueError("existing twin must have an immutable SHA256 digest") + name = source.get("name", "") + if not name.startswith("flex-auth-"): + raise ValueError("existing pin must have an exact flex-auth release name") + if source.get("callerAuth", {}).get("mode") != "enforce": + raise ValueError("new twin requires existing enforced caller authentication") + if source.get("outbox", {}).get("enabled") or source.get("auditDelivery", {}).get("enabled"): + raise ValueError("durable outbox/audit twins require independent source/custody; never share writer state") + result["name"] = "access-engine-" + name[len("flex-auth-"):] + result["namespace"] = "access-engine" + result["callerAuth"]["audience"] = "access-engine" + return result + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + for pin in PINS: + source = yaml.safe_load((ROOT / "values" / (pin + ".yaml")).read_text()) + target = ROOT / "values" / "access-engine" / (pin + ".yaml") + result = twin(source) + if args.check: + if not target.exists() or yaml.safe_load(target.read_text()) != result: + raise SystemExit(f"runtime twin drift: {target}") + else: + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text("# GENERATED by tools/gen_runtime_twins.py; same image/package as existing twin.\n" + yaml.safe_dump(result, sort_keys=False)) + +if __name__ == "__main__": + main() diff --git a/values/access-engine/application-readback.yaml b/values/access-engine/application-readback.yaml new file mode 100644 index 0000000..cfd22b0 --- /dev/null +++ b/values/access-engine/application-readback.yaml @@ -0,0 +1,34 @@ +# GENERATED by tools/gen_runtime_twins.py; same image/package as existing twin. +name: access-engine-application-readback +image: + repository: forgejo.coulomb.social/coulomb/flex-auth + digest: sha256:e8c57c18a4a9d5583be099146984f7e9cd881446218b65aeefd7619b18aa9229 +args: +- serve-readback +- --addr +- 0.0.0.0:8080 +- --registry +- /opt/flex-auth/examples/application-readback/registry_snapshot.json +- --registry +- /opt/flex-auth/examples/informed-decision-lifecycle/registry.json +- --package +- informed-decision=/opt/flex-auth/examples/informed-decision-lifecycle/policy.md +callerAuth: + mode: enforce + kubernetesURL: https://10.43.0.1 + audience: access-engine +readback: + enabled: true + caller: system:serviceaccount:user-engine:user-engine +consumer: + isolated: false + namespace: user-engine + podName: user-engine +resources: + requests: + cpu: 5m + memory: 32Mi + limits: + cpu: 300m + memory: 192Mi +namespace: access-engine diff --git a/values/access-engine/informed-decision-lifecycle.yaml b/values/access-engine/informed-decision-lifecycle.yaml new file mode 100644 index 0000000..d5c30b4 --- /dev/null +++ b/values/access-engine/informed-decision-lifecycle.yaml @@ -0,0 +1,30 @@ +# GENERATED by tools/gen_runtime_twins.py; same image/package as existing twin. +name: access-engine-informed-decision-lifecycle +image: + repository: forgejo.coulomb.social/coulomb/flex-auth + digest: sha256:e8c57c18a4a9d5583be099146984f7e9cd881446218b65aeefd7619b18aa9229 +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 +callerAuth: + mode: enforce + kubernetesURL: https://10.43.0.1 + binding: informed-decision=system:serviceaccount:informed-decision:review + audience: access-engine +consumer: + isolated: false + namespace: informed-decision + podName: informed-decision +resources: + requests: + cpu: 5m + memory: 32Mi + limits: + cpu: 300m + memory: 192Mi +namespace: access-engine diff --git a/values/access-engine/informed-decision-sitting.yaml b/values/access-engine/informed-decision-sitting.yaml new file mode 100644 index 0000000..7a3a9e9 --- /dev/null +++ b/values/access-engine/informed-decision-sitting.yaml @@ -0,0 +1,30 @@ +# GENERATED by tools/gen_runtime_twins.py; same image/package as existing twin. +name: access-engine-informed-decision-sitting +image: + repository: forgejo.coulomb.social/coulomb/flex-auth + digest: sha256:bdac62bc3f089cba0df7c35eeb8d0d9b3af2d4bae5e4ea4767516c1ed20b1e99 +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 +callerAuth: + mode: enforce + kubernetesURL: https://10.43.0.1 + binding: informed-decision=system:serviceaccount:informed-decision:review + audience: access-engine +consumer: + isolated: false + namespace: informed-decision + podName: informed-decision +resources: + requests: + cpu: 5m + memory: 32Mi + limits: + cpu: 300m + memory: 192Mi +namespace: access-engine diff --git a/values/access-engine/informed-decision-t03.yaml b/values/access-engine/informed-decision-t03.yaml new file mode 100644 index 0000000..948aad9 --- /dev/null +++ b/values/access-engine/informed-decision-t03.yaml @@ -0,0 +1,30 @@ +# GENERATED by tools/gen_runtime_twins.py; same image/package as existing twin. +name: access-engine-informed-decision-t03 +image: + repository: forgejo.coulomb.social/coulomb/flex-auth + digest: sha256:6600c7a62c36dfb4fbb42590722613827d07ff49875b1f0f4b5de63d8351658d +args: +- serve +- --addr +- 0.0.0.0:8080 +- --registry +- /opt/flex-auth/examples/informed-decision-t03/registry.json +- --policy +- /opt/flex-auth/examples/informed-decision-t03/policy.md +callerAuth: + mode: enforce + kubernetesURL: https://10.43.0.1 + binding: informed-decision=system:serviceaccount:informed-decision:review + audience: access-engine +consumer: + isolated: false + namespace: informed-decision + podName: informed-decision +resources: + requests: + cpu: 5m + memory: 32Mi + limits: + cpu: 300m + memory: 192Mi +namespace: access-engine diff --git a/values/access-engine/ops-warden-fetch.yaml b/values/access-engine/ops-warden-fetch.yaml new file mode 100644 index 0000000..8fa5fee --- /dev/null +++ b/values/access-engine/ops-warden-fetch.yaml @@ -0,0 +1,23 @@ +# GENERATED by tools/gen_runtime_twins.py; same image/package as existing twin. +name: access-engine-ops-warden-fetch +image: + repository: forgejo.coulomb.social/coulomb/flex-auth + digest: sha256:419bc37420d0d0a5304a8587448f1fdbf87117fb041fb43081b0449b4a7bb13b +args: +- serve +- --addr +- 0.0.0.0:8080 +- --registry +- /opt/flex-auth/examples/ops-warden-fetch/registry_snapshot.json +- --policy +- /opt/flex-auth/examples/ops-warden-fetch/policy_package.md +callerAuth: + mode: enforce + kubernetesURL: https://10.43.0.1 + binding: key-cape=system:serviceaccount:ops-warden:ops-warden + extraBindings: + - railiance-platform=system:serviceaccount:ops-warden:ops-warden + audience: access-engine +consumer: + isolated: true +namespace: access-engine diff --git a/values/access-engine/ops-warden.yaml b/values/access-engine/ops-warden.yaml new file mode 100644 index 0000000..d3af255 --- /dev/null +++ b/values/access-engine/ops-warden.yaml @@ -0,0 +1,21 @@ +# GENERATED by tools/gen_runtime_twins.py; same image/package as existing twin. +name: access-engine-ops-warden +image: + repository: forgejo.coulomb.social/coulomb/flex-auth + digest: sha256:138aa3471c46bca6e814691fa1e6520aedda3dffd743e6b09141ab433afdb64b +args: +- serve +- --addr +- 0.0.0.0:8080 +- --registry +- /opt/flex-auth/examples/ops-warden/production_registry_snapshot.json +- --policy +- /opt/flex-auth/examples/ops-warden/policy_package.md +callerAuth: + mode: enforce + kubernetesURL: https://10.43.0.1 + binding: ops-warden=system:serviceaccount:ops-warden:ops-warden + audience: access-engine +consumer: + isolated: true +namespace: access-engine diff --git a/values/access-engine/secrets-engine.yaml b/values/access-engine/secrets-engine.yaml new file mode 100644 index 0000000..c73619a --- /dev/null +++ b/values/access-engine/secrets-engine.yaml @@ -0,0 +1,23 @@ +# GENERATED by tools/gen_runtime_twins.py; same image/package as existing twin. +name: access-engine-secrets-engine +image: + repository: forgejo.coulomb.social/coulomb/flex-auth + digest: sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd +args: +- serve +- --addr +- 0.0.0.0:8080 +- --registry +- /opt/flex-auth/examples/secrets-engine/registry_snapshot.json +- --policy +- /opt/flex-auth/examples/secrets-engine/policy_package.md +callerAuth: + mode: enforce + kubernetesURL: https://10.43.0.1 + binding: secrets-engine=system:serviceaccount:secrets-engine:secrets-engine + audience: access-engine +consumer: + isolated: false + namespace: secrets-engine + podName: secrets-engine +namespace: access-engine diff --git a/values/access-engine/tenant-engine.yaml b/values/access-engine/tenant-engine.yaml new file mode 100644 index 0000000..8160bec --- /dev/null +++ b/values/access-engine/tenant-engine.yaml @@ -0,0 +1,23 @@ +# GENERATED by tools/gen_runtime_twins.py; same image/package as existing twin. +name: access-engine-tenant-engine +image: + repository: forgejo.coulomb.social/coulomb/flex-auth + digest: sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd +args: +- serve +- --addr +- 0.0.0.0:8080 +- --registry +- /opt/flex-auth/examples/tenant-engine/registry_snapshot.json +- --policy +- /opt/flex-auth/examples/tenant-engine/policy_package.md +callerAuth: + mode: enforce + kubernetesURL: https://10.43.0.1 + binding: tenant-engine=system:serviceaccount:tenant-engine:tenant-engine + audience: access-engine +consumer: + isolated: false + namespace: tenant-engine + podName: tenant-engine +namespace: access-engine diff --git a/values/access-engine/user-engine.yaml b/values/access-engine/user-engine.yaml new file mode 100644 index 0000000..a533efb --- /dev/null +++ b/values/access-engine/user-engine.yaml @@ -0,0 +1,23 @@ +# GENERATED by tools/gen_runtime_twins.py; same image/package as existing twin. +name: access-engine-user-engine +image: + repository: forgejo.coulomb.social/coulomb/flex-auth + digest: sha256:138aa3471c46bca6e814691fa1e6520aedda3dffd743e6b09141ab433afdb64b +args: +- serve +- --addr +- 0.0.0.0:8080 +- --registry +- /opt/flex-auth/examples/user-engine/registry_snapshot.json +- --policy +- /opt/flex-auth/examples/user-engine/policy_package.md +callerAuth: + mode: enforce + kubernetesURL: https://10.43.0.1 + binding: user-engine=system:serviceaccount:user-engine:user-engine + audience: access-engine +consumer: + isolated: false + namespace: user-engine + podName: user-engine +namespace: access-engine diff --git a/workplans/FLEX-WP-0036-runtime-rename-access-engine.md b/workplans/FLEX-WP-0036-runtime-rename-access-engine.md index ee6942c..07e4d4c 100644 --- a/workplans/FLEX-WP-0036-runtime-rename-access-engine.md +++ b/workplans/FLEX-WP-0036-runtime-rename-access-engine.md @@ -181,7 +181,7 @@ args reference. ```task id: FLEX-WP-0036-T05 -status: todo +status: wait priority: medium depends_on: [FLEX-WP-0036-T03, FLEX-WP-0036-T04] state_hub_task_id: "13b93d7a-8341-5c76-9ae8-0f36b45b10fc" @@ -197,6 +197,24 @@ that each twin pair runs the same digest. Gate: every new pin Ready, `/healthz` 200, anonymous requests 401; founder go. Blocked on `FLEX-WP-0020-T11` closing. +2026-10-11: native wait qualified once against existing `depends_on` T03/T04: +coordinator must prove predecessor image rollout on existing pins before any +new namespace/pin creation. No wait-task inspection or new grant was needed. +Actual source preparation adds the renamed chart, exact-content generated twins, +explicit audience/reviewer paths and render/fixture correspondence checks. +Twins retain the exact existing image repository/digest and policy argv; these +are migration compatibility details until an identical digest is published under +the renamed repository and the baked new paths are proven. No namespace or +release has been created by this preparation. Local source preparation is reversible blue work under +`the-custodian/canon/standards/build-mode-change-authorization_v0.1.md`. +Native rollout creates nine new ServiceAccount subjects bound to cluster-scoped +`create` on `authentication.k8s.io/tokenreviews`; matching the prior role's verbs +does not make those new principals an unchanged access scope. It is one red +access-scope-expansion item in the single daily batch, including exact binding +revocation recovery. No per-item founder request is needed. The first prepared +blue receipt is preserved as superseded by the classification correction; no +native action occurred. T03/T04 and ordered predecessor gates remain unchanged. + ## Consumers switch over, one at a time ```task