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 d206a89b5..6c51d314c 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-issuer-refs.yaml - best-practices/restrict-tenant-secret-stores.yaml - best-practices/validate-host-restrictions.yaml 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. 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