From 12f954f29e3721f84c948251046dc803427c6eac Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 1 Aug 2026 11:59:01 +0200 Subject: [PATCH 1/2] fix(security): restrict doggy countdown route hostname --- .../restrict-doggy-countdown-hostnames.yaml | 43 ++++++++++++++++++ .../cluster-policies/kustomization.yaml | 1 + .../kyverno-test.yaml | 31 +++++++++++++ .../resources.yaml | 44 +++++++++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 k8s/bases/infrastructure/cluster-policies/best-practices/restrict-doggy-countdown-hostnames.yaml create mode 100644 tests/restrict-doggy-countdown-hostnames/kyverno-test.yaml create mode 100644 tests/restrict-doggy-countdown-hostnames/resources.yaml diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-doggy-countdown-hostnames.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-doggy-countdown-hostnames.yaml new file mode 100644 index 000000000..4db51bc83 --- /dev/null +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-doggy-countdown-hostnames.yaml @@ -0,0 +1,43 @@ +# The doggy-countdown deployment is sourced from a separately published OCI +# artifact and is applied as a ServiceAccount with permission to manage +# HTTPRoutes in this namespace. Keep that publishing path from claiming another +# hostname on the shared, cross-namespace Gateway (and from making external-dns +# publish that claim). Requiring exactly one hostname also rejects a hostname- +# less route, which Gateway API would otherwise match against every hostname. +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: restrict-doggy-countdown-hostnames + annotations: + policies.kyverno.io/title: Restrict Doggy Countdown Hostnames + policies.kyverno.io/category: Security, Gateway API + policies.kyverno.io/severity: high + policies.kyverno.io/subject: HTTPRoute + policies.kyverno.io/minversion: 1.6.0 + policies.kyverno.io/description: >- + Restricts HTTPRoutes in the doggy-countdown namespace to the tenant's + assigned simba hostname so its remote manifest source cannot claim other + names on the shared platform Gateway. +spec: + validationFailureAction: Enforce + background: true + rules: + - name: require-doggy-countdown-hostname + match: + any: + - resources: + kinds: + - HTTPRoute + namespaces: + - doggy-countdown + validate: + message: >- + Doggy countdown HTTPRoutes must declare exactly one hostname: + simba.${domain}. + deny: + conditions: + any: + - key: "{{ request.object.spec.hostnames || `[]` }}" + operator: NotEquals + value: + - "simba.${domain}" diff --git a/k8s/bases/infrastructure/cluster-policies/kustomization.yaml b/k8s/bases/infrastructure/cluster-policies/kustomization.yaml index 381a7b082..ed7580fe6 100644 --- a/k8s/bases/infrastructure/cluster-policies/kustomization.yaml +++ b/k8s/bases/infrastructure/cluster-policies/kustomization.yaml @@ -12,6 +12,7 @@ resources: - best-practices/disable-default-sa-automount.yaml - best-practices/disallow-latest-tag.yaml - best-practices/propagate-reloader-to-flagger-primary.yaml + - best-practices/restrict-doggy-countdown-hostnames.yaml - best-practices/restrict-tenant-secret-stores.yaml - best-practices/validate-host-restrictions.yaml - best-practices/validate-pdb-drain-safe.yaml diff --git a/tests/restrict-doggy-countdown-hostnames/kyverno-test.yaml b/tests/restrict-doggy-countdown-hostnames/kyverno-test.yaml new file mode 100644 index 000000000..c395bad7b --- /dev/null +++ b/tests/restrict-doggy-countdown-hostnames/kyverno-test.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: restrict-doggy-countdown-hostnames +policies: + - >- + ../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-doggy-countdown-hostnames.yaml +resources: + - resources.yaml +results: + - policy: restrict-doggy-countdown-hostnames + rule: require-doggy-countdown-hostname + resources: + - doggy-countdown/intended-hostname + kind: HTTPRoute + result: pass + - policy: restrict-doggy-countdown-hostnames + rule: require-doggy-countdown-hostname + resources: + - doggy-countdown/arbitrary-hostname + - doggy-countdown/hostname-less + - doggy-countdown/mixed-hostnames + kind: HTTPRoute + result: fail + - policy: restrict-doggy-countdown-hostnames + rule: require-doggy-countdown-hostname + resources: + - another-tenant/arbitrary-hostname + kind: HTTPRoute + result: skip diff --git a/tests/restrict-doggy-countdown-hostnames/resources.yaml b/tests/restrict-doggy-countdown-hostnames/resources.yaml new file mode 100644 index 000000000..44f35d107 --- /dev/null +++ b/tests/restrict-doggy-countdown-hostnames/resources.yaml @@ -0,0 +1,44 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: intended-hostname + namespace: doggy-countdown +spec: + hostnames: + - "simba.${domain}" +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: arbitrary-hostname + namespace: doggy-countdown +spec: + hostnames: + - login.platform.devantler.tech +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: hostname-less + namespace: doggy-countdown +spec: {} +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: mixed-hostnames + namespace: doggy-countdown +spec: + hostnames: + - "simba.${domain}" + - login.platform.devantler.tech +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: arbitrary-hostname + namespace: another-tenant +spec: + hostnames: + - login.platform.devantler.tech From 2991e54c6043db0a4585c4b3de59ed1daa076e0e Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 15 Aug 2026 08:22:47 +0200 Subject: [PATCH 2/2] fix(security): approve rendered surface for the new hostname policy The added Kyverno ClusterPolicy enters the fingerprinted authorization surface as an indirect authorization policy, so the aggregate value moved. Measured against main 44f14fab: 524 -> 525 documents, membership differing by exactly the one added ClusterPolicy, zero removed or renamed, and the remaining 524 byte-identical (negative control fired). The policy is a validate.deny rule that can only refuse admission; it creates no identity, binding, role, or policy document. --- scripts/validate-eks-ci-role-policy/main.go | 37 ++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/scripts/validate-eks-ci-role-policy/main.go b/scripts/validate-eks-ci-role-policy/main.go index 237d4e34c..8f609e555 100644 --- a/scripts/validate-eks-ci-role-policy/main.go +++ b/scripts/validate-eks-ci-role-policy/main.go @@ -363,7 +363,42 @@ const ( // per-resource fingerprint, since the moved documents belong to main's change // rather than the branch's. Compare like with like before concluding the // toolchain is at fault. -const expectedRenderedSurfaceSHA = "26e28178117fa9dc0f7d66c8bd526b1b5f000beeedac9f327207b8a674c56755" +// +// Measured against main 44f14fab before approving this value: 524 rendered +// documents on main and 525 on this branch across all five production roots, +// with membership differing by exactly ONE entry and zero removed or renamed +// (set difference in both directions over the complete +// apiVersion|kind|namespace|name identity). The single added entry is +// +// kyverno.io/v1 ClusterPolicy restrict-doggy-countdown-hostnames +// +// and after removing just that entry the remaining 524 documents are +// byte-identical to main's render — checked with a negative control that fired +// on a one-line perturbation, so the identical result is not vacuous. Nothing +// added, removed, or renamed touches a Role, ClusterRole, RoleBinding, +// ClusterRoleBinding, ServiceAccount, or IAM document. +// +// The added policy is selected into the surface only because Kyverno +// ClusterPolicies are indirect authorization policies. It is a single +// `validate.deny` rule matching HTTPRoute in the doggy-countdown namespace and +// requiring exactly the tenant's assigned hostname, with +// validationFailureAction Enforce. It creates no identity, binding, role, or +// policy document and grants nothing to the aws/aws service account; it can +// only refuse admission, so it moves the surface strictly in the restrictive +// direction. +// +// Two independent renderers agree on this value: the required CI job on the +// approved toolchain, and a local render on kubectl v1.36.1 with the same +// embedded kustomize v5.8.1. Agreement across a kubectl patch difference is +// what rules out a renderer-version artifact in the value. +// +// That local corroboration is reachable only through the test path, and the +// reason is worth recording: TestValidateAuthorizationAcceptsCommittedPolicy +// calls validateAuthorization directly, so it never passes through +// validateRendererVersion — only run() does. A local `go run` on v1.36.1 is +// still rejected by design. Do not read a passing `go test` as evidence that +// the local toolchain matches the pin. +const expectedRenderedSurfaceSHA = "e02856de56721b06897d6c151974796b08ecd73c23b461edde4c4f1efb44636b" // authorizationOverlayPaths lists every independently reconciled production // layer where an object can grant privileges to the aws/aws service account.