From 31aa4101fbd6d5e4dd19df2c2a2695809b724a64 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 10 Nov 2026 21:15:38 +0100 Subject: [PATCH 1/7] fix(storage): trust the dedicated Longhorn migration image publishers --- .github/workflows/ci.yaml | 8 ++-- .../best-practices/verify-app-images.yaml | 44 ++++++++++++++++++- .../tests/arc-staging/image_routing_test.go | 8 ++++ ...patibility-image-verifier-prerequisite.sh} | 13 ++++-- talos/cluster/verify-first-party-images.yaml | 9 ++++ 5 files changed, 73 insertions(+), 9 deletions(-) rename scripts/tests/{test-coroot-image-verifier-prerequisite.sh => test-compatibility-image-verifier-prerequisite.sh} (78%) mode change 100755 => 100644 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 001cccb942..582ea7bdd9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -413,7 +413,7 @@ jobs: - 'scripts/tests/test-validate-image-verifier-liveness.sh' # Stages exact admission and pull-time signer routes before the # compatibility-image workloads consume them. - - 'scripts/tests/test-coroot-image-verifier-prerequisite.sh' + - 'scripts/tests/test-compatibility-image-verifier-prerequisite.sh' # The trigger-set test for the liveness WORKFLOW (listed above), so a # PR that edits only the test still runs it. - 'scripts/tests/test-validate-image-verifier-liveness-workflow.sh' @@ -2548,11 +2548,11 @@ jobs: bash scripts/tests/test-validate-image-verifier-liveness.sh bash scripts/tests/test-validate-image-verifier-liveness-workflow.sh - - name: 🔏 Validate Coroot compatibility-image verifier prerequisite + - name: 🔏 Validate compatibility-image verifier prerequisites if: needs.changes.outputs.k8s == 'true' run: | - shellcheck scripts/tests/test-coroot-image-verifier-prerequisite.sh - bash scripts/tests/test-coroot-image-verifier-prerequisite.sh + shellcheck scripts/tests/test-compatibility-image-verifier-prerequisite.sh + bash scripts/tests/test-compatibility-image-verifier-prerequisite.sh - name: 🔏 Validate image-signature enforcement probe if: needs.changes.outputs.k8s == 'true' diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/verify-app-images.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/verify-app-images.yaml index b482b931d6..f1ef57ad13 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/verify-app-images.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/verify-app-images.yaml @@ -21,8 +21,8 @@ metadata: publish-app for app images, the platform's dedicated compatibility-image publishers, each Crossplane provider's own package workflow, KSail's publish-ksail-analysis-runner.yaml workflow, the mail bootstrap publisher, - the dedicated customer identity publisher, the source-built Dex - publisher, and KSail's release workflow. + the dedicated customer identity, Dex and Longhorn migration publishers, + and KSail's release workflow. Each class is verified only against its own identity; third-party images admit unverified. spec: @@ -112,6 +112,18 @@ spec: identities: - issuer: https://token.actions.githubusercontent.com subjectRegExp: ^https://github\.com/devantler-tech/platform/\.github/workflows/publish-coroot-checkpoint-hotfix\.yaml@refs/heads/main$ + - name: publishlonghornretirement + cosign: + keyless: + identities: + - issuer: https://token.actions.githubusercontent.com + subjectRegExp: ^https://github\.com/devantler-tech/platform/\.github/workflows/publish-longhorn-v1-retirement\.yaml@refs/heads/main$ + - name: publishlonghornv2 + cosign: + keyless: + identities: + - issuer: https://token.actions.githubusercontent.com + subjectRegExp: ^https://github\.com/devantler-tech/platform/\.github/workflows/publish-longhorn-v2-only\.yaml@refs/heads/main$ # The reports-controller repair is signed only by its dedicated publisher. - name: publishkyvernoreports cosign: @@ -235,6 +247,12 @@ spec: image != 'ghcr.io/devantler-tech/platform-dex' && !image.startsWith('ghcr.io/devantler-tech/platform-dex:') && !image.startsWith('ghcr.io/devantler-tech/platform-dex@') && + image != 'ghcr.io/devantler-tech/platform-longhorn-instance-manager' && + !image.startsWith('ghcr.io/devantler-tech/platform-longhorn-instance-manager:') && + !image.startsWith('ghcr.io/devantler-tech/platform-longhorn-instance-manager@') && + image != 'ghcr.io/devantler-tech/platform-longhorn-manager' && + !image.startsWith('ghcr.io/devantler-tech/platform-longhorn-manager:') && + !image.startsWith('ghcr.io/devantler-tech/platform-longhorn-manager@') && image != 'ghcr.io/devantler-tech/platform-kubescape-storage' && !image.startsWith('ghcr.io/devantler-tech/platform-kubescape-storage:') && !image.startsWith('ghcr.io/devantler-tech/platform-kubescape-storage@') && @@ -348,6 +366,28 @@ spec: message: >- Coroot node-agent compatibility image is not signed by the platform repository's dedicated main-branch publisher (keyless cosign) + - expression: |- + (images.containers + images.initContainers + images.ephemeralContainers) + .filter(image, + image == 'ghcr.io/devantler-tech/platform-longhorn-instance-manager' || + image.startsWith('ghcr.io/devantler-tech/platform-longhorn-instance-manager:') || + image.startsWith('ghcr.io/devantler-tech/platform-longhorn-instance-manager@')) + .map(image, verifyImageSignatures(image, [attestors.publishlonghornretirement])) + .all(e, e > 0) + message: >- + The Longhorn V1 retirement image must be signed by its dedicated + main-branch publisher. + - expression: |- + (images.containers + images.initContainers + images.ephemeralContainers) + .filter(image, + image == 'ghcr.io/devantler-tech/platform-longhorn-manager' || + image.startsWith('ghcr.io/devantler-tech/platform-longhorn-manager:') || + image.startsWith('ghcr.io/devantler-tech/platform-longhorn-manager@')) + .map(image, verifyImageSignatures(image, [attestors.publishlonghornv2])) + .all(e, e > 0) + message: >- + The Longhorn V2-only manager image must be signed by its dedicated + main-branch publisher. - expression: |- (images.containers + images.initContainers + images.ephemeralContainers) .filter(image, diff --git a/scripts/tests/arc-staging/image_routing_test.go b/scripts/tests/arc-staging/image_routing_test.go index f0a188fe7b..500178244b 100644 --- a/scripts/tests/arc-staging/image_routing_test.go +++ b/scripts/tests/arc-staging/image_routing_test.go @@ -19,6 +19,8 @@ func TestAnalysisImageAdmissionRoutingAndSignerIsolation(t *testing.T) { policy := readYAML(t, "k8s/bases/infrastructure/cluster-policies/best-practices/verify-app-images.yaml") evaluator := newImagePolicyEvaluator(t, policy) identityPublisher := "https://github.com/devantler-tech/business-site/.github/workflows/publish-identity.yaml@refs/heads/main" + retirementPublisher := "https://github.com/devantler-tech/platform/.github/workflows/publish-longhorn-v1-retirement.yaml@refs/heads/main" + v2Publisher := "https://github.com/devantler-tech/platform/.github/workflows/publish-longhorn-v2-only.yaml@refs/heads/main" tests := []struct { repository, attestor, subject string }{ @@ -33,6 +35,10 @@ func TestAnalysisImageAdmissionRoutingAndSignerIsolation(t *testing.T) { {"ghcr.io/devantler-tech/platform-kubescape-storage", "publishkubescapestorage", "https://github.com/devantler-tech/platform/.github/workflows/publish-kubescape-storage-hotfix.yaml@refs/heads/main"}, {"ghcr.io/devantler-tech/platform-kubescape-node-agent", "publishkubescapenodeagent", "https://github.com/devantler-tech/platform/.github/workflows/publish-kubescape-node-agent-hotfix.yaml@refs/heads/main"}, {"ghcr.io/devantler-tech/platform-coroot-node-agent", "publishcorootnodeagent", "https://github.com/devantler-tech/platform/.github/workflows/publish-coroot-node-agent-hotfix.yaml@refs/heads/main"}, + {"ghcr.io/devantler-tech/platform-longhorn-instance-manager", "publishlonghornretirement", retirementPublisher}, + {"ghcr.io/devantler-tech/platform-longhorn-manager", "publishlonghornv2", v2Publisher}, + {"ghcr.io/devantler-tech/platform-longhorn-instance-manager-other", "publishapp", "https://github.com/devantler-tech/actions/.github/workflows/publish-app.yaml@" + strings.Repeat("b", 40)}, + {"ghcr.io/devantler-tech/platform-longhorn-manager/other", "publishapp", "https://github.com/devantler-tech/actions/.github/workflows/publish-app.yaml@" + strings.Repeat("b", 40)}, {"ghcr.io/devantler-tech/platform-coroot", "publishcorootcheckpoint", "https://github.com/devantler-tech/platform/.github/workflows/publish-coroot-checkpoint-hotfix.yaml@refs/heads/main"}, {"ghcr.io/devantler-tech/platform-coroot-cluster-agent", "publishcorootcheckpoint", "https://github.com/devantler-tech/platform/.github/workflows/publish-coroot-checkpoint-hotfix.yaml@refs/heads/main"}, {"ghcr.io/devantler-tech/platform-coroot-other", "publishapp", "https://github.com/devantler-tech/actions/.github/workflows/publish-app.yaml@" + strings.Repeat("b", 40)}, @@ -71,6 +77,8 @@ func TestAnalysisImageAdmissionRoutingAndSignerIsolation(t *testing.T) { {"identity pull request ref", strings.Replace(identityPublisher, "refs/heads/main", "refs/pull/1/merge", 1), githubIssuer, fixture.attestor == ""}, {"identity lookalike branch", identityPublisher + "-other", githubIssuer, fixture.attestor == ""}, {"checkpoint signer", "https://github.com/devantler-tech/platform/.github/workflows/publish-coroot-checkpoint-hotfix.yaml@refs/heads/main", githubIssuer, fixture.attestor == "" || fixture.attestor == "publishcorootcheckpoint"}, + {"Longhorn retirement signer", retirementPublisher, githubIssuer, fixture.attestor == "" || fixture.attestor == "publishlonghornretirement"}, + {"Longhorn V2-only signer", v2Publisher, githubIssuer, fixture.attestor == "" || fixture.attestor == "publishlonghornv2"}, {"reports signer", "https://github.com/devantler-tech/platform/.github/workflows/publish-kyverno-reports-hotfix.yaml@refs/heads/main", githubIssuer, fixture.attestor == "" || fixture.attestor == "publishkyvernoreports"}, {"migrate signer", "https://github.com/devantler-tech/platform/.github/workflows/publish-kyverno-migration-runtime.yaml@refs/heads/main", githubIssuer, fixture.attestor == "" || fixture.attestor == "publishkyvernomigrate"}, {"background signer", "https://github.com/devantler-tech/platform/.github/workflows/publish-kyverno-background-hotfix.yaml@refs/heads/main", githubIssuer, fixture.attestor == "" || fixture.attestor == "publishkyvernobackground"}, diff --git a/scripts/tests/test-coroot-image-verifier-prerequisite.sh b/scripts/tests/test-compatibility-image-verifier-prerequisite.sh old mode 100755 new mode 100644 similarity index 78% rename from scripts/tests/test-coroot-image-verifier-prerequisite.sh rename to scripts/tests/test-compatibility-image-verifier-prerequisite.sh index 72639c8956..8053e3f508 --- a/scripts/tests/test-coroot-image-verifier-prerequisite.sh +++ b/scripts/tests/test-compatibility-image-verifier-prerequisite.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -# Regression contract for staging the two agent compatibility-image signer -# routes before either DaemonSet consumes those images in production. +# Regression contract for staging dedicated compatibility-image signers +# before production workloads consume those images. set -euo pipefail @@ -53,9 +53,16 @@ while IFS='|' read -r repository attestor workflow; do })" || fail "the Talos verifier rule for ${repository} is missing" [[ "${node_identity}" == "${expected_identity}" ]] || fail "the Talos and admission identities differ for ${repository}" + first_rule="$({ + REPOSITORY="${repository}" yq -er '[.rules[].image | select(. == strenv(REPOSITORY) or test("^ghcr[.]io/devantler-tech/[*]$"))] | .[0]' "${node_verifier}" + })" || fail "the Talos rule order is unreadable for ${repository}" + [[ "${first_rule}" == "${repository}" ]] || + fail "the Talos generic rule shadows ${repository}" done <<'ROUTES' ghcr.io/devantler-tech/platform-kubescape-node-agent|publishkubescapenodeagent|publish-kubescape-node-agent-hotfix ghcr.io/devantler-tech/platform-coroot-node-agent|publishcorootnodeagent|publish-coroot-node-agent-hotfix +ghcr.io/devantler-tech/platform-longhorn-instance-manager|publishlonghornretirement|publish-longhorn-v1-retirement +ghcr.io/devantler-tech/platform-longhorn-manager|publishlonghornv2|publish-longhorn-v2-only ROUTES -printf 'Agent compatibility-image verifier prerequisite is valid.\n' +printf 'Compatibility-image verifier prerequisites are valid.\n' diff --git a/talos/cluster/verify-first-party-images.yaml b/talos/cluster/verify-first-party-images.yaml index 0fd3a6a86a..f24b371c75 100644 --- a/talos/cluster/verify-first-party-images.yaml +++ b/talos/cluster/verify-first-party-images.yaml @@ -224,6 +224,15 @@ rules: keyless: issuer: https://token.actions.githubusercontent.com subjectRegex: ^https://github\.com/devantler-tech/platform/\.github/workflows/publish-dex-security-image\.yaml@refs/heads/main$ + # The migration images have separate, exact main-branch publishers. + - image: ghcr.io/devantler-tech/platform-longhorn-instance-manager + keyless: + issuer: https://token.actions.githubusercontent.com + subjectRegex: ^https://github\.com/devantler-tech/platform/\.github/workflows/publish-longhorn-v1-retirement\.yaml@refs/heads/main$ + - image: ghcr.io/devantler-tech/platform-longhorn-manager + keyless: + issuer: https://token.actions.githubusercontent.com + subjectRegex: ^https://github\.com/devantler-tech/platform/\.github/workflows/publish-longhorn-v2-only\.yaml@refs/heads/main$ # First-party APP images retain legacy full-commit verification and accept the # two reviewed maintained publisher revisions (v8.0.0 #5213, v19.0.5 #5404). # Mutable refs, tags and other maintained commits are refused, as in the app From 546df99a3f8166c27acfced578a9c706dcb3e747 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 10 Nov 2026 21:16:14 +0100 Subject: [PATCH 2/7] fix(ci): preserve the compatibility verifier executable mode --- scripts/tests/test-compatibility-image-verifier-prerequisite.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/tests/test-compatibility-image-verifier-prerequisite.sh diff --git a/scripts/tests/test-compatibility-image-verifier-prerequisite.sh b/scripts/tests/test-compatibility-image-verifier-prerequisite.sh old mode 100644 new mode 100755 From 71807388d431105f088fd1b7c8e23a23b527f0bd Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 10 Nov 2026 21:22:52 +0100 Subject: [PATCH 3/7] =?UTF-8?q?fix(storage):=20preserve=20the=20benchmark?= =?UTF-8?q?=20instance=20manager=E2=80=99s=20host=20identity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...longhorn-benchmark-retirement-context.yaml | 69 ++++++++++ .../hetzner/infrastructure/kustomization.yaml | 1 + .../kyverno-test.yaml | 27 ++++ .../patched-resources.yaml | 24 ++++ .../resources.yaml | 126 ++++++++++++++++++ 5 files changed, 247 insertions(+) create mode 100644 k8s/providers/hetzner/infrastructure/cluster-policies/set-longhorn-benchmark-retirement-context.yaml create mode 100644 tests/longhorn-benchmark-retirement-runtime/kyverno-test.yaml create mode 100644 tests/longhorn-benchmark-retirement-runtime/patched-resources.yaml create mode 100644 tests/longhorn-benchmark-retirement-runtime/resources.yaml diff --git a/k8s/providers/hetzner/infrastructure/cluster-policies/set-longhorn-benchmark-retirement-context.yaml b/k8s/providers/hetzner/infrastructure/cluster-policies/set-longhorn-benchmark-retirement-context.yaml new file mode 100644 index 0000000000..9ab51ec449 --- /dev/null +++ b/k8s/providers/hetzner/infrastructure/cluster-policies/set-longhorn-benchmark-retirement-context.yaml @@ -0,0 +1,69 @@ +--- +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: set-longhorn-benchmark-retirement-context + annotations: + pod-policies.kyverno.io/autogen-controllers: none + policies.kyverno.io/title: Longhorn benchmark retirement runtime + policies.kyverno.io/category: Storage + policies.kyverno.io/subject: Pod + policies.kyverno.io/description: >- + Preserve the host runtime identity for the temporary, already privileged + Longhorn instance manager that retires benchmark scratch. +spec: + background: false + failurePolicy: Fail + rules: + - name: use-host-runtime-identity + match: + any: + - resources: + kinds: [Pod] + namespaces: [longhorn-system] + names: ["instance-manager-*"] + operations: [CREATE] + selector: + matchLabels: + longhorn.io/component: instance-manager + preconditions: + all: + - key: "{{ request.object.spec.serviceAccountName || '' }}" + operator: Equals + value: longhorn-service-account + - key: "{{ length(request.object.metadata.ownerReferences || `[]`) }}" + operator: Equals + value: 1 + - key: "{{ request.object.metadata.ownerReferences[0].apiVersion || '' }}" + operator: Equals + value: longhorn.io/v1beta2 + - key: "{{ request.object.metadata.ownerReferences[0].kind || '' }}" + operator: Equals + value: InstanceManager + - key: "{{ request.object.metadata.ownerReferences[0].name || '' }}" + operator: Equals + value: "{{ request.object.metadata.name }}" + - key: "{{ request.object.metadata.ownerReferences[0].controller || `false` }}" + operator: Equals + value: true + - key: "{{ length(request.object.spec.containers) }}" + operator: Equals + value: 1 + - key: "{{ request.object.spec.containers[0].name || '' }}" + operator: Equals + value: instance-manager + - key: "{{ request.object.spec.containers[0].image || '' }}" + operator: Equals + value: ghcr.io/devantler-tech/platform-longhorn-instance-manager:v1.13.0-retirement.1-725bb1d09517c84f8b73ff2c0cf175421df351f1-1@sha256:93cfc83d8bab91f9b6fbffe20bf9bdaca28e877d91d7b740cae003bf6853059f + - key: "{{ request.object.spec.containers[0].securityContext.privileged || `false` }}" + operator: Equals + value: true + mutate: + patchStrategicMerge: + spec: + containers: + - (name): instance-manager + securityContext: + +(runAsUser): 0 + +(runAsGroup): 0 + +(runAsNonRoot): false diff --git a/k8s/providers/hetzner/infrastructure/kustomization.yaml b/k8s/providers/hetzner/infrastructure/kustomization.yaml index ae3920fbb8..7850eac98e 100644 --- a/k8s/providers/hetzner/infrastructure/kustomization.yaml +++ b/k8s/providers/hetzner/infrastructure/kustomization.yaml @@ -69,6 +69,7 @@ resources: # runtime-generated DaemonSet. Both are exact and version-coupled (#3931). - cluster-policies/remove-longhorn-registrar-prestop.yaml - cluster-policies/remove-existing-longhorn-registrar-prestop.yaml + - cluster-policies/set-longhorn-benchmark-retirement-context.yaml # Prod-only: retrofit the two non-privilege C-0211 fields onto the four # Longhorn CSI sidecar Deployments that predate the longhorn-system # baseline-context opt-in. mutate-existing only -- nothing writes those diff --git a/tests/longhorn-benchmark-retirement-runtime/kyverno-test.yaml b/tests/longhorn-benchmark-retirement-runtime/kyverno-test.yaml new file mode 100644 index 0000000000..b69aa96edc --- /dev/null +++ b/tests/longhorn-benchmark-retirement-runtime/kyverno-test.yaml @@ -0,0 +1,27 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: longhorn-benchmark-retirement-runtime +policies: + - ../../k8s/providers/hetzner/infrastructure/cluster-policies/set-longhorn-benchmark-retirement-context.yaml +resources: + - resources.yaml +results: + - policy: set-longhorn-benchmark-retirement-context + rule: use-host-runtime-identity + resources: + - instance-manager-retirement + kind: Pod + result: pass + patchedResources: patched-resources.yaml + - policy: set-longhorn-benchmark-retirement-context + rule: use-host-runtime-identity + resources: + - instance-manager-other-image + - instance-manager-other-owner + - instance-manager-other-account + - instance-manager-other-namespace + - instance-manager-unprivileged + kind: Pod + result: skip diff --git a/tests/longhorn-benchmark-retirement-runtime/patched-resources.yaml b/tests/longhorn-benchmark-retirement-runtime/patched-resources.yaml new file mode 100644 index 0000000000..4237b0233c --- /dev/null +++ b/tests/longhorn-benchmark-retirement-runtime/patched-resources.yaml @@ -0,0 +1,24 @@ +--- +apiVersion: v1 +kind: Pod +metadata: + name: instance-manager-retirement + namespace: longhorn-system + labels: + longhorn.io/component: instance-manager + ownerReferences: + - apiVersion: longhorn.io/v1beta2 + kind: InstanceManager + name: instance-manager-retirement + uid: 01234567-89ab-cdef-0123-456789abcdef + controller: true +spec: + serviceAccountName: longhorn-service-account + containers: + - name: instance-manager + image: ghcr.io/devantler-tech/platform-longhorn-instance-manager:v1.13.0-retirement.1-725bb1d09517c84f8b73ff2c0cf175421df351f1-1@sha256:93cfc83d8bab91f9b6fbffe20bf9bdaca28e877d91d7b740cae003bf6853059f + securityContext: + privileged: true + runAsUser: 0 + runAsGroup: 0 + runAsNonRoot: false diff --git a/tests/longhorn-benchmark-retirement-runtime/resources.yaml b/tests/longhorn-benchmark-retirement-runtime/resources.yaml new file mode 100644 index 0000000000..37ebd829b5 --- /dev/null +++ b/tests/longhorn-benchmark-retirement-runtime/resources.yaml @@ -0,0 +1,126 @@ +--- +apiVersion: v1 +kind: Pod +metadata: + name: instance-manager-retirement + namespace: longhorn-system + labels: + longhorn.io/component: instance-manager + ownerReferences: + - apiVersion: longhorn.io/v1beta2 + kind: InstanceManager + name: instance-manager-retirement + uid: 01234567-89ab-cdef-0123-456789abcdef + controller: true +spec: + serviceAccountName: longhorn-service-account + containers: + - name: instance-manager + image: ghcr.io/devantler-tech/platform-longhorn-instance-manager:v1.13.0-retirement.1-725bb1d09517c84f8b73ff2c0cf175421df351f1-1@sha256:93cfc83d8bab91f9b6fbffe20bf9bdaca28e877d91d7b740cae003bf6853059f + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: instance-manager-other-image + namespace: longhorn-system + labels: + longhorn.io/component: instance-manager + ownerReferences: + - apiVersion: longhorn.io/v1beta2 + kind: InstanceManager + name: instance-manager-other-image + uid: 01234567-89ab-cdef-0123-456789abcdef + controller: true +spec: + serviceAccountName: longhorn-service-account + containers: + - name: instance-manager + image: docker.io/longhornio/longhorn-instance-manager:v1.13.0 + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: instance-manager-other-owner + namespace: longhorn-system + labels: + longhorn.io/component: instance-manager + ownerReferences: + - apiVersion: longhorn.io/v1beta2 + kind: Volume + name: instance-manager-other-owner + uid: 01234567-89ab-cdef-0123-456789abcdef + controller: true +spec: + serviceAccountName: longhorn-service-account + containers: + - name: instance-manager + image: ghcr.io/devantler-tech/platform-longhorn-instance-manager:v1.13.0-retirement.1-725bb1d09517c84f8b73ff2c0cf175421df351f1-1@sha256:93cfc83d8bab91f9b6fbffe20bf9bdaca28e877d91d7b740cae003bf6853059f + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: instance-manager-other-account + namespace: longhorn-system + labels: + longhorn.io/component: instance-manager + ownerReferences: + - apiVersion: longhorn.io/v1beta2 + kind: InstanceManager + name: instance-manager-other-account + uid: 01234567-89ab-cdef-0123-456789abcdef + controller: true +spec: + serviceAccountName: default + containers: + - name: instance-manager + image: ghcr.io/devantler-tech/platform-longhorn-instance-manager:v1.13.0-retirement.1-725bb1d09517c84f8b73ff2c0cf175421df351f1-1@sha256:93cfc83d8bab91f9b6fbffe20bf9bdaca28e877d91d7b740cae003bf6853059f + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: instance-manager-other-namespace + namespace: default + labels: + longhorn.io/component: instance-manager + ownerReferences: + - apiVersion: longhorn.io/v1beta2 + kind: InstanceManager + name: instance-manager-other-namespace + uid: 01234567-89ab-cdef-0123-456789abcdef + controller: true +spec: + serviceAccountName: longhorn-service-account + containers: + - name: instance-manager + image: ghcr.io/devantler-tech/platform-longhorn-instance-manager:v1.13.0-retirement.1-725bb1d09517c84f8b73ff2c0cf175421df351f1-1@sha256:93cfc83d8bab91f9b6fbffe20bf9bdaca28e877d91d7b740cae003bf6853059f + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: instance-manager-unprivileged + namespace: longhorn-system + labels: + longhorn.io/component: instance-manager + ownerReferences: + - apiVersion: longhorn.io/v1beta2 + kind: InstanceManager + name: instance-manager-unprivileged + uid: 01234567-89ab-cdef-0123-456789abcdef + controller: true +spec: + serviceAccountName: longhorn-service-account + containers: + - name: instance-manager + image: ghcr.io/devantler-tech/platform-longhorn-instance-manager:v1.13.0-retirement.1-725bb1d09517c84f8b73ff2c0cf175421df351f1-1@sha256:93cfc83d8bab91f9b6fbffe20bf9bdaca28e877d91d7b740cae003bf6853059f + securityContext: + privileged: false From 327530c3fe30f7560f07d0f494adb7b2ff12b343 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 10 Nov 2026 22:04:03 +0100 Subject: [PATCH 4/7] fix(storage): verify the native Longhorn benchmark admission policy --- ...binding-longhorn-v1-benchmark-runtime.yaml | 12 ++ ...-policy-longhorn-v1-benchmark-runtime.yaml | 70 +++++++ ...longhorn-benchmark-retirement-context.yaml | 69 ------- .../hetzner/infrastructure/kustomization.yaml | 4 +- .../testdata/retirement-fence-proof/go.mod | 2 +- .../runtime_admission_test.go | 186 ++++++++++++++++++ .../fake_kubectl_test.go | 54 ++++- ...-inventory-first-party-image-signatures.sh | 6 +- .../test-validate-image-verifier-liveness.sh | 35 ++-- .../kyverno-test.yaml | 27 --- .../patched-resources.yaml | 24 --- .../resources.yaml | 126 ------------ 12 files changed, 332 insertions(+), 283 deletions(-) create mode 100644 k8s/providers/hetzner/infrastructure/cluster-policies/mutating-admission-policy-binding-longhorn-v1-benchmark-runtime.yaml create mode 100644 k8s/providers/hetzner/infrastructure/cluster-policies/mutating-admission-policy-longhorn-v1-benchmark-runtime.yaml delete mode 100644 k8s/providers/hetzner/infrastructure/cluster-policies/set-longhorn-benchmark-retirement-context.yaml create mode 100644 scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/runtime_admission_test.go delete mode 100644 tests/longhorn-benchmark-retirement-runtime/kyverno-test.yaml delete mode 100644 tests/longhorn-benchmark-retirement-runtime/patched-resources.yaml delete mode 100644 tests/longhorn-benchmark-retirement-runtime/resources.yaml diff --git a/k8s/providers/hetzner/infrastructure/cluster-policies/mutating-admission-policy-binding-longhorn-v1-benchmark-runtime.yaml b/k8s/providers/hetzner/infrastructure/cluster-policies/mutating-admission-policy-binding-longhorn-v1-benchmark-runtime.yaml new file mode 100644 index 0000000000..97dc4bf4ee --- /dev/null +++ b/k8s/providers/hetzner/infrastructure/cluster-policies/mutating-admission-policy-binding-longhorn-v1-benchmark-runtime.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingAdmissionPolicyBinding +metadata: + name: longhorn-v1-benchmark-runtime +spec: + policyName: longhorn-v1-benchmark-runtime + matchResources: + matchPolicy: Exact + namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: longhorn-system diff --git a/k8s/providers/hetzner/infrastructure/cluster-policies/mutating-admission-policy-longhorn-v1-benchmark-runtime.yaml b/k8s/providers/hetzner/infrastructure/cluster-policies/mutating-admission-policy-longhorn-v1-benchmark-runtime.yaml new file mode 100644 index 0000000000..ad0c9a725d --- /dev/null +++ b/k8s/providers/hetzner/infrastructure/cluster-policies/mutating-admission-policy-longhorn-v1-benchmark-runtime.yaml @@ -0,0 +1,70 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingAdmissionPolicy +metadata: + name: longhorn-v1-benchmark-runtime +spec: + failurePolicy: Fail + reinvocationPolicy: IfNeeded + matchConstraints: + matchPolicy: Exact + resourceRules: + - apiGroups: [""] + apiVersions: [v1] + operations: [CREATE] + resources: [pods] + scope: Namespaced + matchConditions: + - name: longhorn-controller + expression: >- + request.namespace == 'longhorn-system' && + request.userInfo.username == 'system:serviceaccount:longhorn-system:longhorn-service-account' && + object.metadata.namespace == request.namespace && + object.spec.serviceAccountName == 'longhorn-service-account' + - name: native-v1-manager + expression: >- + has(object.metadata.labels) && + object.metadata.name.matches('^instance-manager-[a-f0-9]{32}$') && + 'longhorn.io/component' in object.metadata.labels && + object.metadata.labels['longhorn.io/component'] == 'instance-manager' && + 'longhorn.io/instance-manager-type' in object.metadata.labels && + object.metadata.labels['longhorn.io/instance-manager-type'] == 'aio' && + 'longhorn.io/data-engine' in object.metadata.labels && + object.metadata.labels['longhorn.io/data-engine'] == 'v1' && + 'longhorn.io/managed-by' in object.metadata.labels && + object.metadata.labels['longhorn.io/managed-by'] == 'longhorn-manager' + - name: instance-manager-owner + expression: >- + has(object.metadata.ownerReferences) && + size(object.metadata.ownerReferences) == 1 && + object.metadata.ownerReferences.all(owner, + owner.apiVersion == 'longhorn.io/v1beta2' && + owner.kind == 'InstanceManager' && + owner.name == object.metadata.name && + owner.uid.matches('^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$') && + has(owner.controller) && owner.controller) + - name: approved-privileged-container + expression: >- + object.spec.restartPolicy == 'Never' && + (!has(object.spec.initContainers) || size(object.spec.initContainers) == 0) && + size(object.spec.containers) == 1 && + object.spec.containers.all(container, + container.name == 'instance-manager' && + container.image == 'ghcr.io/devantler-tech/platform-longhorn-instance-manager:v1.13.0-retirement.1-725bb1d09517c84f8b73ff2c0cf175421df351f1-1@sha256:93cfc83d8bab91f9b6fbffe20bf9bdaca28e877d91d7b740cae003bf6853059f' && + has(container.securityContext) && + has(container.securityContext.privileged) && container.securityContext.privileged && + !has(container.securityContext.runAsUser) && !has(container.securityContext.runAsGroup) && + (!has(container.securityContext.runAsNonRoot) || !container.securityContext.runAsNonRoot)) + - name: no-pod-identity-override + expression: >- + !has(object.spec.securityContext) || + (!has(object.spec.securityContext.runAsUser) && !has(object.spec.securityContext.runAsGroup) && + (!has(object.spec.securityContext.runAsNonRoot) || !object.spec.securityContext.runAsNonRoot)) + mutations: + - patchType: JSONPatch + jsonPatch: + expression: >- + [ + JSONPatch{op: 'add', path: '/spec/containers/0/securityContext/runAsUser', value: 0}, + JSONPatch{op: 'add', path: '/spec/containers/0/securityContext/runAsGroup', value: 0} + ] diff --git a/k8s/providers/hetzner/infrastructure/cluster-policies/set-longhorn-benchmark-retirement-context.yaml b/k8s/providers/hetzner/infrastructure/cluster-policies/set-longhorn-benchmark-retirement-context.yaml deleted file mode 100644 index 9ab51ec449..0000000000 --- a/k8s/providers/hetzner/infrastructure/cluster-policies/set-longhorn-benchmark-retirement-context.yaml +++ /dev/null @@ -1,69 +0,0 @@ ---- -apiVersion: kyverno.io/v1 -kind: ClusterPolicy -metadata: - name: set-longhorn-benchmark-retirement-context - annotations: - pod-policies.kyverno.io/autogen-controllers: none - policies.kyverno.io/title: Longhorn benchmark retirement runtime - policies.kyverno.io/category: Storage - policies.kyverno.io/subject: Pod - policies.kyverno.io/description: >- - Preserve the host runtime identity for the temporary, already privileged - Longhorn instance manager that retires benchmark scratch. -spec: - background: false - failurePolicy: Fail - rules: - - name: use-host-runtime-identity - match: - any: - - resources: - kinds: [Pod] - namespaces: [longhorn-system] - names: ["instance-manager-*"] - operations: [CREATE] - selector: - matchLabels: - longhorn.io/component: instance-manager - preconditions: - all: - - key: "{{ request.object.spec.serviceAccountName || '' }}" - operator: Equals - value: longhorn-service-account - - key: "{{ length(request.object.metadata.ownerReferences || `[]`) }}" - operator: Equals - value: 1 - - key: "{{ request.object.metadata.ownerReferences[0].apiVersion || '' }}" - operator: Equals - value: longhorn.io/v1beta2 - - key: "{{ request.object.metadata.ownerReferences[0].kind || '' }}" - operator: Equals - value: InstanceManager - - key: "{{ request.object.metadata.ownerReferences[0].name || '' }}" - operator: Equals - value: "{{ request.object.metadata.name }}" - - key: "{{ request.object.metadata.ownerReferences[0].controller || `false` }}" - operator: Equals - value: true - - key: "{{ length(request.object.spec.containers) }}" - operator: Equals - value: 1 - - key: "{{ request.object.spec.containers[0].name || '' }}" - operator: Equals - value: instance-manager - - key: "{{ request.object.spec.containers[0].image || '' }}" - operator: Equals - value: ghcr.io/devantler-tech/platform-longhorn-instance-manager:v1.13.0-retirement.1-725bb1d09517c84f8b73ff2c0cf175421df351f1-1@sha256:93cfc83d8bab91f9b6fbffe20bf9bdaca28e877d91d7b740cae003bf6853059f - - key: "{{ request.object.spec.containers[0].securityContext.privileged || `false` }}" - operator: Equals - value: true - mutate: - patchStrategicMerge: - spec: - containers: - - (name): instance-manager - securityContext: - +(runAsUser): 0 - +(runAsGroup): 0 - +(runAsNonRoot): false diff --git a/k8s/providers/hetzner/infrastructure/kustomization.yaml b/k8s/providers/hetzner/infrastructure/kustomization.yaml index 7850eac98e..7d2082e8c5 100644 --- a/k8s/providers/hetzner/infrastructure/kustomization.yaml +++ b/k8s/providers/hetzner/infrastructure/kustomization.yaml @@ -69,7 +69,9 @@ resources: # runtime-generated DaemonSet. Both are exact and version-coupled (#3931). - cluster-policies/remove-longhorn-registrar-prestop.yaml - cluster-policies/remove-existing-longhorn-registrar-prestop.yaml - - cluster-policies/set-longhorn-benchmark-retirement-context.yaml + # Keep the existing root identity for the exact temporary V1 benchmark runtime. + - cluster-policies/mutating-admission-policy-longhorn-v1-benchmark-runtime.yaml + - cluster-policies/mutating-admission-policy-binding-longhorn-v1-benchmark-runtime.yaml # Prod-only: retrofit the two non-privilege C-0211 fields onto the four # Longhorn CSI sidecar Deployments that predate the longhorn-system # baseline-context opt-in. mutate-existing only -- nothing writes those diff --git a/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.mod b/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.mod index 59a9307b19..865711dcc6 100644 --- a/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.mod +++ b/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.mod @@ -7,6 +7,7 @@ require ( k8s.io/apimachinery v0.36.4 k8s.io/apiserver v0.36.4 k8s.io/client-go v0.36.4 + sigs.k8s.io/yaml v1.6.0 ) require ( @@ -80,5 +81,4 @@ require ( sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.3 // indirect - sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/runtime_admission_test.go b/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/runtime_admission_test.go new file mode 100644 index 0000000000..4c5847a232 --- /dev/null +++ b/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/runtime_admission_test.go @@ -0,0 +1,186 @@ +package retirementproof_test + +import ( + "context" + "os" + "reflect" + "testing" + + registration "k8s.io/api/admissionregistration/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" + plugincel "k8s.io/apiserver/pkg/admission/plugin/cel" + "k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch" + "k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" + celconfig "k8s.io/apiserver/pkg/apis/cel" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/cel/environment" + "sigs.k8s.io/yaml" +) + +const retirementImage = "ghcr.io/devantler-tech/platform-longhorn-instance-manager:v1.13.0-retirement.1-725bb1d09517c84f8b73ff2c0cf175421df351f1-1@sha256:93cfc83d8bab91f9b6fbffe20bf9bdaca28e877d91d7b740cae003bf6853059f" +const longhornCreator = "system:serviceaccount:longhorn-system:longhorn-service-account" +const retirementPolicyPath = "../../../../k8s/providers/hetzner/infrastructure/cluster-policies/" + +func TestNativeRetirementRuntimeAdmission(t *testing.T) { + var policy registration.MutatingAdmissionPolicy + readRetirementManifest(t, "mutating-admission-policy-longhorn-v1-benchmark-runtime.yaml", &policy) + var binding registration.MutatingAdmissionPolicyBinding + readRetirementManifest(t, "mutating-admission-policy-binding-longhorn-v1-benchmark-runtime.yaml", &binding) + scope := registration.NamespacedScope + match := registration.Exact + wantMatch := ®istration.MatchResources{ + MatchPolicy: &match, + ResourceRules: []registration.NamedRuleWithOperations{{ + RuleWithOperations: registration.RuleWithOperations{ + Operations: []registration.OperationType{registration.Create}, + Rule: registration.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}, Scope: &scope}, + }, + }}, + } + wantBindingMatch := ®istration.MatchResources{ + MatchPolicy: &match, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"kubernetes.io/metadata.name": "longhorn-system"}, + }, + } + if !reflect.DeepEqual(policy.Spec.MatchConstraints, wantMatch) || !reflect.DeepEqual(binding.Spec.MatchResources, wantBindingMatch) || + policy.Name != "longhorn-v1-benchmark-runtime" || binding.Name != policy.Name || binding.Spec.PolicyName != policy.Name || + binding.Spec.ParamRef != nil || policy.Spec.FailurePolicy == nil || *policy.Spec.FailurePolicy != registration.Fail || + len(policy.Spec.Mutations) != 1 || policy.Spec.Mutations[0].JSONPatch == nil || len(policy.Spec.MatchConditions) == 0 { + t.Fatal("deployed admission policy must match only Longhorn Pod creation with fail-closed semantics") + } + compiler, err := plugincel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) + if err != nil { + t.Fatal(err) + } + expressions := make([]plugincel.ExpressionAccessor, 0, len(policy.Spec.MatchConditions)) + for _, condition := range policy.Spec.MatchConditions { + item := matchconditions.MatchCondition(condition) + expressions = append(expressions, &item) + } + matcher := matchconditions.NewMatcher(compiler.CompileCondition(expressions, plugincel.OptionalVariableDeclarations{}, environment.StoredExpressions), + policy.Spec.FailurePolicy, "mutating", "policy", policy.Name) + mutation := compiler.CompileMutatingEvaluator(&patch.JSONPatchCondition{Expression: policy.Spec.Mutations[0].JSONPatch.Expression}, + plugincel.OptionalVariableDeclarations{HasPatchTypes: true}, environment.StoredExpressions) + + for _, test := range []struct { + name string + edit func(*unstructured.Unstructured, *user.DefaultInfo) + matches bool + }{ + {"native controller", func(*unstructured.Unstructured, *user.DefaultInfo) {}, true}, + {"different creator", func(_ *unstructured.Unstructured, u *user.DefaultInfo) { u.Name = "system:admin" }, false}, + {"different service account", func(p *unstructured.Unstructured, _ *user.DefaultInfo) { + retirementPodSpec(p)["serviceAccountName"] = "default" + }, false}, + {"different namespace", func(p *unstructured.Unstructured, _ *user.DefaultInfo) { p.SetNamespace("other") }, false}, + {"V2", func(p *unstructured.Unstructured, _ *user.DefaultInfo) { + labels := p.GetLabels() + labels["longhorn.io/data-engine"] = "v2" + p.SetLabels(labels) + }, false}, + {"no labels", func(p *unstructured.Unstructured, _ *user.DefaultInfo) { p.SetLabels(nil) }, false}, + {"different image", func(p *unstructured.Unstructured, _ *user.DefaultInfo) { + retirementPodContainers(p)[0].(map[string]any)["image"] = retirementImage[:len(retirementImage)-1] + "b" + }, false}, + {"stock image", func(p *unstructured.Unstructured, _ *user.DefaultInfo) { + retirementPodContainers(p)[0].(map[string]any)["image"] = "longhornio/longhorn-instance-manager:v1.13.0" + }, false}, + {"extra container", func(p *unstructured.Unstructured, _ *user.DefaultInfo) { + retirementPodSpec(p)["containers"] = append(retirementPodContainers(p), map[string]any{"name": "other"}) + }, false}, + {"unprivileged", func(p *unstructured.Unstructured, _ *user.DefaultInfo) { + retirementContainerSecurity(p)["privileged"] = false + }, false}, + {"explicit container UID", func(p *unstructured.Unstructured, _ *user.DefaultInfo) { + retirementContainerSecurity(p)["runAsUser"] = int64(65532) + }, false}, + {"explicit pod group", func(p *unstructured.Unstructured, _ *user.DefaultInfo) { + retirementPodSpec(p)["securityContext"].(map[string]any)["runAsGroup"] = int64(65532) + }, false}, + {"nonroot required", func(p *unstructured.Unstructured, _ *user.DefaultInfo) { + retirementPodSpec(p)["securityContext"].(map[string]any)["runAsNonRoot"] = true + }, false}, + {"unpaired owner", func(p *unstructured.Unstructured, _ *user.DefaultInfo) { p.SetName("instance-manager-other") }, false}, + {"missing owner", func(p *unstructured.Unstructured, _ *user.DefaultInfo) { p.SetOwnerReferences(nil) }, false}, + } { + t.Run(test.name, func(t *testing.T) { + pod := retirementRuntimePod() + creator := &user.DefaultInfo{Name: longhornCreator} + test.edit(pod, creator) + gvk := schema.GroupVersionKind{Version: "v1", Kind: "Pod"} + gvr := schema.GroupVersionResource{Version: "v1", Resource: "pods"} + a := admission.NewAttributesRecord(pod, nil, gvk, pod.GetNamespace(), pod.GetName(), gvr, "", admission.Create, &metav1.CreateOptions{}, false, creator) + v := &admission.VersionedAttributes{Attributes: a, VersionedKind: gvk, VersionedObject: pod} + result := matcher.Match(context.Background(), v, nil, nil) + if result.Error != nil || result.Matches != test.matches { + t.Fatalf("match = %+v, want %v", result, test.matches) + } + if !result.Matches { + return + } + out, err := patch.NewJSONPatcher(mutation).Patch(compiler.CreateContext(context.Background()), + patch.Request{MatchedResource: gvr, VersionedAttributes: v, ObjectInterfaces: admission.NewObjectInterfacesFromScheme(runtime.NewScheme())}, + celconfig.RuntimeCELCostBudget) + if err != nil { + t.Fatal(err) + } + security := retirementContainerSecurity(out.(*unstructured.Unstructured)) + if security["runAsUser"] != int64(0) || security["runAsGroup"] != int64(0) || security["privileged"] != true || security["seLinuxOptions"] == nil { + t.Fatalf("unexpected security context: %+v", security) + } + }) + } +} + +func readRetirementManifest(t *testing.T, file string, target any) { + t.Helper() + raw, err := os.ReadFile(retirementPolicyPath + file) + if err != nil { + t.Fatal(err) + } + if err := yaml.UnmarshalStrict(raw, target); err != nil { + t.Fatal(err) + } +} + +func retirementRuntimePod() *unstructured.Unstructured { + const name = "instance-manager-11111111111111111111111111111111" + return &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]any{ + "name": name, "namespace": "longhorn-system", + "labels": map[string]any{ + "longhorn.io/component": "instance-manager", "longhorn.io/data-engine": "v1", + "longhorn.io/instance-manager-type": "aio", "longhorn.io/managed-by": "longhorn-manager", + }, + "ownerReferences": []any{map[string]any{ + "apiVersion": "longhorn.io/v1beta2", "kind": "InstanceManager", "name": name, + "uid": "11111111-1111-4111-8111-111111111111", "controller": true, + }}, + }, + "spec": map[string]any{ + "serviceAccountName": "longhorn-service-account", "restartPolicy": "Never", + "securityContext": map[string]any{"fsGroupChangePolicy": "OnRootMismatch"}, + "containers": []any{map[string]any{"name": "instance-manager", "image": retirementImage, + "securityContext": map[string]any{"privileged": true, "seLinuxOptions": map[string]any{}}}}, + }, + }} +} + +func retirementPodSpec(pod *unstructured.Unstructured) map[string]any { + return pod.Object["spec"].(map[string]any) +} + +func retirementPodContainers(pod *unstructured.Unstructured) []any { + return retirementPodSpec(pod)["containers"].([]any) +} + +func retirementContainerSecurity(pod *unstructured.Unstructured) map[string]any { + return retirementPodContainers(pod)[0].(map[string]any)["securityContext"].(map[string]any) +} diff --git a/scripts/tests/refresh-flux-ghcr-auth/fake_kubectl_test.go b/scripts/tests/refresh-flux-ghcr-auth/fake_kubectl_test.go index f1580660d5..ad4678f31b 100644 --- a/scripts/tests/refresh-flux-ghcr-auth/fake_kubectl_test.go +++ b/scripts/tests/refresh-flux-ghcr-auth/fake_kubectl_test.go @@ -1203,9 +1203,23 @@ func fakeKubectlPatchConsolidatedImageValidatingPolicy(args []string, patchFile spec, _ := patch["spec"].(map[string]any) webhookConfiguration, _ := spec["webhookConfiguration"].(map[string]any) attestors, _ := spec["attestors"].([]any) - if webhookConfiguration["timeoutSeconds"] != float64(30) || len(attestors) != 15 { + if webhookConfiguration["timeoutSeconds"] != float64(30) || len(attestors) != 17 { return commandFailure(91, "consolidated image-validating policy patch omitted its timeout or attestors") } + longhornPublishers := map[string]struct { + repository string + subject string + }{ + "publishlonghornretirement": { + repository: "ghcr.io/devantler-tech/platform-longhorn-instance-manager", + subject: `^https://github\.com/devantler-tech/platform/\.github/workflows/publish-longhorn-v1-retirement\.yaml@refs/heads/main$`, + }, + "publishlonghornv2": { + repository: "ghcr.io/devantler-tech/platform-longhorn-manager", + subject: `^https://github\.com/devantler-tech/platform/\.github/workflows/publish-longhorn-v2-only\.yaml@refs/heads/main$`, + }, + } + longhornAttestors := make(map[string]bool, len(longhornPublishers)) storageAttestorValid := false kubescapeNodeAgentAttestorValid := false corootNodeAgentAttestorValid := false @@ -1221,7 +1235,8 @@ func fakeKubectlPatchConsolidatedImageValidatingPolicy(args []string, patchFile for _, rawAttestor := range attestors { attestor, _ := rawAttestor.(map[string]any) name, _ := attestor["name"].(string) - if name != "publishkubescapestorage" && name != "publishkubescapenodeagent" && name != "publishcorootnodeagent" && name != "publishcorootcheckpoint" && name != "publishkyvernoreports" && name != "publishkyvernomigrate" && name != "publishkyvernobackground" && name != "publishwarzone" && name != "publishksailanalysis" && name != "publishprivatemail" && name != "publishcustomeridentity" && name != "publishdexsecurity" { + longhornPublisher, isLonghornPublisher := longhornPublishers[name] + if !isLonghornPublisher && name != "publishkubescapestorage" && name != "publishkubescapenodeagent" && name != "publishcorootnodeagent" && name != "publishcorootcheckpoint" && name != "publishkyvernoreports" && name != "publishkyvernomigrate" && name != "publishkyvernobackground" && name != "publishwarzone" && name != "publishksailanalysis" && name != "publishprivatemail" && name != "publishcustomeridentity" && name != "publishdexsecurity" { continue } cosign, _ := attestor["cosign"].(map[string]any) @@ -1233,6 +1248,10 @@ func fakeKubectlPatchConsolidatedImageValidatingPolicy(args []string, patchFile identity, _ := identities[0].(map[string]any) issuerValid := identity["issuer"] == "https://token.actions.githubusercontent.com" subject, _ := identity["subjectRegExp"].(string) + if isLonghornPublisher { + longhornAttestors[name] = issuerValid && subject == longhornPublisher.subject + continue + } switch name { case "publishkubescapestorage": storageAttestorValid = issuerValid && subject == "^https://github\\.com/devantler-tech/platform/\\.github/workflows/publish-kubescape-storage-hotfix\\.yaml@refs/heads/main$" @@ -1285,9 +1304,19 @@ func fakeKubectlPatchConsolidatedImageValidatingPolicy(args []string, patchFile dedicatedRoutesCustomerIdentity := false genericExcludesDex := false dedicatedRoutesDex := false + genericExcludesLonghorn := make(map[string]bool, len(longhornPublishers)) + dedicatedRoutesLonghorn := make(map[string]bool, len(longhornPublishers)) for _, rawValidation := range validations { validation, _ := rawValidation.(map[string]any) expression, _ := validation["expression"].(string) + for name, publisher := range longhornPublishers { + if strings.Contains(expression, "attestors.publishapp") { + genericExcludesLonghorn[name] = imageRouteContains(expression, publisher.repository, true) + } + if strings.Contains(expression, "attestors."+name) { + dedicatedRoutesLonghorn[name] = imageRouteContains(expression, publisher.repository, false) + } + } if strings.Contains(expression, "attestors.publishapp") { genericExcludesCustomerIdentity = strings.Contains(expression, "image != 'ghcr.io/devantler-tech/business-site-identity'") && strings.Contains(expression, "!image.startsWith('ghcr.io/devantler-tech/business-site-identity:')") && @@ -1383,6 +1412,11 @@ func fakeKubectlPatchConsolidatedImageValidatingPolicy(args []string, patchFile strings.Contains(expression, "image.startsWith('ghcr.io/devantler-tech/platform-dex@')") } } + for name := range longhornPublishers { + if !longhornAttestors[name] || !genericExcludesLonghorn[name] || !dedicatedRoutesLonghorn[name] { + return commandFailure(91, "consolidated image-validating policy patch omitted the exact %s publisher identity or repository routing", name) + } + } if !storageAttestorValid || !kubescapeNodeAgentAttestorValid || !corootNodeAgentAttestorValid || !checkpointAttestorValid || !genericExcludesCheckpoint || !dedicatedRoutesCheckpoint || !reportsAttestorValid || !genericExcludesReports || !dedicatedRoutesReports || @@ -1413,18 +1447,18 @@ func fakeKubectlPatchConsolidatedImageValidatingPolicy(args []string, patchFile } func checkpointRouteContains(expression string, excluded bool) bool { + return imageRouteContains(expression, "ghcr.io/devantler-tech/platform-coroot", excluded) && + imageRouteContains(expression, "ghcr.io/devantler-tech/platform-coroot-cluster-agent", excluded) +} + +func imageRouteContains(expression, repository string, excluded bool) bool { comparison, negation := "==", "" if excluded { comparison, negation = "!=", "!" } - for _, repository := range []string{"ghcr.io/devantler-tech/platform-coroot", "ghcr.io/devantler-tech/platform-coroot-cluster-agent"} { - if !strings.Contains(expression, "image "+comparison+" '"+repository+"'") || - !strings.Contains(expression, negation+"image.startsWith('"+repository+":')") || - !strings.Contains(expression, negation+"image.startsWith('"+repository+"@')") { - return false - } - } - return true + return strings.Contains(expression, "image "+comparison+" '"+repository+"'") && + strings.Contains(expression, negation+"image.startsWith('"+repository+":')") && + strings.Contains(expression, negation+"image.startsWith('"+repository+"@')") } func fakeKubectlDeleteRetiredImageValidatingPolicy(args []string) int { diff --git a/scripts/tests/test-inventory-first-party-image-signatures.sh b/scripts/tests/test-inventory-first-party-image-signatures.sh index 12074e68e7..5987217b14 100755 --- a/scripts/tests/test-inventory-first-party-image-signatures.sh +++ b/scripts/tests/test-inventory-first-party-image-signatures.sh @@ -301,12 +301,14 @@ expected_globs="$(printf '%s\n' \ 'ghcr.io/devantler-tech/platform-kyverno-background' \ 'ghcr.io/devantler-tech/business-site-identity' \ 'ghcr.io/devantler-tech/platform-dex' \ + 'ghcr.io/devantler-tech/platform-longhorn-instance-manager' \ + 'ghcr.io/devantler-tech/platform-longhorn-manager' \ 'ghcr.io/devantler-tech/*')" if [ "$real_globs" != "$expected_globs" ]; then - echo "FAIL the real rules must retain all sixteen exact ordered image routes" + echo "FAIL the real rules must retain all eighteen exact ordered image routes" failures=$((failures + 1)) else - echo "ok real rules: sixteen ordered rules, dedicated publishers before catch-all" + echo "ok real rules: eighteen ordered rules, dedicated publishers before catch-all" fi # The real file must also satisfy the completeness gate the fixtures exercise. diff --git a/scripts/tests/test-validate-image-verifier-liveness.sh b/scripts/tests/test-validate-image-verifier-liveness.sh index fb10336604..16b2bcf66f 100755 --- a/scripts/tests/test-validate-image-verifier-liveness.sh +++ b/scripts/tests/test-validate-image-verifier-liveness.sh @@ -182,6 +182,8 @@ readonly zone_pattern='ghcr.io/devantler-tech/world-at-ruin/zone' readonly mail_pattern='ghcr.io/devantler-tech/platform-private-mail-bootstrap' readonly customer_identity_pattern='ghcr.io/devantler-tech/business-site-identity' readonly dex_pattern='ghcr.io/devantler-tech/platform-dex' +readonly longhorn_retirement_pattern='ghcr.io/devantler-tech/platform-longhorn-instance-manager' +readonly longhorn_v2_pattern='ghcr.io/devantler-tech/platform-longhorn-manager' readonly app_pattern='ghcr.io/devantler-tech/*' fixture_rules_json="$(yq -o=json '.rules' "${root_dir}/talos/cluster/verify-first-party-images.yaml" | jq ' map({imagePattern: .image, skip: (.skip // false), deny: (.deny // false), @@ -247,7 +249,9 @@ write_healthy_rules() { resource_obj 0012 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${background_pattern}" resource_obj 0013 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${customer_identity_pattern}" resource_obj 0014 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${dex_pattern}" - resource_obj 0015 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${app_pattern}" + resource_obj 0015 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${longhorn_retirement_pattern}" + resource_obj 0016 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${longhorn_v2_pattern}" + resource_obj 0017 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${app_pattern}" } | write_rules "${node}" } @@ -273,7 +277,7 @@ run_script() { healthy_node good output="$(run_script TALOS_NODES=good 2>&1)" || fail "case 1: expected exit 0 for a node that can enforce" require_text "${output}" 'OK good' 'case 1: reports the healthy node' -require_text "${output}" '16 rule(s) in phase running' 'case 1: counts every declared running rule' +require_text "${output}" '18 rule(s) in phase running' 'case 1: counts every declared running rule' require_text "${output}" 'with exact declared decisions' 'case 1: confirms complete rule decisions were compared' require_text "${output}" 'All 1 node(s) can enforce image verification.' 'case 1: reports the summary' @@ -313,27 +317,12 @@ require_text "${output}" 'declared rule set' 'case 1a: names the incomplete poli # Case 1b — RED: the right NUMBER of rules is running, but the last pattern is # stale. Counts alone cannot prove that the declared ordered policy materialised. # =========================================================================== -write_node driftrules -{ - resource_obj 0000 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${analysis_pattern}" - resource_obj 0001 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${ksail_pattern}" - resource_obj 0002 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${provider_pattern}" - resource_obj 0003 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${storage_pattern}" - resource_obj 0004 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${kubescape_node_agent_pattern}" - resource_obj 0005 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${coroot_node_agent_pattern}" - resource_obj 0006 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${checkpoint_server_pattern}" - resource_obj 0007 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${checkpoint_collector_pattern}" - resource_obj 0008 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${zone_pattern}" - resource_obj 0009 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${mail_pattern}" - resource_obj 0010 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${reports_pattern}" - resource_obj 0011 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${migrate_pattern}" - resource_obj 0012 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${background_pattern}" - resource_obj 0013 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${customer_identity_pattern}" - resource_obj 0014 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' "${dex_pattern}" - resource_obj 0015 running "${rules_owner}" 'ImageVerificationRules.security.talos.dev' 'ghcr.io/devantler-tech/stale-*' -} | write_rules driftrules -resource_obj trusted_root.json running "${roots_owner}" 'TUFTrustedRoots.security.talos.dev' | - write_roots driftrules +healthy_node driftrules +drift_rules="${fixtures}/driftrules/resources/${rules_type}" +jq -s --arg pattern "${app_pattern}" \ + 'map(if .spec.imagePattern == $pattern then .spec.imagePattern = "ghcr.io/devantler-tech/stale-*" else . end)[]' \ + "${drift_rules}" >"${work_dir}/changed-rules.json" +mv "${work_dir}/changed-rules.json" "${drift_rules}" status=0 output="$(run_script TALOS_NODES=driftrules 2>&1)" || status=$? [[ "${status}" -eq 1 ]] || fail 'case 1b: a runtime rule pattern that differs from the declaration MUST fail' diff --git a/tests/longhorn-benchmark-retirement-runtime/kyverno-test.yaml b/tests/longhorn-benchmark-retirement-runtime/kyverno-test.yaml deleted file mode 100644 index b69aa96edc..0000000000 --- a/tests/longhorn-benchmark-retirement-runtime/kyverno-test.yaml +++ /dev/null @@ -1,27 +0,0 @@ ---- -apiVersion: cli.kyverno.io/v1alpha1 -kind: Test -metadata: - name: longhorn-benchmark-retirement-runtime -policies: - - ../../k8s/providers/hetzner/infrastructure/cluster-policies/set-longhorn-benchmark-retirement-context.yaml -resources: - - resources.yaml -results: - - policy: set-longhorn-benchmark-retirement-context - rule: use-host-runtime-identity - resources: - - instance-manager-retirement - kind: Pod - result: pass - patchedResources: patched-resources.yaml - - policy: set-longhorn-benchmark-retirement-context - rule: use-host-runtime-identity - resources: - - instance-manager-other-image - - instance-manager-other-owner - - instance-manager-other-account - - instance-manager-other-namespace - - instance-manager-unprivileged - kind: Pod - result: skip diff --git a/tests/longhorn-benchmark-retirement-runtime/patched-resources.yaml b/tests/longhorn-benchmark-retirement-runtime/patched-resources.yaml deleted file mode 100644 index 4237b0233c..0000000000 --- a/tests/longhorn-benchmark-retirement-runtime/patched-resources.yaml +++ /dev/null @@ -1,24 +0,0 @@ ---- -apiVersion: v1 -kind: Pod -metadata: - name: instance-manager-retirement - namespace: longhorn-system - labels: - longhorn.io/component: instance-manager - ownerReferences: - - apiVersion: longhorn.io/v1beta2 - kind: InstanceManager - name: instance-manager-retirement - uid: 01234567-89ab-cdef-0123-456789abcdef - controller: true -spec: - serviceAccountName: longhorn-service-account - containers: - - name: instance-manager - image: ghcr.io/devantler-tech/platform-longhorn-instance-manager:v1.13.0-retirement.1-725bb1d09517c84f8b73ff2c0cf175421df351f1-1@sha256:93cfc83d8bab91f9b6fbffe20bf9bdaca28e877d91d7b740cae003bf6853059f - securityContext: - privileged: true - runAsUser: 0 - runAsGroup: 0 - runAsNonRoot: false diff --git a/tests/longhorn-benchmark-retirement-runtime/resources.yaml b/tests/longhorn-benchmark-retirement-runtime/resources.yaml deleted file mode 100644 index 37ebd829b5..0000000000 --- a/tests/longhorn-benchmark-retirement-runtime/resources.yaml +++ /dev/null @@ -1,126 +0,0 @@ ---- -apiVersion: v1 -kind: Pod -metadata: - name: instance-manager-retirement - namespace: longhorn-system - labels: - longhorn.io/component: instance-manager - ownerReferences: - - apiVersion: longhorn.io/v1beta2 - kind: InstanceManager - name: instance-manager-retirement - uid: 01234567-89ab-cdef-0123-456789abcdef - controller: true -spec: - serviceAccountName: longhorn-service-account - containers: - - name: instance-manager - image: ghcr.io/devantler-tech/platform-longhorn-instance-manager:v1.13.0-retirement.1-725bb1d09517c84f8b73ff2c0cf175421df351f1-1@sha256:93cfc83d8bab91f9b6fbffe20bf9bdaca28e877d91d7b740cae003bf6853059f - securityContext: - privileged: true ---- -apiVersion: v1 -kind: Pod -metadata: - name: instance-manager-other-image - namespace: longhorn-system - labels: - longhorn.io/component: instance-manager - ownerReferences: - - apiVersion: longhorn.io/v1beta2 - kind: InstanceManager - name: instance-manager-other-image - uid: 01234567-89ab-cdef-0123-456789abcdef - controller: true -spec: - serviceAccountName: longhorn-service-account - containers: - - name: instance-manager - image: docker.io/longhornio/longhorn-instance-manager:v1.13.0 - securityContext: - privileged: true ---- -apiVersion: v1 -kind: Pod -metadata: - name: instance-manager-other-owner - namespace: longhorn-system - labels: - longhorn.io/component: instance-manager - ownerReferences: - - apiVersion: longhorn.io/v1beta2 - kind: Volume - name: instance-manager-other-owner - uid: 01234567-89ab-cdef-0123-456789abcdef - controller: true -spec: - serviceAccountName: longhorn-service-account - containers: - - name: instance-manager - image: ghcr.io/devantler-tech/platform-longhorn-instance-manager:v1.13.0-retirement.1-725bb1d09517c84f8b73ff2c0cf175421df351f1-1@sha256:93cfc83d8bab91f9b6fbffe20bf9bdaca28e877d91d7b740cae003bf6853059f - securityContext: - privileged: true ---- -apiVersion: v1 -kind: Pod -metadata: - name: instance-manager-other-account - namespace: longhorn-system - labels: - longhorn.io/component: instance-manager - ownerReferences: - - apiVersion: longhorn.io/v1beta2 - kind: InstanceManager - name: instance-manager-other-account - uid: 01234567-89ab-cdef-0123-456789abcdef - controller: true -spec: - serviceAccountName: default - containers: - - name: instance-manager - image: ghcr.io/devantler-tech/platform-longhorn-instance-manager:v1.13.0-retirement.1-725bb1d09517c84f8b73ff2c0cf175421df351f1-1@sha256:93cfc83d8bab91f9b6fbffe20bf9bdaca28e877d91d7b740cae003bf6853059f - securityContext: - privileged: true ---- -apiVersion: v1 -kind: Pod -metadata: - name: instance-manager-other-namespace - namespace: default - labels: - longhorn.io/component: instance-manager - ownerReferences: - - apiVersion: longhorn.io/v1beta2 - kind: InstanceManager - name: instance-manager-other-namespace - uid: 01234567-89ab-cdef-0123-456789abcdef - controller: true -spec: - serviceAccountName: longhorn-service-account - containers: - - name: instance-manager - image: ghcr.io/devantler-tech/platform-longhorn-instance-manager:v1.13.0-retirement.1-725bb1d09517c84f8b73ff2c0cf175421df351f1-1@sha256:93cfc83d8bab91f9b6fbffe20bf9bdaca28e877d91d7b740cae003bf6853059f - securityContext: - privileged: true ---- -apiVersion: v1 -kind: Pod -metadata: - name: instance-manager-unprivileged - namespace: longhorn-system - labels: - longhorn.io/component: instance-manager - ownerReferences: - - apiVersion: longhorn.io/v1beta2 - kind: InstanceManager - name: instance-manager-unprivileged - uid: 01234567-89ab-cdef-0123-456789abcdef - controller: true -spec: - serviceAccountName: longhorn-service-account - containers: - - name: instance-manager - image: ghcr.io/devantler-tech/platform-longhorn-instance-manager:v1.13.0-retirement.1-725bb1d09517c84f8b73ff2c0cf175421df351f1-1@sha256:93cfc83d8bab91f9b6fbffe20bf9bdaca28e877d91d7b740cae003bf6853059f - securityContext: - privileged: false From 6a448f939fb8067088e8e3b7e4cde198bb9068d8 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 10 Nov 2026 22:21:20 +0100 Subject: [PATCH 5/7] test(storage): attest the exact Longhorn runtime mutation --- ...-policy-longhorn-v1-benchmark-runtime.yaml | 12 +++++----- .../guard-consumer-discovery-conservation.sh | 15 +++++++----- ...t-guard-consumer-discovery-conservation.sh | 23 +++++++++++++++++++ 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/k8s/providers/hetzner/infrastructure/cluster-policies/mutating-admission-policy-longhorn-v1-benchmark-runtime.yaml b/k8s/providers/hetzner/infrastructure/cluster-policies/mutating-admission-policy-longhorn-v1-benchmark-runtime.yaml index ad0c9a725d..25fc425b99 100644 --- a/k8s/providers/hetzner/infrastructure/cluster-policies/mutating-admission-policy-longhorn-v1-benchmark-runtime.yaml +++ b/k8s/providers/hetzner/infrastructure/cluster-policies/mutating-admission-policy-longhorn-v1-benchmark-runtime.yaml @@ -16,13 +16,13 @@ spec: scope: Namespaced matchConditions: - name: longhorn-controller - expression: >- + expression: |- request.namespace == 'longhorn-system' && request.userInfo.username == 'system:serviceaccount:longhorn-system:longhorn-service-account' && object.metadata.namespace == request.namespace && object.spec.serviceAccountName == 'longhorn-service-account' - name: native-v1-manager - expression: >- + expression: |- has(object.metadata.labels) && object.metadata.name.matches('^instance-manager-[a-f0-9]{32}$') && 'longhorn.io/component' in object.metadata.labels && @@ -34,7 +34,7 @@ spec: 'longhorn.io/managed-by' in object.metadata.labels && object.metadata.labels['longhorn.io/managed-by'] == 'longhorn-manager' - name: instance-manager-owner - expression: >- + expression: |- has(object.metadata.ownerReferences) && size(object.metadata.ownerReferences) == 1 && object.metadata.ownerReferences.all(owner, @@ -44,7 +44,7 @@ spec: owner.uid.matches('^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$') && has(owner.controller) && owner.controller) - name: approved-privileged-container - expression: >- + expression: |- object.spec.restartPolicy == 'Never' && (!has(object.spec.initContainers) || size(object.spec.initContainers) == 0) && size(object.spec.containers) == 1 && @@ -56,14 +56,14 @@ spec: !has(container.securityContext.runAsUser) && !has(container.securityContext.runAsGroup) && (!has(container.securityContext.runAsNonRoot) || !container.securityContext.runAsNonRoot)) - name: no-pod-identity-override - expression: >- + expression: |- !has(object.spec.securityContext) || (!has(object.spec.securityContext.runAsUser) && !has(object.spec.securityContext.runAsGroup) && (!has(object.spec.securityContext.runAsNonRoot) || !object.spec.securityContext.runAsNonRoot)) mutations: - patchType: JSONPatch jsonPatch: - expression: >- + expression: |- [ JSONPatch{op: 'add', path: '/spec/containers/0/securityContext/runAsUser', value: 0}, JSONPatch{op: 'add', path: '/spec/containers/0/securityContext/runAsGroup', value: 0} diff --git a/scripts/guard-consumer-discovery-conservation.sh b/scripts/guard-consumer-discovery-conservation.sh index 9cc84c1a7f..ff11f9d6f3 100755 --- a/scripts/guard-consumer-discovery-conservation.sh +++ b/scripts/guard-consumer-discovery-conservation.sh @@ -769,16 +769,18 @@ while IFS=$'\t' read -r file label; do refuse "production render $label holds consumer generation or cloning that cannot be accounted for by a static build, so its consumers are UNKNOWN" done <<<"$generated" # Admit only complete reviewed apiVersion/kind/spec surfaces: the two - # HelmRelease defaults (#5357) and the native Gateway peer policy. The latter - # changes only physical-peer fields and its ownership marker on one fixed - # CiliumEnvoyConfig; neither carries a publication consumer or constructor. - # Its complete spec fingerprint includes every scope, variable and patch. + # HelmRelease defaults (#5357), the native Gateway peer policy, and the V1 + # benchmark runtime policy (#5665). The Gateway policy changes peer fields + # on one fixed CiliumEnvoyConfig. The benchmark policy sets container UID + # and GID only on CREATE of an exact Longhorn Pod shape. Neither kind carries + # a publication consumer or constructor. The fingerprint includes every + # scope, variable and patch. # A larger or changed policy cannot inherit approval from its name or paths. # Canonical JSON includes every CEL expression, # selector, operation and evaluation field, without whitespace normalization # inside strings. An altered expression or added field cannot inherit an # approval from the policy's name or its mutation paths. These patches touch - # only fixed defaults or Gateway peer fields, never chart values or consumers. + # only fixed defaults, Gateway peer fields or a Pod security context. if ! yq -o=json -I=0 '.. | select(type == "!!map" and (.kind == "MutatingPolicy" or .kind == "MutatingAdmissionPolicy")) | {"apiVersion": .apiVersion, "kind": .kind, "spec": .spec}' "$file" | jq -cS . >"$work/reviewed-mutation-candidates.jsonl"; then @@ -794,7 +796,8 @@ while IFS=$'\t' read -r file label; do case "${candidate_digest%% *}" in 43b7eaf13a1b754ed0449239ce9e337de6881751fd009c738efb46e367524824 | \ 691bf7d05ed9d87ae261f34d51e7cd062d345a6af8f6bb2dcf1ec1aeb3dade17 | \ - 92dc171d2b1e73eec7679ae3f74e806a3fc8472f7ed3445bfca1541c3d27941d) + 92dc171d2b1e73eec7679ae3f74e806a3fc8472f7ed3445bfca1541c3d27941d | \ + 55d0841bf6699a0cbf7bb40bcf2b154facc2305cae538b9f75b71774651895d3) reviewed_cel_surfaces="$(jq -ce --argjson candidate "$candidate" '. + [$candidate]' <<<"$reviewed_cel_surfaces")" || refuse 'could not record the complete reviewed mutation policy' ;; diff --git a/scripts/tests/test-guard-consumer-discovery-conservation.sh b/scripts/tests/test-guard-consumer-discovery-conservation.sh index 8eacc6e2bd..1e73b985e2 100755 --- a/scripts/tests/test-guard-consumer-discovery-conservation.sh +++ b/scripts/tests/test-guard-consumer-discovery-conservation.sh @@ -1168,6 +1168,29 @@ YAML expect_refusal "an altered native Gateway peer $variant cannot inherit approval" "$root" 'CEL admission mutation' 'consumers are UNKNOWN' done + # Only the complete benchmark runtime spec can pass. Its real binding adds + # a second namespace restriction, while the policy also fences the request. + root="$(fixture reviewed-native-longhorn-benchmark)" + benchmark_policies="$REPO_ROOT/k8s/providers/hetzner/infrastructure/cluster-policies" + cp "$benchmark_policies/mutating-admission-policy-longhorn-v1-benchmark-runtime.yaml" "$root/k8s/providers/prod/apps/benchmark-runtime.yaml" + cp "$benchmark_policies/mutating-admission-policy-binding-longhorn-v1-benchmark-runtime.yaml" "$root/k8s/providers/prod/apps/benchmark-binding.yaml" + printf ' - benchmark-runtime.yaml\n - benchmark-binding.yaml\n' >>"$root/k8s/providers/prod/apps/kustomization.yaml" + expect_pass 'the native benchmark runtime changes only the exact Pod security context' "$root" '2 consumer(s)' + for variant in resource-scope patch-path; do + root="$(fixture "native-longhorn-benchmark-$variant")" + cp "$WORK/reviewed-native-longhorn-benchmark/k8s/providers/prod/apps/benchmark-runtime.yaml" "$root/k8s/providers/prod/apps/benchmark-runtime.yaml" + policy="$root/k8s/providers/prod/apps/benchmark-runtime.yaml" + case "$variant" in + resource-scope) yq -i '.spec.matchConstraints.resourceRules[0].resources += ["*"]' "$policy" ;; + patch-path) yq -i '.spec.mutations[0].jsonPatch.expression |= sub("securityContext/runAsUser", "image")' "$policy" ;; + esac + if cmp -s "$WORK/reviewed-native-longhorn-benchmark/k8s/providers/prod/apps/benchmark-runtime.yaml" "$policy"; then + fail "native benchmark runtime negative control did not change $variant" + fi + printf ' - benchmark-runtime.yaml\n' >>"$root/k8s/providers/prod/apps/kustomization.yaml" + expect_refusal "an altered native benchmark runtime $variant cannot inherit approval" "$root" 'CEL admission mutation' 'consumers are UNKNOWN' + done + # #5096: the one reviewed CEL mutation shape passes; each variant widens # exactly one thing and must be refused again. root="$(fixture reviewed-cel-mutation)" From 0ec297d1821e549e33cf2ddfa0855653ba5cf35d Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 10 Nov 2026 22:52:30 +0100 Subject: [PATCH 6/7] test(storage): record the two exact runtime authorization entries --- scripts/validate-eks-ci-role-policy/approved-surface.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/validate-eks-ci-role-policy/approved-surface.txt b/scripts/validate-eks-ci-role-policy/approved-surface.txt index d74d258503..771a71ff34 100644 --- a/scripts/validate-eks-ci-role-policy/approved-surface.txt +++ b/scripts/validate-eks-ci-role-policy/approved-surface.txt @@ -14,8 +14,12 @@ admissionregistration.k8s.io/v1|MutatingAdmissionPolicyBinding||gateway-receiver 835871036a393f9eed5a90088ec575937b8c8a45393967166b1fa3c15aac2289 +admissionregistration.k8s.io/v1|MutatingAdmissionPolicyBinding||longhorn-v1-benchmark-runtime e566faddafced4d88af3c547020852d26f75b0eb8d63c5b2887bd0cd6f349f32 + admissionregistration.k8s.io/v1|MutatingAdmissionPolicy||gateway-receiver 29a4c24f83df36bb1a1cc5880703564b5b853f46e27828aca4493fdc5017d983 +admissionregistration.k8s.io/v1|MutatingAdmissionPolicy||longhorn-v1-benchmark-runtime e2296d89bf4177747418a2ad65850712990fc84a5e9bb4333490038bf73f817d + admissionregistration.k8s.io/v1|ValidatingAdmissionPolicyBinding||gateway-receiver 02e679b3bede26c8ef6793955787d34b266667cafe41636288c87e1fcb83d339 admissionregistration.k8s.io/v1|ValidatingAdmissionPolicyBinding||longhorn-benchmark-delete 33d9144f35086ed96a084f303f90c44c22cc5e19f47a16752a1b871c6de8b04e From 3d697d43262748d5f2e41fe3d68698e833f80de7 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Wed, 11 Nov 2026 00:01:42 +0100 Subject: [PATCH 7/7] test(storage): assert native admission and its exact refusal --- .../retirement-fence-proof/runtime_admission_test.go | 6 +++++- scripts/tests/test-guard-consumer-discovery-conservation.sh | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/runtime_admission_test.go b/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/runtime_admission_test.go index 4c5847a232..2b90f61970 100644 --- a/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/runtime_admission_test.go +++ b/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/runtime_admission_test.go @@ -129,7 +129,11 @@ func TestNativeRetirementRuntimeAdmission(t *testing.T) { if err != nil { t.Fatal(err) } - security := retirementContainerSecurity(out.(*unstructured.Unstructured)) + patched, ok := out.(*unstructured.Unstructured) + if !ok { + t.Fatalf("patched object has type %T, want *unstructured.Unstructured", out) + } + security := retirementContainerSecurity(patched) if security["runAsUser"] != int64(0) || security["runAsGroup"] != int64(0) || security["privileged"] != true || security["seLinuxOptions"] == nil { t.Fatalf("unexpected security context: %+v", security) } diff --git a/scripts/tests/test-guard-consumer-discovery-conservation.sh b/scripts/tests/test-guard-consumer-discovery-conservation.sh index 1e73b985e2..5f893a5d75 100755 --- a/scripts/tests/test-guard-consumer-discovery-conservation.sh +++ b/scripts/tests/test-guard-consumer-discovery-conservation.sh @@ -1188,7 +1188,7 @@ YAML fail "native benchmark runtime negative control did not change $variant" fi printf ' - benchmark-runtime.yaml\n' >>"$root/k8s/providers/prod/apps/kustomization.yaml" - expect_refusal "an altered native benchmark runtime $variant cannot inherit approval" "$root" 'CEL admission mutation' 'consumers are UNKNOWN' + expect_refusal "an altered native benchmark runtime $variant cannot inherit approval" "$root" 'unevaluated CEL admission mutation' 'consumers are UNKNOWN' done # #5096: the one reviewed CEL mutation shape passes; each variant widens