diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c2583626c4..3103719899 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' @@ -2550,11 +2550,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/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..25fc425b99 --- /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/kustomization.yaml b/k8s/providers/hetzner/infrastructure/kustomization.yaml index ae3920fbb8..7d2082e8c5 100644 --- a/k8s/providers/hetzner/infrastructure/kustomization.yaml +++ b/k8s/providers/hetzner/infrastructure/kustomization.yaml @@ -69,6 +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 + # 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/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/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..2b90f61970 --- /dev/null +++ b/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/runtime_admission_test.go @@ -0,0 +1,190 @@ +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) + } + 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) + } + }) + } +} + +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/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/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-coroot-image-verifier-prerequisite.sh b/scripts/tests/test-compatibility-image-verifier-prerequisite.sh 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 100755 --- 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/scripts/tests/test-guard-consumer-discovery-conservation.sh b/scripts/tests/test-guard-consumer-discovery-conservation.sh index 8eacc6e2bd..5f893a5d75 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" 'unevaluated 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)" 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/scripts/validate-eks-ci-role-policy/approved-surface.txt b/scripts/validate-eks-ci-role-policy/approved-surface.txt index 957ef00f8a..7e7b693edb 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 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