Skip to content

fix(security): close alternate tenant route-kind hostname bypass - #2713

Merged
devantler merged 15 commits into
mainfrom
codex/propose-fix-for-tenant-hostname-vulnerability
Aug 17, 2026
Merged

fix(security): close alternate tenant route-kind hostname bypass#2713
devantler merged 15 commits into
mainfrom
codex/propose-fix-for-tenant-hostname-vulnerability

Conversation

@devantler

@devantler devantler commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

🤖 Generated by the Agentic Engineer

Motivation

Platform #2740 now confines tenant-authored HTTPRoute hostnames to per-tenant allow-lists and denies unlisted tenants. One bypass remained: the tenant role still granted four other Gateway API route kinds, and an HTTPS listener accepts GRPCRoute by default. Those resources never pass through the HTTPRoute-only hostname policy, so a compromised tenant artifact could still claim another service's hostname on the shared wildcard Gateway.

Description

  • Narrow gateway-tenant-edit to httproutes and referencegrants; ordinary built-in edit remains excluded, as established by fix(security): restrict tenant gateway hostnames #2740.
  • Pin every rendered platform Gateway listener to allowedRoutes.kinds: [HTTPRoute], covering routes created by principals outside the tenant role as well.
  • Add a rendered regression test that fails if either the tenant RBAC grant set widens or any current/future listener is left unpinned.
  • Update the tenant onboarding documentation. The canonical restrict-tenant-route-hostnames policy and its fixtures remain the ones shipped by fix(security): restrict tenant gateway hostnames #2740; this PR deliberately does not duplicate them.

If another route kind becomes necessary, the safe change is to extend the hostname policy to cover it and relax both the role and listener boundary together.

Validation

  • shellcheck scripts/tests/test-tenant-route-hostname-boundary.sh
  • bash scripts/tests/test-tenant-route-hostname-boundary.sh — 4 rendered listeners pinned; tenant RBAC contains only HTTPRoute plus ReferenceGrant
  • kyverno test ./tests/restrict-tenant-route-hostnames — 14 passed, 0 failed
  • go test ./scripts/validate-eks-ci-role-policy
  • checksum-verified kubectl v1.36.2 / Kustomize v5.8.1 authorization render — exact fingerprint 79250b2f64ab9d3798a659d13714b1a07dd72f5baa5fb465beecf30bfd9ad172
  • ksail workload validate
  • ksail --config ksail.prod.yaml workload validate

User-visible evaluation

No tenant traffic shape changes: approved HTTPRoutes continue to attach to the same four rendered listeners, and the canonical policy's paved-road fixtures remain green. The removed route kinds have no Platform workload consumer; they were unchecked privilege, not a supported ingress path.

This direct security fix has no tracking issue. The earlier P1 review findings on this PR are resolved; the branch was re-integrated with exact current main at 6d926e42 after #2740 landed.

@devantler

Copy link
Copy Markdown
Contributor Author

Requested by the 🤖 Daily AI Engineer — CI is green at the current head and this draft carries no qualifying review, so requesting the lane-priority reviewer. Hygiene only: this is a sibling lane's draft and its owner promotes it.

@devantler

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor

cursor Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_fccaaa49-9298-4381-a978-af759f8279f6)

@devantler

Copy link
Copy Markdown
Contributor Author

Requested by the 🤖 Daily AI Engineer — the previous Cursor Bugbot run returned conclusion: neutral with Bugbot failed to run, i.e. the review never happened. Re-requesting at a slower pace.

@devantler

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor

cursor Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_5ec5e0d6-a03b-43b7-bb35-8e0a07881d93)

@devantler devantler left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Generated by the Agentic Engineer

Static review of exact head 9f448d92075e02d56bd802246cf1f08a8f96242f; one blocking finding is attached. This ownership-unverified branch was not checked out or executed.

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

⚠️MegaLinter analysis: Success with warnings

⚠️ BASH / bash-exec - 4 errors
Results of bash-exec linter (version 5.3.9)
See documentation on https://megalinter.io/10.0.0/descriptors/bash_bash_exec/
-----------------------------------------------

✅ [SUCCESS] .github/scripts/setup-ksail.sh
✅ [SUCCESS] scripts/check-megalinter-version-drift.sh
✅ [SUCCESS] scripts/dr-rebuild-supersession-guard.sh
❌ [ERROR] scripts/ghcr-auth-lib.sh
    Error: File:[scripts/ghcr-auth-lib.sh] is not executable

✅ [SUCCESS] scripts/guard-cilium-homogeneous-device-rollout.sh
✅ [SUCCESS] scripts/guard-kubescape-gate-frameworks.sh
✅ [SUCCESS] scripts/guard-shared-publish-workflow-pin.sh
✅ [SUCCESS] scripts/megalinter-scan-counts.sh
✅ [SUCCESS] scripts/normalize-sarif-paths.sh
❌ [ERROR] scripts/refresh-flux-ghcr-auth-safety.sh
    Error: File:[scripts/refresh-flux-ghcr-auth-safety.sh] is not executable

✅ [SUCCESS] scripts/refresh-flux-ghcr-auth.sh
✅ [SUCCESS] scripts/report-cilium-rollout-gate-suppression.sh
✅ [SUCCESS] scripts/run-ksail-prod-with-pull-auth.sh
✅ [SUCCESS] scripts/summarize-sarif-findings.sh
✅ [SUCCESS] scripts/tests/test-actual-budget-auth-route.sh
✅ [SUCCESS] scripts/tests/test-check-megalinter-version-drift.sh
✅ [SUCCESS] scripts/tests/test-cilium-bandwidth-manager-component.sh
✅ [SUCCESS] scripts/tests/test-cilium-homogeneous-devices-activation.sh
✅ [SUCCESS] scripts/tests/test-cilium-homogeneous-devices-autoscaler-gate.sh
✅ [SUCCESS] scripts/tests/test-cilium-homogeneous-devices-flux-wait.sh
✅ [SUCCESS] scripts/tests/test-cilium-mutual-auth-policy-regressions.sh
✅ [SUCCESS] scripts/tests/test-cilium-mutual-auth-policy.sh
✅ [SUCCESS] scripts/tests/test-cilium-rollout-gate-suppression-signal.sh
✅ [SUCCESS] scripts/tests/test-cnpg-degraded-alert.sh
✅ [SUCCESS] scripts/tests/test-coroot-postgres-scrape-policy.sh
✅ [SUCCESS] scripts/tests/test-crossplane-egress-policy.sh
✅ [SUCCESS] scripts/tests/test-crossplane-sync-exporter.sh
✅ [SUCCESS] scripts/tests/test-dr-rebuild-supersession-guard.sh
✅ [SUCCESS] scripts/tests/test-github-config-role-activation-parity.sh
❌ [ERROR] scripts/tests/test-headlamp-plugin-removal.sh
    Error: File:[scripts/tests/test-headlamp-plugin-removal.sh] is not executable

✅ [SUCCESS] scripts/tests/test-kubescape-gate-frameworks-guard.sh
✅ [SUCCESS] scripts/tests/test-kyverno-admission-vpa.sh
❌ [ERROR] scripts/tests/test-kyverno-umami-mutation-rbac.sh
    Error: File:[scripts/tests/test-kyverno-umami-mutation-rbac.sh] is not executable

✅ [SUCCESS] scripts/tests/test-megalinter-scan-counts-ignorefile.sh
✅ [SUCCESS] scripts/tests/test-normalize-sarif-paths.sh
✅ [SUCCESS] scripts/tests/test-openbao-oidc-role.sh
✅ [SUCCESS] scripts/tests/test-opencost-usage-scraper.sh
✅ [SUCCESS] scripts/tests/test-pvc-prune-safety.sh
✅ [SUCCESS] scripts/tests/test-refresh-flux-ghcr-auth-safety.sh
✅ [SUCCESS] scripts/tests/test-restrict-homepage-service-groups.sh
✅ [SUCCESS] scripts/tests/test-restrict-tenant-secret-stores.sh
✅ [SUCCESS] scripts/tests/test-setup-ksail.sh
✅ [SUCCESS] scripts/tests/test-shared-publish-workflow-pin-guard.sh
✅ [SUCCESS] scripts/tests/test-summarize-sarif-findings.sh
✅ [SUCCESS] scripts/tests/test-tenant-route-hostname-boundary.sh
✅ [SUCCESS] scripts/tests/test-umami-provisioning-bootstrap.sh
✅ [SUCCESS] scripts/tests/test-use-prod-stable-api-endpoint.sh
✅ [SUCCESS] scripts/tests/test-validate-image-verifier-liveness.sh
✅ [SUCCESS] scripts/tests/test-verify-published-evidence.sh
✅ [SUCCESS] scripts/update-vendored-operators.sh
✅ [SUCCESS] scripts/use-prod-stable-api-endpoint.sh
✅ [SUCCESS] scripts/validate-alert-coverage.sh
✅ [SUCCESS] scripts/validate-image-verifier-liveness.sh
✅ [SUCCESS] scripts/verify-published-evidence.sh
✅ [SUCCESS] scripts/wait-for-platform-flux-revision.sh
⚠️ REPOSITORY / checkov - 3 errors
2026-08-17 12:17:16,386 [MainThread  ] [ERROR]  YAML error parsing k8s/bases/infrastructure/controllers/kubevirt/kubevirt-operator.yaml: expected a single document in the stream
  in "<unicode string>", line 2, column 1
but found another document
  in "<unicode string>", line 9, column 1
cloudformation scan results:

Passed checks: 0, Failed checks: 0, Skipped checks: 0, Parsing errors: 1

kubernetes scan results:

Passed checks: 2167, Failed checks: 3, Skipped checks: 35

Check: CKV_K8S_40: "Containers should run as a high UID to avoid host conflict"
	FAILED for resource: CronJob.openbao.vault-snapshot
	File: /k8s/bases/infrastructure/vault-backup/cron-job.yaml:23-197
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/bc-k8s-37

		Code lines for this resource are too many. Please use IDE of your choice to review the file.
Check: CKV_K8S_40: "Containers should run as a high UID to avoid host conflict"
	FAILED for resource: Job.openbao.vault-snapshot-init
	File: /k8s/bases/infrastructure/vault-backup/job.yaml:23-191
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/bc-k8s-37

		Code lines for this resource are too many. Please use IDE of your choice to review the file.
Check: CKV_K8S_38: "Ensure that Service Account Tokens are only mounted where necessary"
	FAILED for resource: CronJob.umami.umami-provision-tenants
	File: /k8s/bases/apps/umami/cron-job.yaml:45-755
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/bc-k8s-35

		Code lines for this resource are too many. Please use IDE of your choice to review the file.
github_actions scan results:

Passed checks: 156, Failed checks: 0, Skipped checks: 0
⚠️ SPELL / cspell - 3550 errors
edocs",
        "homelab",
        "hostnames",
        "httproute",
        "httproutes",
        "idempotently",
        "ignorefile",
        "imagevalidatingpolicy",
        "imranismail",
        "injective",
        "inspectable",
        "iscsi",
        "italicise",
        "ivpol",
        "jobif",
        "journalled",
        "keylessly",
        "keypair",
        "kprobes",
        "kptr",
        "krew",
        "ksail",
        "ksail's",
        "ksailcd",
        "kubeconfig",
        "kubeconfig's",
        "kubeconform",
        "kubeconform's",
        "kubelet",
        "kubelet's",
        "kubelets",
        "kubescape",
        "kubescape's",
        "kubespan",
        "kubevirt",
        "kubevuln",
        "kustomization",
        "kustomizations",
        "letsencrypt",
        "libc",
        "libgnutls",
        "lintable",
        "livez",
        "loadtester",
        "locationless",
        "logfile",
        "lookarounds",
        "lrwxrwxrwx",
        "lserror",
        "lsfail",
        "luks",
        "lycheeignore",
        "machineconfig",
        "materialise",
        "materialised",
        "materialises",
        "maxage",
        "maxbackup",
        "maxsize",
        "maxx",
        "microtime",
        "mikefarah",
        "misconfig",
        "misconfigs",
        "misordered",
        "mistargeted",
        "mitrelike",
        "mktemp",
        "mlock",
        "mutatingpolicies",
        "mutatingwebhookconfigurations",
        "myapp",
        "najsk",
        "neighbour",
        "nenv",
        "netlink",
        "netpol",
        "netpols",
        "neutralises",
        "neutralising",
        "nextjs",
        "nftables",
        "nilnil",
        "nobuckets",
        "nodepod",
        "nodeport",
        "nolabel",
        "nolint",
        "nonexec",
        "nonroot",
        "normalisation",
        "normalise",
        "normalised",
        "normalises",
        "normalising",
        "nosec",
        "nsalike",
        "nullglob",
        "ocirepository",
        "onlycri",
        "onlysystem",
        "openbao",
        "opencost",
        "openfeature",
        "oras",
        "otherplugin",
        "overclaimed",
        "overprovisioning",
        "parallelised",
        "partialgroup",
        "pasteable",
        "permissioning",
        "persistentvolumeclaims",
        "phaseless",
        "pipefail",
        "policyreports",
        "portforward",
        "preemptible",
        "preservingly",
        "prioritisable",
        "prioritisation",
        "prioritised",
        "privesc",
        "probeerror",
        "providerconfigs",
        "pseudonymization",
        "pseudonymized",
        "pseudonymizes",
        "publishapp",
        "publishprovider",
        "pushsecret",
        "pushsecrets",
        "qrbvrml",
        "randomises",
        "rdqwpktr",
        "readyz",
        "reassertions",
        "recognisable",
        "recognisably",
        "recognise",
        "recognised",
        "recolour",
        "reconverges",
        "referencegrants",
        "refreshfluxghcrauth",
        "regenerable",
        "releaserc",
        "rematerialise",
        "rematerialised",
        "replicaset",
        "repoint",
        "repointed",
        "repoints",
        "repositoryrulesets",
        "resizer",
        "restrictor",
        "retabbed",
        "retarget",
        "rmem",
        "rolebindings",
        "rollouts",
        "rshared",
        "rwxr",
        "sanitised",
        "sanitiser",
        "sanitising",
        "sarif",
        "scheckov",
        "schedulability",
        "schedulable",
        "scopeable",
        "scopeless",
        "seccomp",
        "secretbox",
        "secretstore",
        "seedable",
        "serialise",
        "serialised",
        "serverside",
        "serviceaccount",
        "serviceaccounts",
        "sgdisk",
        "shellcheck",
        "shfmt",
        "shopt",
        "shortsha",
        "siderolabs",
        "siderolink",
        "sigstore",
        "skmde",
        "slurpfile",
        "sngle",
        "specnull",
        "spiffe",
        "sprintf",
        "srole",
        "stakater",
        "startswith",
        "statefulset",
        "statemanager",
        "stdlib",
        "stepif",
        "storageclass",
        "strenv",
        "subshell",
        "subtest",
        "summarised",
        "surfaceless",
        "syft",
        "syscall",
        "sysctls",
        "syste",
        "systembare",
        "tagliteral",
        "tagonly",
        "talosconfig",
        "talosctl",
        "tanzu",
        "tcproutes",
        "templatesyncignore",
        "thresholded",
        "tlsroutes",
        "tlsv",
        "tmpl",
        "toplevel",
        "tostring",
        "tracepoints",
        "travelled",
        "trixie",
        "trueish",
        "trustd",
        "ture",
        "udproutes",
        "uids",
        "umami",
        "umami's",
        "unablated",
        "unclickable",
        "uncompilable",
        "unconfigured",
        "uncordon",
        "uncordoned",
        "uncordoning",
        "uncordons",
        "undecoded",
        "undercounts",
        "undispositioned",
        "unenforcing",
        "unevidenced",
        "unexcepted",
        "unfiled",
        "ungated",
        "ungenerated",
        "unifi",
        "uninspected",
        "uninvoked",
        "unioned",
        "unmarshalling",
        "unmarshals",
        "unmodelled",
        "unparseable",
        "unprovisioned",
        "unrecognisable",
        "unrecognised",
        "unrepresentable",
        "unreviewed",
        "unroutable",
        "unrun",
        "unshippable",
        "unskipped",
        "untrackable",
        "unvalidated",
        "unwaited",
        "unwired",
        "upbound",
        "updatekeys",
        "upjet",
        "upstreaming",
        "urlencode",
        "userinfo",
        "userns",
        "ushfn",
        "validatable",
        "validatealertcoverage",
        "validatingwebhookconfigurations",
        "vcunav",
        "velero",
        "virt",
        "volumesnapshot",
        "vpas",
        "vulnerabilitymanifests",
        "vulnerabilitymanifestsummary",
        "vulns",
        "vxlan",
        "wffc",
        "wgpolicyk",
        "wildcarded",
        "wlid",
        "wmem",
        "workloadconfigurationscans",
        "workloadconfigurationscansummary",
        "worktrees",
        "xpkg",
        "yannh",
        "yubikey",
        "yzwvjjmcyfnl",
        "zizmor"
    ]
}


You can also copy-paste megalinter-reports/.cspell.json at the root of your repository

(Truncated to last 6666 characters out of 640095)
⚠️ COPYPASTE / jscpd - 142 errors
ts/test-crossplane-egress-policy.sh [260:25 - 268:19] (9 lines, 52 tokens)
   scripts/tests/test-crossplane-egress-policy.sh [280:24 - 288:19]
Clone found (bash)
 - scripts/tests/test-crossplane-egress-policy.sh [260:25 - 268:18] (9 lines, 51 tokens)
   scripts/tests/test-crossplane-egress-policy.sh [305:31 - 313:18]
Clone found (bash)
 - scripts/tests/test-crossplane-egress-policy.sh [291:13 - 299:8] (9 lines, 52 tokens)
   scripts/tests/test-crossplane-egress-policy.sh [370:18 - 378:14]
Clone found (bash)
 - scripts/tests/test-crossplane-egress-policy.sh [369:22 - 378:15] (10 lines, 63 tokens)
   scripts/tests/test-crossplane-egress-policy.sh [379:33 - 388:15]
Clone found (bash)
 - scripts/tests/test-crossplane-sync-exporter.sh [186:1 - 235:2] (50 lines, 299 tokens)
   scripts/tests/test-kyverno-umami-mutation-rbac.sh [16:1 - 63:2]
Clone found (bash)
 - scripts/tests/test-crossplane-sync-exporter.sh [186:1 - 237:11] (52 lines, 303 tokens)
   scripts/tests/test-opencost-usage-scraper.sh [33:1 - 84:11]
Clone found (bash)
 - scripts/tests/test-crossplane-sync-exporter.sh [221:27 - 238:53] (18 lines, 53 tokens)
   scripts/tests/test-opencost-usage-scraper.sh [68:27 - 85:51]
Clone found (bash)
 - scripts/tests/test-headlamp-plugin-removal.sh [121:89 - 126:22] (6 lines, 65 tokens)
   scripts/tests/test-headlamp-plugin-removal.sh [165:84 - 170:22]
Clone found (bash)
 - scripts/tests/test-refresh-flux-ghcr-auth-safety.sh [348:5 - 356:51] (9 lines, 76 tokens)
   scripts/tests/test-refresh-flux-ghcr-auth-safety.sh [357:5 - 365:51]
Clone found (python)
 - scripts/tests/test_validate_homepage_bookmarks.py [46:57 - 54:54] (9 lines, 58 tokens)
   scripts/tests/test_validate_homepage_bookmarks.py [100:53 - 109:54]
Clone found (go)
 - scripts/validate-dr-signing/main_test.go [1007:13 - 1015:2] (9 lines, 51 tokens)
   scripts/validate-dr-signing/main_test.go [1075:12 - 1083:2]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [564:50 - 569:24] (6 lines, 103 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1076:31 - 1081:24]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [596:14 - 602:4] (7 lines, 115 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [923:43 - 929:4]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [622:13 - 631:7] (10 lines, 158 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1205:18 - 1214:7]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [631:1 - 636:8] (6 lines, 93 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1215:1 - 1220:8]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [708:5 - 713:2] (6 lines, 82 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [894:8 - 899:9]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [709:1 - 719:23] (11 lines, 220 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [986:60 - 997:3]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [713:15 - 724:2] (12 lines, 185 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [814:17 - 825:2]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [760:15 - 771:12] (12 lines, 264 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [969:1 - 982:3]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [823:2 - 829:19] (7 lines, 89 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1008:7 - 1014:19]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [823:2 - 829:33] (7 lines, 103 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1390:58 - 1396:33]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [823:2 - 829:4] (7 lines, 74 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1503:5 - 1509:4]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [916:30 - 921:8] (6 lines, 50 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [943:44 - 948:8]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [1030:22 - 1036:2] (7 lines, 135 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1589:22 - 1595:2]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [1328:33 - 1336:11] (9 lines, 118 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1340:130 - 1348:11]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [1535:47 - 1540:2] (6 lines, 166 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1615:46 - 1620:2]
Clone found (go)
 - scripts/validate-flux-verify/instance_test.go [109:62 - 126:31] (18 lines, 57 tokens)
   scripts/validate-flux-verify/instance_test.go [161:57 - 178:31]
Clone found (go)
 - scripts/validate-flux-verify/instance_test.go [109:62 - 128:35] (20 lines, 62 tokens)
   scripts/validate-flux-verify/instance_test.go [198:55 - 217:26]
Clone found (python)
 - scripts/validate-naming.py [126:52 - 132:25] (7 lines, 53 tokens)
   scripts/validate-naming.py [171:82 - 177:29]
┌────────┬────────────────┬─────────────┬──────────────┬──────────────┬──────────────────┬───────────────────┐
│ Format │ Files analyzed │ Total lines │ Total tokens │ Clones found │ Duplicated lines │ Duplicated tokens │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ bash   │ 55             │ 18261       │ 71945        │ 36           │ 490 (2.68%)      │ 3377 (4.69%)      │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ go     │ 37             │ 32369       │ 193468       │ 104          │ 858 (2.65%)      │ 7991 (4.13%)      │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ python │ 4              │ 807         │ 5606         │ 2            │ 14 (1.73%)       │ 111 (1.98%)       │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ txt    │ 1              │ 211         │ 1163         │ 0            │ 0 (0.00%)        │ 0 (0.00%)         │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ Total: │ 97             │ 51648       │ 272182       │ 142          │ 1362 (2.64%)     │ 11479 (4.22%)     │
└────────┴────────────────┴─────────────┴──────────────┴──────────────┴──────────────────┴───────────────────┘
Found 142 clones.
HTML report saved to megalinter-reports/copy-paste/jscpd-report.html
ERROR: jscpd found too many duplicates (2.6%) over threshold (0.0%)
time: 1.21s

(Truncated to last 6666 characters out of 27032)
⚠️ MARKDOWN / markdownlint - 65 errors
length Line length [Expected: 400; Actual: 439]
AGENTS.md:105:401 error MD013/line-length Line length [Expected: 400; Actual: 1126]
AGENTS.md:106:401 error MD013/line-length Line length [Expected: 400; Actual: 628]
AGENTS.md:107:401 error MD013/line-length Line length [Expected: 400; Actual: 1774]
AGENTS.md:160:401 error MD013/line-length Line length [Expected: 400; Actual: 649]
AGENTS.md:162:401 error MD013/line-length Line length [Expected: 400; Actual: 971]
AGENTS.md:189:401 error MD013/line-length Line length [Expected: 400; Actual: 970]
AGENTS.md:193:401 error MD013/line-length Line length [Expected: 400; Actual: 660]
AGENTS.md:211:401 error MD013/line-length Line length [Expected: 400; Actual: 1510]
AGENTS.md:279:401 error MD013/line-length Line length [Expected: 400; Actual: 1016]
AGENTS.md:280:401 error MD013/line-length Line length [Expected: 400; Actual: 491]
AGENTS.md:281:401 error MD013/line-length Line length [Expected: 400; Actual: 468]
AGENTS.md:287:401 error MD013/line-length Line length [Expected: 400; Actual: 532]
AGENTS.md:289:401 error MD013/line-length Line length [Expected: 400; Actual: 523]
AGENTS.md:292:401 error MD013/line-length Line length [Expected: 400; Actual: 613]
AGENTS.md:293:401 error MD013/line-length Line length [Expected: 400; Actual: 714]
AGENTS.md:297:401 error MD013/line-length Line length [Expected: 400; Actual: 502]
AGENTS.md:301:401 error MD013/line-length Line length [Expected: 400; Actual: 441]
AGENTS.md:306:401 error MD013/line-length Line length [Expected: 400; Actual: 427]
AGENTS.md:409:401 error MD013/line-length Line length [Expected: 400; Actual: 1139]
AGENTS.md:411:401 error MD013/line-length Line length [Expected: 400; Actual: 1240]
AGENTS.md:427:401 error MD013/line-length Line length [Expected: 400; Actual: 1137]
AGENTS.md:824:401 error MD013/line-length Line length [Expected: 400; Actual: 790]
AGENTS.md:829:401 error MD013/line-length Line length [Expected: 400; Actual: 515]
CLAUDE.md:1 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "@AGENTS.md"]
docs/dr/alerting.md:226:28 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:22:389 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:23:264 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:27:35 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:27:161 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:27:239 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:114 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "Custody recommendations"]
docs/dr/crypto-custody.md:245 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "Custody recommendations"]
docs/dr/crypto-custody.md:251 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "What to do if it leaks"]
docs/dr/crypto-custody.md:258 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "What to do if it is *lost* (no..."]
docs/dr/restore-drill.md:42 error MD028/no-blanks-blockquote Blank line inside blockquote
docs/dr/runbook.md:23:102 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/runbook.md:23:487 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/runbook.md:34 error MD028/no-blanks-blockquote Blank line inside blockquote
docs/dr/runbook.md:41 error MD028/no-blanks-blockquote Blank line inside blockquote
docs/dr/runbook.md:50 error MD028/no-blanks-blockquote Blank line inside blockquote
docs/dr/runbook.md:589:92 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/spire-server-ha.md:93 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/dr/velero-cnpg.md:11 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/dr/velero-cnpg.md:56:78 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/velero-cnpg.md:56:166 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/velero-cnpg.md:57:78 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/velero-cnpg.md:57:227 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/velero-cnpg.md:58:78 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/velero-cnpg.md:58:166 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/github-management.md:38:401 error MD013/line-length Line length [Expected: 400; Actual: 419]
docs/github-management.md:40:401 error MD013/line-length Line length [Expected: 400; Actual: 522]
docs/node-autoscaling.md:14 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/oidc-kubectl.md:95 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/runtime-security.md:114 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/rwx-storage.md:9 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/unifi-management.md:14 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/unifi-management.md:62 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
README.md:116:401 error MD013/line-length Line length [Expected: 400; Actual: 540]
README.md:237:32 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
README.md:237:36 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]

(Truncated to last 6666 characters out of 7160)
⚠️ REPOSITORY / trivy - 1 error
aquasec.com/misconfig/ksv-0125
────────────────────────────────────────
 k8s/providers/hetzner/apps/userns-longhorn-smoke/job.yaml:54-100
────────────────────────────────────────
  54 ┌         - name: write-sentinel
  55 │           image: docker.io/library/busybox:1.38.0@sha256:fd8d9aa63ba2f0982b5304e1ee8d3b90a210bc1ffb5314d980eb6962f1a9715d
  56 │           imagePullPolicy: IfNotPresent
  57 │           command:
  58 │             - /bin/sh
  59 │             - -ec
  60 │           args:
  61 │             - |
  62 └               set -eu
  ..   
────────────────────────────────────────



k8s/providers/hetzner/infrastructure/controllers/longhorn/cron-job-stale-node-cleanup.yaml (kubernetes)
=======================================================================================================
Tests: 116 (SUCCESSES: 115, FAILURES: 1)
Failures: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0)

KSV-0125 (MEDIUM): Container cleanup in cronjob longhorn-stale-node-cleanup (namespace: longhorn-system) uses an image from an untrusted registry.
════════════════════════════════════════
Ensure that all containers use images only from trusted registry domains.

See https://avd.aquasec.com/misconfig/ksv-0125
────────────────────────────────────────
 k8s/providers/hetzner/infrastructure/controllers/longhorn/cron-job-stale-node-cleanup.yaml:73-112
────────────────────────────────────────
  73 ┌             - name: cleanup
  74 │               # NOT registry.k8s.io/kubectl: that image is distroless (kubectl
  75 │               # binary only, no /bin/sh), so the shell script below could never
  76 │               # start — every run since the CronJob shipped failed with
  77 │               # StartError exit 128 "stat /bin/sh: no such file or directory"
  78 │               # (observed live 2026-07-02). alpine/k8s ships kubectl + a POSIX
  79 │               # shell; the tag tracks the kubectl minor, matching the cluster.
  80 │               image: docker.io/alpine/k8s:1.36.2@sha256:44ef4942e171939b9c665a4a84beb80e2dcdb9a24330d4651cfdfd2e9deecc47
  81 └               securityContext:
  ..   
────────────────────────────────────────



k8s/providers/hetzner/infrastructure/coroot/cron-job-alert-autosuppressor.yaml (kubernetes)
===========================================================================================
Tests: 116 (SUCCESSES: 115, FAILURES: 1)
Failures: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0)

KSV-0125 (MEDIUM): Container autosuppressor in cronjob coroot-alert-autosuppressor (namespace: observability) uses an image from an untrusted registry.
════════════════════════════════════════
Ensure that all containers use images only from trusted registry domains.

See https://avd.aquasec.com/misconfig/ksv-0125
────────────────────────────────────────
 k8s/providers/hetzner/infrastructure/coroot/cron-job-alert-autosuppressor.yaml:93-116
────────────────────────────────────────
  93 ┌             - name: autosuppressor
  94 │               # curl + jq, digest-pinned (same image as custom-cloud-pricing).
  95 │               # observability is exempt from disallow-latest-tag.
  96 │               image: docker.io/badouralix/curl-jq:latest@sha256:1e7c0284e24572ace7170df9fc91f15fd3b79ebf056d4dde17244d5d74bbfabc
  97 │               securityContext:
  98 │                 allowPrivilegeEscalation: false
  99 │                 readOnlyRootFilesystem: true
 100 │                 runAsNonRoot: true
 101 └                 runAsUser: 65532
 ...   
────────────────────────────────────────



k8s/providers/hetzner/infrastructure/coroot/cron-job-crossplane-sync-alerter.yaml (kubernetes)
==============================================================================================
Tests: 116 (SUCCESSES: 115, FAILURES: 1)
Failures: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0)

KSV-0125 (MEDIUM): Container alerter in cronjob crossplane-sync-alerter (namespace: observability) uses an image from an untrusted registry.
════════════════════════════════════════
Ensure that all containers use images only from trusted registry domains.

See https://avd.aquasec.com/misconfig/ksv-0125
────────────────────────────────────────
 k8s/providers/hetzner/infrastructure/coroot/cron-job-crossplane-sync-alerter.yaml:71-94
────────────────────────────────────────
  71 ┌             - name: alerter
  72 │               # curl + jq, digest-pinned (same image as the autosuppressor).
  73 │               # observability is exempt from disallow-latest-tag.
  74 │               image: docker.io/badouralix/curl-jq:latest@sha256:1e7c0284e24572ace7170df9fc91f15fd3b79ebf056d4dde17244d5d74bbfabc
  75 │               securityContext:
  76 │                 allowPrivilegeEscalation: false
  77 │                 readOnlyRootFilesystem: true
  78 │                 runAsNonRoot: true
  79 └                 runAsUser: 65532
  ..   
────────────────────────────────────────



k8s/providers/hetzner/infrastructure/coroot/cron-job-custom-cloud-pricing.yaml (kubernetes)
===========================================================================================
Tests: 116 (SUCCESSES: 115, FAILURES: 1)
Failures: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0)

KSV-0125 (MEDIUM): Container set-pricing in cronjob coroot-custom-cloud-pricing (namespace: observability) uses an image from an untrusted registry.
════════════════════════════════════════
Ensure that all containers use images only from trusted registry domains.

See https://avd.aquasec.com/misconfig/ksv-0125
────────────────────────────────────────
 k8s/providers/hetzner/infrastructure/coroot/cron-job-custom-cloud-pricing.yaml:77-114
────────────────────────────────────────
  77 ┌             - name: set-pricing
  78 │               # curl + jq, pinned by digest. jq replaces the former grep/sed/awk
  79 │               # JSON parsing + awk float compare with a robust, structure-tolerant
  80 │               # parse (the heartbeat CronJob still uses curlimages/curl — it has no
  81 │               # JSON to parse). No official curl+jq image exists, so this is the
  82 │               # de-facto community one, digest-pinned; observability is exempt from
  83 │               # disallow-latest-tag. Swappable for any curl+jq image.
  84 │               image: docker.io/badouralix/curl-jq:latest@sha256:1e7c0284e24572ace7170df9fc91f15fd3b79ebf056d4dde17244d5d74bbfabc
  85 └               securityContext:
  ..   
────────────────────────────────────────



📣 Notices:
  - Version 0.74.0 of Trivy is now available, current version is 0.73.0

To suppress version checks, run Trivy scans with the --skip-version-check flag

(Truncated to last 6666 characters out of 296170)

✅ Linters with no issues

actionlint, betterleaks, git_diff, grype, jsonlint, lychee, osv-scanner, prettier, prettier, revive, secretlint, shellcheck, shfmt, syft, trivy-sbom, trufflehog, v8r, v8r, yamllint, zizmor

Notices

⚠️ Your configuration references items that have been removed from MegaLinter and are ignored: REPOSITORY_GITLEAKS. See Removed linters to find their replacements.

See detailed reports in MegaLinter artifacts

MegaLinter is provided by OX Security
Show us your support by starring ⭐ the repository

…tener

The tenant hostname allow-list matches HTTPRoute, but the tenant role also
granted grpcroutes/tcproutes/tlsroutes/udproutes and the Gateway listeners
pinned no route kinds — and an HTTPS listener accepts GRPCRoute by default. A
tenant artifact could therefore claim another service's hostname under the
wildcard certificate without the policy ever running.

Narrow the role to httproutes + referencegrants (what its own comment already
described), pin allowedRoutes.kinds to HTTPRoute on all four listeners, and add
the negative coverage: a kyverno fixture for the hostname policy, which had
none, plus a structural test over the rendered prod overlay.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@coderabbitai review

New head 8fbb9d3c closes the P1 raised on this PR: the hostname policy matches HTTPRoute, so the tenant RBAC is narrowed to httproutes+referencegrants and every Gateway listener now pins allowedRoutes.kinds to HTTPRoute. Worth reviewing the two layers together — relaxing either alone reopens the gap.

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

@devantler I will review the hostname policy, tenant RBAC, and Gateway listener restrictions together.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@devantler, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

Limit details: You’ve used all 1 included review currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4d2a86cf-31ad-49d1-b53d-82e0172cf81c

📥 Commits

Reviewing files that changed from the base of the PR and between 6d926e4 and edf68b5.

📒 Files selected for processing (7)
  • .github/workflows/ci.yaml
  • docs/TENANTS.md
  • k8s/bases/infrastructure/cluster-roles/gateway-tenant-edit.yaml
  • k8s/bases/infrastructure/gateway/gateway.yaml
  • k8s/providers/hetzner/infrastructure/patches/add-ascoachingogvaner-dk-listeners.yaml
  • scripts/tests/test-tenant-route-hostname-boundary.sh
  • scripts/validate-eks-ci-role-policy/main.go

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@codex review

CodeRabbit is rate-limited at this head (Review rate limited, 57-minute window, free OSS quota exhausted), so advancing to the next lane. Recording the completed outcome for the lane before this one: cr:no-gate@8fbb9d3cd0251905fc27680db076309617c33a6b.

Focus: this closes a P1 where the tenant hostname policy matches HTTPRoute while the tenant RBAC also granted grpcroutes/tcproutes/tlsroutes/udproutes and no Gateway listener pinned allowedRoutes.kinds. The two layers must be assessed together — relaxing either alone reopens the hostname-claiming path.

…-tenant-hostname-vulnerability

# Conflicts:
#	.github/workflows/ci.yaml

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8fbb9d3cd0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The per-tenant rules are keyed by namespace name, which is fail-open: a tenant
with no rule is unrestricted and nothing signals it. doggy-countdown was in
exactly that state — an active tenant bound to tenant-edit, matched by no rule,
free to claim any hostname on the shared wildcard listener.

Add its rule (simba.platform.devantler.tech, from its own deploy/httproute.yaml)
and a default-deny for any other ksail-managed tenant namespace, so onboarding
the next tenant fails closed with a message naming the fix instead of silently
reopening the gap.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

State at 5cab71da — both P1 findings on this PR are fixed and every review thread is resolved. One mechanical step remains before this can merge.

Remaining blocker: the authorization-surface re-approval. 🔐 Validate EKS Authorization fails with unapproved rendered authorization surface fingerprint, and that is the guard working correctly, not a defect: this PR narrows the gateway-tenant-edit ClusterRole and adds two Kyverno rules, so the rendered authorization surface legitimately moved and expectedRenderedSurfaceSHA in scripts/validate-eks-ci-role-policy/main.go needs updating with a written justification of the delta.

Why it is not done in this run, rather than an oversight: that constant is deliberately not computable locally — the validator refuses the local toolchain as an unapproved renderer, so the fingerprint must be read from the required CI job's own output. The run for this head was still in progress when this run ended. Whoever picks it up:

  1. Read the fingerprint from the 🔐 Validate EKS Authorization job on the newest run for the current head.
  2. Update expectedRenderedSurfaceSHA with a justification in the established style — the delta here is: gateway-tenant-edit loses grpcroutes/tcproutes/tlsroutes/udproutes (grant-narrowing only; no identity, binding, or new verb), and restrict-tenant-http-route-hostnames gains two rules. Nothing is added to any identity's privileges.
  3. Re-request a review — every lane was exhausted at the time of writing: CodeRabbit rate-limited (free OSS quota, ~57-minute window), Codex spent on the previous head, Cursor Bugbot at its account usage limit.

⚠️ Note for whoever also drives platform#2742: it fails the same check for the same reason, so both PRs want to update the same constant. Whichever merges first invalidates the other's fingerprint, which will need re-reading rather than rebasing.

… tenant route grants

Narrowing gateway-tenant-edit and adding the tenant hostname policy move the
rendered authorization surface, so the guard correctly refused the old
fingerprint.

The delta is two documents and only one is RBAC: gateway-tenant-edit drops
grpcroutes/tcproutes/tlsroutes/udproutes with zero additions, identical verbs,
apiGroup and aggregation labels; the added ClusterPolicy only denies. Measured
by rendering all five authorization overlays from main and from this head — 71
RBAC documents each side, identity sets identical, exactly one differing.

Reproduced under both renderer controls rather than copied from CI: main
reproduces its prior constant and this tree reproduces the new one.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Authorization surface re-approved in 42a13b3e — this was the remaining blocker noted above, and it is now closed rather than carried forward.

The delta, measured rather than asserted. Rendered all five authorization overlays from main and from this head and diffed the canonical documents:

main head
Role / ClusterRole / RoleBinding / ClusterRoleBinding / ServiceAccount 10 / 22 / 15 / 10 / 14 identical
ClusterPolicy 26 27

71 RBAC documents on each side, identity sets identical, and exactly ONE differing in contentClusterRole/gateway-tenant-edit. Nothing added, removed or renamed anywhere else.

That one change is a strict narrowing: grpcroutes, tcproutes, tlsroutes, udproutes removed, leaving httproutes+referencegrants. The set difference in the addition direction is empty, the seven verbs are byte-identical, the single apiGroup is unchanged, and both aggregation labels are unchanged — so no identity gains anything. The added ClusterPolicy is the admission boundary itself; it grants nothing and only denies.

Both renderer controls pass, so this is verified rather than copied. The existing narrative in main.go says the local toolchain is refused as unapproved, so I initially wrote that the value could only be taken from CI — that turned out to be wrong for this surface and I corrected it before committing. Measured: local kubectl is v1.36.1 against the pinned v1.36.2 (same embedded Kustomize v5.8.1), and TestValidateAuthorizationAcceptsCommittedPolicy runs and passes locally in both treesmain reproduces its prior constant 1c4c1986…, and this tree reproduces 55533168…, the value CI reported. A local render that could not reproduce a known-good constant would say nothing about what CI hashed; both controls passing is what makes the delta above admissible.

Ablated it too: reverting to the old constant fails reporting exactly the new fingerprint, and restoring it passes.

⚠️ Still true for #2742: it fails the same check for its own reasons and will need its own fingerprint — and once either PR merges, the other's becomes stale and must be re-read rather than rebased.

@devantler
devantler added this pull request to the merge queue Aug 16, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 16, 2026
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@coderabbitai review

Current head 21a5c065cf67cfbfdefc2d5d68f0f19af4e2c61c is green after merging the recovered production base. Please review the complete seven-file route-kind boundary: tenant RBAC is narrowed to HTTPRoute plus ReferenceGrant, all rendered Gateway listeners restrict allowed route kinds to HTTPRoute, and the focused regression plus authorization fingerprint bind both enforcement layers.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="devantler-tech/platform"
requested_head="21a5c065cf67cfbfdefc2d5d68f0f19af4e2c61c"

echo '=== Pull request metadata ==='
gh pr view 2713 --repo "$repo" \
  --json number,title,baseRefName,headRefName,isDraft,mergeStateStatus,reviewDecision,commits,files,statusCheckRollup \
  --jq '{
    number, title, base: .baseRefName, head: .headRefName, isDraft, mergeStateStatus, reviewDecision,
    commits: [.commits[].oid],
    files: [.files[].path],
    checks: [.statusCheckRollup[] | {name, conclusion, status}]
  }'

echo
echo '=== Repository heads ==='
printf 'HEAD: '; git rev-parse HEAD
printf 'Requested: %s\n' "$requested_head"
printf 'Merge base: '; git merge-base origin/main HEAD

echo
echo '=== Pull request diff summary ==='
git diff --stat --find-renames origin/main...HEAD
echo '=== Changed paths ==='
git diff --name-status --find-renames origin/main...HEAD
echo '=== Complete pull request diff ==='
git diff --find-renames --no-ext-diff origin/main...HEAD

echo
echo '=== Relevant source files ==='
for path in \
  k8s/bases/infrastructure/cluster-roles/gateway-tenant-edit.yaml \
  k8s/bases/infrastructure/cluster-roles/tenant-edit.yaml \
  k8s/bases/infrastructure/gateway/gateway.yaml \
  k8s/providers/hetzner/infrastructure/patches/add-ascoachingogvaner-dk-listeners.yaml \
  k8s/bases/infrastructure/cluster-policies/best-practices/restrict-tenant-http-route-hostnames.yaml \
  scripts/tests/test-tenant-route-hostname-boundary.sh \
  scripts/validate-eks-ci-role-policy/main.go \
  .github/workflows/ci.yaml \
  docs/TENANTS.md; do
  if [ -f "$path" ]; then
    echo "--- ${path} ---"
    nl -ba "$path" | sed -n '1,300p'
  fi
done

echo
echo '=== Gateway API RBAC rules and tenant bindings ==='
rg -n -i -U -C 6 \
  'apiGroups:\s*\[?"?gateway\.networking\.k8s\.io"?\]?[\s\S]{0,800}?verbs:|kind:\s*RoleBinding[\s\S]{0,1400}?roleRef:[\s\S]{0,350}?name:\s*tenant-edit\b|aggregate-to-tenant-edit' \
  -g '*.yaml' -g '*.yml' k8s

echo
echo '=== Gateway resources and route-kind restrictions ==='
rg -n -i -U -C 8 \
  '^kind:\s*Gateway\s*$[\s\S]{0,6000}?(^---\s*$|\z)|allowedRoutes:|kind:\s*(HTTPRoute|GRPCRoute|TCPRoute|TLSRoute|UDPRoute)' \
  -g '*.yaml' -g '*.yml' k8s

echo
echo '=== Test invocation and authorization-fingerprint context ==='
rg -n -C 10 \
  'test-tenant-route-hostname-boundary\.sh|expectedRenderedSurfaceSHA|RenderedSurface|fingerprint|sha256' \
  .github scripts

Length of output: 22761


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Current head and base relationship ==='
printf 'HEAD: '; git rev-parse HEAD
printf 'origin/main: '; git rev-parse origin/main
printf 'merge-base: '; git merge-base origin/main HEAD
git merge-base --is-ancestor origin/main HEAD && echo 'origin/main is contained in HEAD' || echo 'origin/main is not contained in HEAD'
echo '=== Commits after merge base ==='
git log --oneline "$(git merge-base origin/main HEAD)..HEAD" --max-count=30

print_file() {
  path="$1"
  if [ -f "$path" ]; then
    echo "--- $path ---"
    awk '{ printf "%6d  %s\n", NR, $0 }' "$path"
  else
    echo "--- MISSING: $path ---"
  fi
}

echo
echo '=== Boundary source inputs ==='
for path in \
  k8s/bases/infrastructure/cluster-roles/gateway-tenant-edit.yaml \
  k8s/bases/infrastructure/cluster-roles/tenant-edit.yaml \
  k8s/bases/infrastructure/gateway/gateway.yaml \
  k8s/providers/hetzner/infrastructure/patches/add-ascoachingogvaner-dk-listeners.yaml \
  scripts/tests/test-tenant-route-hostname-boundary.sh \
  .github/workflows/ci.yaml \
  scripts/validate-eks-ci-role-policy/main.go; do
  print_file "$path"
done

echo
echo '=== All Gateway API grants ==='
rg -n -i -U -C 8 \
  'apiGroups:\s*\[?"?gateway\.networking\.k8s\.io"?\]?[\s\S]{0,800}?verbs:' \
  -g '*.yaml' -g '*.yml' k8s

echo
echo '=== All tenant-edit aggregation labels and RoleBindings ==='
rg -n -i -U -C 8 \
  'aggregate-to-tenant-edit|kind:\s*RoleBinding[\s\S]{0,1400}?roleRef:[\s\S]{0,350}?name:\s*tenant-edit\b' \
  -g '*.yaml' -g '*.yml' k8s

echo
echo '=== Gateway resources, listener patches, and overlay wiring ==='
rg -n -i -U -C 8 \
  '^kind:\s*Gateway\s*$[\s\S]{0,6000}?(^---\s*$|\z)|allowedRoutes:|kind:\s*(HTTPRoute|GRPCRoute|TCPRoute|TLSRoute|UDPRoute)' \
  -g '*.yaml' -g '*.yml' k8s
fd -t f '^kustomization\.yaml$' k8s/providers/hetzner/infrastructure -x sh -c \
  'echo "--- $1 ---"; awk "{ printf \"%6d  %s\\n\", NR, \$0 }" "$1"' sh {}

echo
echo '=== CI path filter and boundary-test invocation ==='
rg -n -C 16 \
  'test-tenant-route-hostname-boundary\.sh|k8s/\*\*|k8s/|scripts/tests/' \
  .github/workflows/ci.yaml

echo
echo '=== Relevant test fixtures and policy references ==='
fd -t f . tests | rg 'restrict-tenant-route-hostnames|tenant-route'
rg -n -C 5 \
  'restrict-tenant-route-hostnames|validationFailureAction|failureAction|HTTPRoute' \
  -g '*.yaml' -g '*.yml' k8s/bases/infrastructure/cluster-policies tests

Length of output: 187514


@devantler No findings in the seven-file route-kind boundary at 21a5c065.

I verified these controls:

  • gateway-tenant-edit grants exactly httproutes and referencegrants.
  • The role remains tenant-specific. It does not aggregate into built-in edit.
  • The base Gateway listeners and the two Hetzner-added listeners pin allowedRoutes.kinds to HTTPRoute.
  • The boundary test renders the Hetzner production overlay. It fails if the RBAC resource set widens or any Gateway/platform listener allows a route kind other than HTTPRoute.
  • CI runs ShellCheck and the boundary test for Kubernetes changes.
  • The authorization fingerprint rationale matches the strict removal of the four alternate Gateway API route kinds.
  • The hostname policy remains Enforce, and its fixtures cover the tenant hostname cases that the route-kind test intentionally does not cover.

The reported CI checks succeeded, including 🔐 Validate EKS Authorization and 🧪 Validate Manifests.

GitHub currently reports mergeStateStatus: DIRTY. Resolve the current base conflict or divergence before merge, then rerun the required checks.


🧠 Learnings used
Learnt from: devantler
Repo: devantler-tech/platform PR: 2740
File: k8s/bases/infrastructure/cluster-policies/best-practices/restrict-tenant-route-hostnames.yaml:61-61
Timestamp: 2026-08-16T03:58:51.588Z
Learning: In devantler-tech/platform, the live Kyverno v1.18.2 deployment still serves and enforces the deprecated top-level `spec.validationFailureAction` field for ClusterPolicy resources. All 10 ClusterPolicy manifests currently use the top-level field and none use per-rule `validate.failureAction`. Migrate this setting repository-wide, while preserving the existing 6 Audit and 4 Enforce actions, under GitHub issue `#3163`. Include an effective-action guard because `kyverno test` evaluates rule results but does not verify the admission failure action.

You are interacting with an AI system.

…-tenant-hostname-vulnerability

# Conflicts:
#	scripts/validate-eks-ci-role-policy/main.go
…merge result

The branch's previous value was rendered against an older main and did not
describe the merge result. Records the value both the required CI job and a
local render measured, with the reviewed delta unchanged.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="devantler-tech/platform"
requested_head="03df5f161b74b909aed87d87baa95b42b526dab4"
prior_head="21a5c065cf67cfbfdefc2d5d68f0f19af4e2c61c"

echo '=== Pull request state ==='
gh pr view 2713 --repo "$repo" \
  --json number,title,baseRefName,headRefName,isDraft,mergeStateStatus,reviewDecision,commits,files,statusCheckRollup \
  --jq '{
    number,title,base:.baseRefName,head:.headRefName,isDraft,mergeStateStatus,reviewDecision,
    commits:[.commits[].oid],
    files:[.files[].path],
    checks:[.statusCheckRollup[]|{name,conclusion,status}]
  }'

echo
echo '=== Head and merge base ==='
printf 'HEAD: '; git rev-parse HEAD
printf 'requested: %s\n' "$requested_head"
printf 'merge base: '; git merge-base origin/main HEAD
git merge-base --is-ancestor origin/main HEAD && echo 'base contained: yes' || echo 'base contained: no'

echo
echo '=== Complete PR diff ==='
git diff --stat --find-renames origin/main...HEAD
git diff --find-renames --no-ext-diff origin/main...HEAD

echo
echo '=== Delta from prior reviewed head ==='
if git cat-file -e "${prior_head}^{commit}" 2>/dev/null; then
  git diff --stat --find-renames "${prior_head}...HEAD"
  git diff --find-renames --no-ext-diff "${prior_head}...HEAD"
else
  echo "Prior head unavailable: ${prior_head}"
fi

echo
echo '=== Security-boundary source files ==='
for path in \
  k8s/bases/infrastructure/cluster-roles/gateway-tenant-edit.yaml \
  k8s/bases/infrastructure/cluster-roles/tenant-edit.yaml \
  k8s/bases/infrastructure/gateway/gateway.yaml \
  k8s/providers/hetzner/infrastructure/patches/add-ascoachingogvaner-dk-listeners.yaml \
  k8s/bases/infrastructure/cluster-policies/best-practices/restrict-tenant-http-route-hostnames.yaml \
  scripts/tests/test-tenant-route-hostname-boundary.sh \
  scripts/validate-eks-ci-role-policy/main.go \
  .github/workflows/ci.yaml \
  docs/TENANTS.md; do
  if [ -f "$path" ]; then
    echo "--- ${path} ---"
    nl -ba "$path" | sed -n '1,320p'
  fi
done

echo
echo '=== Gateway API RBAC, Gateway listeners, and tenant bindings ==='
rg -n -i -U -C 6 \
  'apiGroups:\s*\[?"?gateway\.networking\.k8s\.io"?\]?[\s\S]{0,800}?verbs:|^kind:\s*Gateway\s*$[\s\S]{0,6000}?(^---\s*$|\z)|allowedRoutes:|kind:\s*(HTTPRoute|GRPCRoute|TCPRoute|TLSRoute|UDPRoute)|aggregate-to-tenant-edit|kind:\s*RoleBinding[\s\S]{0,1400}?roleRef:[\s\S]{0,350}?name:\s*tenant-edit\b' \
  -g '*.yaml' -g '*.yml' k8s

echo
echo '=== Boundary-test and CI invocation evidence ==='
rg -n -C 12 \
  'test-tenant-route-hostname-boundary\.sh|shellcheck.*tenant-route-hostname-boundary|expectedRenderedSurfaceSHA|restrict-tenant-http-route-hostnames' \
  .github scripts docs k8s

echo
echo '=== Conflict markers in changed files ==='
changed="$(git diff --name-only origin/main...HEAD)"
if [ -n "$changed" ] && printf '%s\n' "$changed" | xargs -r rg -n '^(<<<<<<<|=======|>>>>>>>)'; then
  exit 1
else
  echo 'none'
fi

Length of output: 251118


🏁 Script executed:

#!/bin/bash
set -euo pipefail

print_numbered() {
  path="$1"
  echo "--- ${path} ---"
  awk '{ printf "%6d  %s\n", NR, $0 }' "$path"
}

echo '=== Current head commit and direct parent delta ==='
git show --no-ext-diff --format=fuller --stat HEAD
git show --no-ext-diff --format= --find-renames HEAD
echo '--- parent 1 delta ---'
git diff --no-ext-diff --find-renames HEAD^1..HEAD -- \
  .github/workflows/ci.yaml \
  docs/TENANTS.md \
  k8s/bases/infrastructure/cluster-roles/gateway-tenant-edit.yaml \
  k8s/bases/infrastructure/gateway/gateway.yaml \
  k8s/providers/hetzner/infrastructure/patches/add-ascoachingogvaner-dk-listeners.yaml \
  scripts/tests/test-tenant-route-hostname-boundary.sh \
  scripts/validate-eks-ci-role-policy/main.go

echo
echo '=== Current boundary source ==='
for path in \
  k8s/bases/infrastructure/cluster-roles/gateway-tenant-edit.yaml \
  k8s/bases/infrastructure/cluster-roles/tenant-edit.yaml \
  k8s/bases/infrastructure/gateway/gateway.yaml \
  k8s/providers/hetzner/infrastructure/patches/add-ascoachingogvaner-dk-listeners.yaml \
  scripts/tests/test-tenant-route-hostname-boundary.sh \
  .github/workflows/ci.yaml \
  scripts/validate-eks-ci-role-policy/main.go; do
  print_numbered "$path"
done

echo
echo '=== Gateway manifests and Kustomize overlay wiring ==='
rg -n -i -U -C 8 \
  '^kind:\s*Gateway\s*$[\s\S]{0,6500}?(^---\s*$|\z)|allowedRoutes:|^\s*kinds:|kind:\s*(HTTPRoute|GRPCRoute|TCPRoute|TLSRoute|UDPRoute)' \
  -g '*.yaml' -g '*.yml' k8s
fd -t f '^kustomization\.yaml$' k8s/providers/hetzner/infrastructure -x sh -c \
  'echo "--- $1 ---"; awk "{ printf \"%6d  %s\\n\", NR, \$0 }" "$1"' sh {}

echo
echo '=== Tenant role aggregation and bindings ==='
rg -n -i -U -C 8 \
  'gateway-tenant-edit|aggregate-to-tenant-edit|aggregate-to-edit|kind:\s*RoleBinding[\s\S]{0,1500}?roleRef:[\s\S]{0,400}?name:\s*tenant-edit\b' \
  -g '*.yaml' -g '*.yml' k8s

echo
echo '=== Boundary test dependencies and CI execution ==='
rg -n -C 12 \
  'test-tenant-route-hostname-boundary\.sh|shellcheck scripts/tests/test-tenant-route-hostname-boundary\.sh|bash scripts/tests/test-tenant-route-hostname-boundary\.sh' \
  .github scripts

echo
echo '=== Policy surface relevant to HTTPRoute hostname restrictions ==='
rg -n -C 10 \
  'restrict-tenant-route-hostnames|kind:\s*HTTPRoute|validationFailureAction|failureAction' \
  -g '*.yaml' -g '*.yml' \
  k8s/bases/infrastructure/cluster-policies tests/restrict-tenant-route-hostnames

Length of output: 202746


@devantler No findings in the route-kind boundary at 03df5f161b74b909aed87d87baa95b42b526dab4.

I verified these controls:

  • gateway-tenant-edit grants only httproutes and referencegrants.
  • The role aggregates only into tenant-edit.
  • Tenant ServiceAccounts bind to tenant-edit.
  • The base Gateway listeners and both Hetzner-added listeners allow only HTTPRoute.
  • The Hetzner infrastructure overlay applies the listener patch to Gateway/platform.
  • The boundary test renders that overlay and fails if either the tenant RBAC set widens or a listener permits another route kind.
  • CI runs ShellCheck and the boundary test for Kubernetes changes.
  • The authorization-surface fingerprint rationale matches the strict removal of the four alternate route kinds.

GitHub reports the pull request as CLEAN. The reported required checks succeeded, including 🔐 Validate EKS Authorization and 🧪 Validate Manifests.


🧠 Learnings used
Learnt from: devantler
Repo: devantler-tech/platform PR: 2740
File: k8s/bases/infrastructure/cluster-policies/best-practices/restrict-tenant-route-hostnames.yaml:61-61
Timestamp: 2026-08-16T03:58:51.588Z
Learning: In devantler-tech/platform, the live Kyverno v1.18.2 deployment still serves and enforces the deprecated top-level `spec.validationFailureAction` field for ClusterPolicy resources. All 10 ClusterPolicy manifests currently use the top-level field and none use per-rule `validate.failureAction`. Migrate this setting repository-wide, while preserving the existing 6 Audit and 4 Enforce actions, under GitHub issue `#3163`. Include an effective-action guard because `kyverno test` evaluates rule results but does not verify the admission failure action.

You are interacting with an AI system.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Parked on a named blocker: the authorization fingerprint needs a re-measurement this run could not complete to the protocol's standard. Recording what I established so the next attempt is one step rather than a rediscovery.

The conflict is two files, and only one is hard.

.github/workflows/ci.yaml is trivial — the two sides add different test files to the same filter list (test-tenant-route-hostname-boundary.sh here, test-umami-provisioning-bootstrap.sh from #2725). Both entries belong; keeping both resolves it.

scripts/validate-eks-ci-role-policy/main.go conflicts on expectedRenderedSurfaceSHA, and neither side's value describes the merge result:

  • this branch's 9a9a0102… was measured against main 6ebcb24f, before #2725 merged (04:43Z today), so it predates the Umami grant now in main;
  • main's 295c44e3… was measured without this PR's ClusterRole reduction.

So the merge needs a fresh measurement against main d925654e, with both comment blocks retained — the running log reads chronologically and #2725's entry is referred to by later text.

Why I did not just write a value. The recorded protocol requires two independent renderers agreeing. I obtained the pinned renderer (kubectl v1.36.2 / Kustomize v5.8.1, checksum-verified against the published dl.k8s.io digest, since the validator rejects v1.36.1 outright as unapproved renderer) and rendered the resolved merge locally. It produced a candidate — b8235416c8452d2a123eb9279d64a4d6adc11625b9ed3be6801cd8abd1794a78but simultaneously reported 35 unresolved Flux substitution problems spanning HelmReleases this PR does not touch (cilium, dex, velero, longhorn and others). That means the local render is incomplete relative to the CI one, so its fingerprint is not trustworthy on its own and I will not approve a security constant on it.

Treat that candidate as unconfirmed, not as the answer. The next step is a render in an environment where those substitutions resolve, cross-checked against the required 🔐 Validate EKS Authorization job — which is the second renderer the protocol names, and the repo's own source of truth.

Nothing else on this PR is blocking: 0 unresolved threads, 0 body findings, and no current-head green review (CodeRabbit answered the 04:31Z request at this head with its generic chat tip rather than a review — recorded as cr:no-gate@03df5f16). The review lane should be advanced only after the conflict is resolved, since that push stales any green anyway.

…ility

Resolves two conflicts.

.github/workflows/ci.yaml — each side appended a different test path to the
same k8s filter list; both belong, so both are kept.

scripts/validate-eks-ci-role-policy/main.go — both parents re-approved
expectedRenderedSurfaceSHA against the same exact main 6ebcb24, but each
described only its own delta (this branch's gateway-tenant-edit route-kind
narrowing; main's #2725 Umami provisioning grant). Neither value describes the
merge result. Both records are retained because both deltas are present in the
merged surface and neither cancels the other; main's value is carried through
UNMEASURED so the required authorization job reports the digest the merged
surface actually renders to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

This was DIRTY against main and could not merge. Resolved the conflict (03df5f16c790f999); no code in the fix itself changed.

.github/workflows/ci.yaml — each side appended a different test path to the same k8s filter list (this branch's tenant route-hostname boundary test, main's Umami provisioning bootstrap test). Both belong, so both are kept.

scripts/validate-eks-ci-role-policy/main.go — the interesting one. Both parents had re-approved expectedRenderedSurfaceSHA against the same exact main 6ebcb24f, but each measured only its own delta: this branch the gateway-tenant-edit route-kind narrowing, main the #2725 Umami provisioning grant. Neither value describes their union, so the conflict could not be resolved by picking a side.

Both records are retained, because both deltas are genuinely present in the merged surface and neither cancels the other — one is a strict privilege reduction, the other a Lease-scoped grant. Main's value is carried through unmeasured, so 🔐 Validate EKS Authorization will reject it and report the digest the merged surface actually renders to; that reported value is what should replace it. Guessing it locally is not possible while Flux substitutions are unresolved.

I did not touch the security fix's own reasoning — the reduction it makes is unaffected by the merge.

Note the branch is in the codex/* lane. I took this on because its last push was ~12h old, so the active-work test showed nobody mid-flight, and per the portfolio PR-ownership rule a conflict is repair work rather than parallel work. Nothing else on the branch was modified.

The previous value was main's, carried through the merge unmeasured, so
`🔐 Validate EKS Authorization` rejected it exactly as the comment beside
it predicted. This records the digest the merged surface actually renders
to.

Two independent renderers agree: the required CI job (run 32019353067)
and a local render on kubectl v1.36.1 / kustomize v5.8.1.

Conservation: the whole-surface fingerprint is the only control that
moved. Both renderers report zero per-identity mismatches and zero
missing resources, so every individually approved entry — both parents'
deltas included — is byte-identical to its recorded value. Both report
the same 35 unresolved-substitution notes, which the validator emits as
diagnostics rather than a control.

That symmetry also corrects the assumption recorded before the
measurement: the approved toolchain reports the same notes and the same
digest, so a local render is a genuine second renderer here, not a
degraded one.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Completed the digest follow-up the expectedRenderedSurfaceSHA comment described. 90cf208e records b8235416c8452d2a123eb9279d64a4d6adc11625b9ed3be6801cd8abd1794a78.

Diagnosis. 🔐 Validate EKS Authorization and 🧪 Test both failed on this head, reporting an unapproved surface fingerprint plus 35 unresolved Flux substitution notes. The notes are not a second failure — validateAuthorization collects them into substitutionProblems and emits them only alongside a mismatch, deliberately, because a resource carrying ${…} is forced into the aggregate so its literal text is already covered by the digest. The code says so directly: promoting them to errors was measured on 2026-07-21 to fail the approved tree on thirty-plus HelmReleases. So there was exactly one control failure: the whole-surface digest.

Not main. platform#3171, also at an up-to-date head, passes both workflows, so main is healthy and this is confined to this PR. This head is behind_by=0, so it is not a stale-base case either.

Two independent renderers agree, satisfying the protocol:

Renderer Digest Substitution notes
Required CI job (run 32019353067) b8235416… 35
Local, kubectl v1.36.1 / kustomize v5.8.1 b8235416… 35

Note the two CI jobs are one renderer, not two — both run main_test.go:1140. The local render is what makes the second.

Conservation. The aggregate is the only thing that moved: both renderers report zero per-identity mismatches against expectedRenderedHashes and zero missing resources, so every individually approved entry — both parents’ deltas included — is byte-identical to its recorded value. The reviewed reasoning for the gateway-tenant-edit route-kind narrowing is untouched.

One correction recorded in the comment. The pre-measurement note held that a local render could not approve this, being incomplete without the CI substitution inputs. Measured, the approved toolchain reports the same 35 notes and the same digest, so the renders are equivalent here and the local one is a genuine second renderer rather than a degraded copy.

Package green locally. CI re-runs at 90cf208e.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

CI is now fully green at this head (🔐 Validate EKS Authorization included) and there is no conflict with base. This PR is gated only on a current-head review.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="devantler-tech/platform"
requested_head="90cf208efc4405cc5bacbe27a1321ca6029b7984"
prior_head="03df5f161b74b909aed87d87baa95b42b526dab4"

echo '=== Pull request state ==='
gh pr view 2713 --repo "$repo" \
  --json number,title,baseRefName,headRefName,isDraft,mergeStateStatus,reviewDecision,commits,files,statusCheckRollup \
  --jq '{
    number,title,base:.baseRefName,head:.headRefName,isDraft,mergeStateStatus,reviewDecision,
    commits:[.commits[].oid],
    files:[.files[].path],
    checks:[.statusCheckRollup[]|{name,conclusion,status}]
  }'

echo
echo '=== Head and base ==='
printf 'HEAD: '; git rev-parse HEAD
printf 'requested: %s\n' "$requested_head"
printf 'merge-base: '; git merge-base origin/main HEAD
git merge-base --is-ancestor origin/main HEAD && echo 'base-contained: yes' || echo 'base-contained: no'

echo
echo '=== PR summary and complete diff ==='
git diff --stat --find-renames origin/main...HEAD
git diff --name-status --find-renames origin/main...HEAD
git diff --find-renames --no-ext-diff origin/main...HEAD

echo
echo '=== Delta since prior reviewed head ==='
git diff --stat --find-renames "${prior_head}...HEAD"
git diff --find-renames --no-ext-diff "${prior_head}...HEAD"

echo
echo '=== Current head commit ==='
git show --no-ext-diff --format=fuller --stat HEAD
git show --no-ext-diff --format= --find-renames HEAD

echo
echo '=== Relevant source excerpts ==='
for path in \
  k8s/bases/infrastructure/cluster-roles/gateway-tenant-edit.yaml \
  k8s/bases/infrastructure/cluster-roles/tenant-edit.yaml \
  k8s/bases/infrastructure/gateway/gateway.yaml \
  k8s/providers/hetzner/infrastructure/patches/add-ascoachingogvaner-dk-listeners.yaml \
  k8s/bases/infrastructure/cluster-policies/best-practices/restrict-tenant-http-route-hostnames.yaml \
  scripts/tests/test-tenant-route-hostname-boundary.sh \
  scripts/validate-eks-ci-role-policy/main.go \
  .github/workflows/ci.yaml \
  docs/TENANTS.md; do
  if [ -f "$path" ]; then
    echo "--- $path ---"
    nl -ba "$path" | sed -n '1,700p'
  fi
done

echo
echo '=== Gateway API RBAC and Gateway listener definitions ==='
rg -n -i -U -C 8 \
  'apiGroups:\s*\[?"?gateway\.networking\.k8s\.io"?\]?[\s\S]{0,800}?verbs:|^kind:\s*Gateway\s*$[\s\S]{0,6500}?(^---\s*$|\z)|allowedRoutes:|kind:\s*(HTTPRoute|GRPCRoute|TCPRoute|TLSRoute|UDPRoute)|aggregate-to-tenant-edit|aggregate-to-edit' \
  -g '*.yaml' -g '*.yml' k8s

echo
echo '=== Boundary test and workflow invocation ==='
rg -n -C 12 \
  'test-tenant-route-hostname-boundary\.sh|shellcheck scripts/tests/test-tenant-route-hostname-boundary\.sh|bash scripts/tests/test-tenant-route-hostname-boundary\.sh|expectedRenderedSurfaceSHA|b8235416c8452d2a123eb9279d64a4d6adc11625b9ed3be6801cd8abd1794a78|unresolved Flux substitution' \
  .github scripts

echo
echo '=== Conflict markers in changed files ==='
changed="$(git diff --name-only origin/main...HEAD)"
if [ -n "$changed" ] && printf '%s\n' "$changed" | xargs -r rg -n '^(<<<<<<<|=======|>>>>>>>)'; then
  exit 1
else
  echo 'none'
fi

Length of output: 131819


🏁 Script executed:

#!/bin/bash
set -euo pipefail

print_file() {
  path="$1"
  echo "--- ${path} ---"
  awk '{ printf "%6d  %s\n", NR, $0 }' "$path"
}

echo '=== Exact route-kind boundary source ==='
for path in \
  k8s/bases/infrastructure/cluster-roles/gateway-tenant-edit.yaml \
  k8s/bases/infrastructure/cluster-roles/tenant-edit.yaml \
  k8s/bases/infrastructure/gateway/gateway.yaml \
  k8s/providers/hetzner/infrastructure/patches/add-ascoachingogvaner-dk-listeners.yaml \
  scripts/tests/test-tenant-route-hostname-boundary.sh \
  .github/workflows/ci.yaml; do
  print_file "$path"
done

echo
echo '=== Authorization validator map and validation logic ==='
validator="scripts/validate-eks-ci-role-policy/main.go"
ast-grep outline "$validator" --items all --type function 2>/dev/null || true
rg -n -C 16 \
  'expectedRenderedSurfaceSHA|expectedRenderedHashes|substitutionProblems|unresolved Flux substitution|validateAuthorization|rendered surface|surface fingerprint' \
  "$validator"

echo
echo '=== Boundary-test dependency and render selector checks ==='
rg -n -C 8 \
  'kubectl kustomize|Gateway|gateway-tenant-edit|expected_grant|allowedRoutes|listener_count|unpinned|yq eval' \
  scripts/tests/test-tenant-route-hostname-boundary.sh

echo
echo '=== Hetzner infrastructure Kustomize patch wiring ==='
fd -t f '^kustomization\.yaml$' k8s/providers/hetzner/infrastructure -x sh -c \
  'echo "--- $1 ---"; awk "{ printf \"%6d  %s\\n\", NR, \$0 }" "$1"' sh {}

echo
echo '=== Tenant-edit aggregation and tenant binding references ==='
rg -n -U -C 7 \
  'gateway-tenant-edit|aggregate-to-tenant-edit|aggregate-to-edit|kind:\s*RoleBinding[\s\S]{0,1400}?roleRef:[\s\S]{0,350}?name:\s*tenant-edit\b' \
  -g '*.yaml' -g '*.yml' k8s

echo
echo '=== All Gateway API route-kind grants and listeners ==='
rg -n -i -U -C 6 \
  'apiGroups:\s*\[?"?gateway\.networking\.k8s\.io"?\]?[\s\S]{0,700}?verbs:|^kind:\s*Gateway\s*$[\s\S]{0,6500}?(^---\s*$|\z)|allowedRoutes:|kind:\s*(HTTPRoute|GRPCRoute|TCPRoute|TLSRoute|UDPRoute)' \
  -g '*.yaml' -g '*.yml' k8s

Length of output: 245352


@devantler No findings at 90cf208efc4405cc5bacbe27a1321ca6029b7984.

I verified the complete route-kind boundary:

  • gateway-tenant-edit grants only httproutes and referencegrants.
  • The role aggregates only into tenant-edit.
  • Tenant ServiceAccounts bind to tenant-edit.
  • All four rendered Gateway/platform listeners restrict allowedRoutes.kinds to HTTPRoute.
  • The boundary test renders the Hetzner infrastructure overlay and rejects either RBAC widening or any listener that allows another route kind.
  • CI runs ShellCheck and the boundary test for Kubernetes changes.
  • The updated authorization-surface fingerprint is consistent with the merged surface. The validator retains unresolved Flux substitutions in the aggregate fingerprint and reports them only as diagnostics on a mismatch.

GitHub reports the pull request as CLEAN. The required checks succeeded, including 🔐 Validate EKS Authorization and 🧪 Validate Manifests.

You are interacting with an AI system.

…ility

Resolves one conflict in scripts/validate-eks-ci-role-policy/main.go, where
both parents had re-approved expectedRenderedSurfaceSHA independently — this
branch for the gateway-tenant-edit route-kind narrowing, main for the crossview
reload annotation. Neither parent's value describes the merge result, so the
conflict could not be resolved by picking a side; the merged surface renders to
b5b39418. Both parents' prose records are retained because both deltas are
present in the merged surface.

Two independent renderers agree on the new value, and each was first proved
against clean main b9af389 as a matched control: the approved toolchain
(checksum-verified kubectl v1.36.2 / Kustomize v5.8.1) running the validator,
and a local render on kubectl v1.36.1 / Kustomize v5.8.1 through
TestValidateAuthorizationAcceptsCommittedPolicy. The matched controls are what
make this a measurement rather than a guess: both renderers reproduce every
already-approved digest in main, so the single value they disagree with main
about is the one this merge moved.

Conservation: the whole-surface fingerprint is the only control that moved.
The run reports exactly one problem — this aggregate — plus zero per-identity
mismatches and zero missing resources, and the same 35 unresolved-substitution
notes clean main reports, which the validator emits as diagnostics rather than
as a control.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Un-conflicted at 8d607154. #3171 merged into main 30 minutes after this PR's last head, and both sides had independently re-approved expectedRenderedSurfaceSHA — this branch for the gateway-tenant-edit route-kind narrowing, main for the crossview reload annotation. Neither parent's value describes the merge result, so the conflict could not be resolved by picking a side; it needed a genuine re-measurement. Both parents' prose records are retained, because both deltas are present in the merged surface.

Measured value: b5b394181a8f2bc325bd427a90990fbc3872fb1f41f3d938f1e28f0aac1075f4

Two independent renderers agree, and each was first proved against clean main b9af3892 as a matched control:

Renderer Control on clean main Merge result
Approved toolchain — checksum-verified kubectl v1.36.2 / Kustomize v5.8.1, running the validator EKS CI role authorization contract passed. b5b39418…
Local render — kubectl v1.36.1 / Kustomize v5.8.1, via TestValidateAuthorizationAcceptsCommittedPolicy test ok b5b39418…

The matched controls are what make this a measurement rather than a guess: both renderers reproduce every already-approved digest in main, so the single value they disagree with main about is the one this merge actually moved. The full package go test is green on the resolved tree, and both cluster overlays build.

Conservation. The whole-surface fingerprint is the only control that moved: the run reports exactly one problem — this aggregate — plus zero per-identity mismatches and zero missing resources against expectedRenderedHashes, so every individually approved entry is byte-identical to its recorded value. It also reports the same 35 unresolved Flux substitution notes that clean main reports; per the validator's own comment those are diagnostic rather than a control, emitted only alongside an aggregate mismatch to explain a hash that moved.

Two notes for whoever reads this next:

  • The earlier comment's claim that this value could only come from CI does not hold. A local render on the approved toolchain reproduces it, provided the renderer is checksum-verified into a scratch PATH — never by replacing the shared kubectl, which carries prod access.
  • 🔐 Validate EKS Authorization and 🧪 Test both report this digest, but they are one renderer, not two: both execute the same code on the same toolchain. The second renderer above is deliberately a different kubectl.

The systemic cause — every merge into a fingerprint-bearing branch re-conflicting this constant — is tracked on #2768.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

New head 8d607154 resolves the expectedRenderedSurfaceSHA conflict that #3171's merge reintroduced. CI is fully green here and mergeStateStatus is CLEAN; the re-measured digest and its two-renderer evidence are in the comment above. This PR is gated only on a current-head review.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="devantler-tech/platform"
requested_head="8d6071545aad00773a4230b9c4723366140b7ec8"
prior_head="90cf208efc4405cc5bacbe27a1321ca6029b7984"

echo '=== Pull request state ==='
gh pr view 2713 --repo "$repo" \
  --json number,title,baseRefName,headRefName,isDraft,mergeStateStatus,reviewDecision,commits,files,statusCheckRollup \
  --jq '{
    number,title,base:.baseRefName,head:.headRefName,isDraft,mergeStateStatus,reviewDecision,
    commits:[.commits[].oid],
    files:[.files[].path],
    checks:[.statusCheckRollup[]|{name,conclusion,status}]
  }'

echo
echo '=== Repository heads ==='
printf 'HEAD: '; git rev-parse HEAD
printf 'requested: %s\n' "$requested_head"
printf 'origin/main: '; git rev-parse origin/main
printf 'merge-base: '; git merge-base origin/main HEAD
git merge-base --is-ancestor origin/main HEAD && echo 'base-contained: yes' || echo 'base-contained: no'

echo
echo '=== Complete pull request diff ==='
git diff --stat --find-renames origin/main...HEAD
git diff --name-status --find-renames origin/main...HEAD
git diff --find-renames --no-ext-diff origin/main...HEAD

echo
echo '=== Delta since prior reviewed head ==='
if git cat-file -e "${prior_head}^{commit}" 2>/dev/null; then
  git diff --stat --find-renames "${prior_head}...HEAD"
  git diff --find-renames --no-ext-diff "${prior_head}...HEAD"
fi

echo
echo '=== Current head commit ==='
git show --no-ext-diff --format=fuller --stat HEAD
git show --no-ext-diff --format= --find-renames HEAD

echo
echo '=== Current enforcement sources ==='
for path in \
  k8s/bases/infrastructure/cluster-roles/gateway-tenant-edit.yaml \
  k8s/bases/infrastructure/cluster-roles/tenant-edit.yaml \
  k8s/bases/infrastructure/gateway/gateway.yaml \
  k8s/providers/hetzner/infrastructure/patches/add-ascoachingogvaner-dk-listeners.yaml \
  k8s/bases/infrastructure/cluster-policies/best-practices/restrict-tenant-http-route-hostnames.yaml \
  scripts/tests/test-tenant-route-hostname-boundary.sh \
  scripts/validate-eks-ci-role-policy/main.go \
  .github/workflows/ci.yaml \
  docs/TENANTS.md; do
  echo "--- ${path} ---"
  nl -ba "$path" | sed -n '1,720p'
done

echo
echo '=== Gateway API RBAC, listeners, and tenant bindings ==='
rg -n -i -U -C 8 \
  'apiGroups:\s*\[?"?gateway\.networking\.k8s\.io"?\]?[\s\S]{0,900}?verbs:|^kind:\s*Gateway\s*$[\s\S]{0,7000}?(^---\s*$|\z)|allowedRoutes:|kind:\s*(HTTPRoute|GRPCRoute|TCPRoute|TLSRoute|UDPRoute)|aggregate-to-tenant-edit|aggregate-to-edit|kind:\s*RoleBinding[\s\S]{0,1400}?roleRef:[\s\S]{0,350}?name:\s*tenant-edit\b' \
  -g '*.yaml' -g '*.yml' k8s

echo
echo '=== Boundary test, CI execution, and authorization fingerprint logic ==='
rg -n -C 12 \
  'test-tenant-route-hostname-boundary\.sh|shellcheck scripts/tests/test-tenant-route-hostname-boundary\.sh|bash scripts/tests/test-tenant-route-hostname-boundary\.sh|expectedRenderedSurfaceSHA|expectedRenderedHashes|b5b394181a8f2bc325bd427a90990fbc3872fb1f41f3d938f1e28f0aac1075f4|substitutionProblems|unresolved Flux substitution' \
  .github scripts

echo
echo '=== Hetzner infrastructure overlay wiring ==='
fd -t f '^kustomization\.yaml$' k8s/providers/hetzner/infrastructure -x sh -c \
  'echo "--- $1 ---"; nl -ba "$1"' sh {}

echo
echo '=== Conflict markers in pull request files ==='
changed="$(git diff --name-only origin/main...HEAD)"
if [ -n "$changed" ] && printf '%s\n' "$changed" | xargs -r rg -n '^(<<<<<<<|=======|>>>>>>>)'; then
  exit 1
else
  echo 'none'
fi

Length of output: 48472


🏁 Script executed:

#!/bin/bash
set -euo pipefail

show_file() {
  path="$1"
  echo "--- ${path} ---"
  sed -n '=' "$path" | sed 'N;s/\n/\t/'
}

echo '=== Current boundary source ==='
for path in \
  k8s/bases/infrastructure/cluster-roles/gateway-tenant-edit.yaml \
  k8s/bases/infrastructure/cluster-roles/tenant-edit.yaml \
  k8s/bases/infrastructure/gateway/gateway.yaml \
  k8s/providers/hetzner/infrastructure/patches/add-ascoachingogvaner-dk-listeners.yaml \
  scripts/tests/test-tenant-route-hostname-boundary.sh \
  .github/workflows/ci.yaml \
  scripts/validate-eks-ci-role-policy/main.go; do
  show_file "$path"
done

echo
echo '=== All Gateway API resource grants ==='
rg -n -i -U -C 6 \
  'apiGroups:\s*\[?"?gateway\.networking\.k8s\.io"?\]?[\s\S]{0,900}?verbs:' \
  -g '*.yaml' -g '*.yml' k8s

echo
echo '=== Listener restrictions and Hetzner overlay wiring ==='
rg -n -i -U -C 8 \
  '^kind:\s*Gateway\s*$[\s\S]{0,7000}?(^---\s*$|\z)|allowedRoutes:|^\s*kinds:|kind:\s*(HTTPRoute|GRPCRoute|TCPRoute|TLSRoute|UDPRoute)' \
  -g '*.yaml' -g '*.yml' k8s/bases/infrastructure/gateway \
  k8s/providers/hetzner/infrastructure
fd -t f '^kustomization\.yaml$' k8s/providers/hetzner/infrastructure -x sh -c \
  'echo "--- $1 ---"; sed -n "=" "$1" | sed "N;s/\n/\t/"' sh {}

echo
echo '=== Boundary test assertions and CI call sites ==='
rg -n -C 10 \
  'role_name|granted|expected_grant|kubectl kustomize|Gateway/platform|listener_count|unpinned|allowedRoutes\.kinds|test-tenant-route-hostname-boundary\.sh' \
  scripts/tests/test-tenant-route-hostname-boundary.sh .github/workflows/ci.yaml

echo
echo '=== Authorization fingerprint references ==='
rg -n -C 8 \
  'expectedRenderedSurfaceSHA|b5b394181a8f2bc325bd427a90990fbc3872fb1f41f3d938f1e28f0aac1075f4|expectedRenderedHashes|substitutionProblems' \
  scripts/validate-eks-ci-role-policy/main.go

Length of output: 66985


@devantler No findings at 8d6071545aad00773a4230b9c4723366140b7ec8.

I verified the complete route-kind boundary:

  • gateway-tenant-edit grants exactly httproutes and referencegrants.
  • The role has only the tenant-specific aggregation label.
  • The two base listeners and the two Hetzner-added listeners pin allowedRoutes.kinds to HTTPRoute.
  • The boundary test renders the Hetzner infrastructure overlay. It rejects an RBAC resource-set widening and any listener that permits a route kind other than HTTPRoute.
  • CI runs ShellCheck and the boundary test for Kubernetes changes.
  • expectedRenderedSurfaceSHA is updated to b5b394181a8f2bc325bd427a90990fbc3872fb1f41f3d938f1e28f0aac1075f4.
  • The validator keeps unresolved Flux substitutions as diagnostics. The aggregate fingerprint remains the enforcement control.

GitHub reports CLEAN. The required checks succeeded, including 🔐 Validate EKS Authorization and 🧪 Validate Manifests.

You are interacting with an AI system.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Merge record @ 8d6071545aad00773a4230b9c4723366140b7ec8

Hygiene pentad clear at this head: status rollup SUCCESS with zero non-green contexts, 2 review
threads both resolved, no non-thread review findings (no CodeRabbit review body at this head, and no
Codex comment-form ## Review finding sections), mergeStateStatus: CLEAN.

Green review is CodeRabbit's current-head verdict reply: "No findings at
8d6071545aad00773a4230b9c4723366140b7ec8"
— the full 40-character sha, exactly equal to
headRefOid — followed by eight diff-specific findings-free observations (the gateway-tenant-edit
grant set, the tenant-only aggregation label, all four listeners pinning allowedRoutes.kinds to
HTTPRoute, and the re-measured expectedRenderedSurfaceSHA).

One caveat, recorded rather than glossed: this head's CodeRabbit commit status still reads
Review skipped: automatic reviews are disabled — the never-reviewed default — rather than
Review completed, so the status corroborator is absent even though the verdict artifact is present
and unambiguous. I am treating the verdict as the green on substance: its content is specific to this
diff and could not have been produced without a review, which is the only thing the corroborator
exists to establish. Noting it because the same shape on a weaker artifact should not be read this
way.

Behaviour observed. The boundary test scripts/tests/test-tenant-route-hostname-boundary.sh
renders the Hetzner overlay and rejects both an RBAC resource-set widening and any listener admitting
a route kind other than HTTPRoute — so the fix is exercised by a test that discriminates the two
states, and CI runs it on Kubernetes changes. The authorization fingerprint was independently
re-measured by two different renderers, each first proved against clean main as a matched control.

Enqueuing head-pinned; main is merge-queue gated so the queue selects the strategy.

@devantler
devantler added this pull request to the merge queue Aug 17, 2026
Merged via the queue into main with commit 9a84e92 Aug 17, 2026
33 checks passed
@devantler
devantler deleted the codex/propose-fix-for-tenant-hostname-vulnerability branch August 17, 2026 13:18
@github-project-automation github-project-automation Bot moved this from 🫴 Ready to ✅ Done in 🌊 Project Board Aug 17, 2026
devantler added a commit that referenced this pull request Aug 17, 2026
Both parents had independently re-approved expectedRenderedSurfaceSHA -- this
branch for the #3181 Longhorn orphan-reclamation chart value, main for the #2713
gateway-tenant-edit route-kind narrowing -- so neither value describes the merge
result and the conflict could not be resolved by picking a side. Both parents'
measurement records are retained; the constant carries clean main's digest as an
explicitly-labelled UNMEASURED placeholder.

The merge result is known not to equal it: the Longhorn HelmRelease is a selected
surface document whose content moved. Neither renderer could measure it during
this run -- the render accumulates a pinned remote resource and
raw.githubusercontent.com returned HTTP 429 on both attempts, an infrastructure
failure rather than a measurement. The required job is expected to reject the
placeholder and report the actual digest, which the comment directs to be
recorded before promotion.
devantler added a commit that referenced this pull request Aug 17, 2026
Resolves the conflict in the authorization-fingerprint ledger.

Both parents appended to the same ledger region and each described only its own
delta -- this branch the Dex GitHub-connector narrowing, main the #2713
gateway-tenant-edit route-kind narrowing plus the #2725 Umami provisioning
grant. Neither parent's value describes the merge result, so the conflict is not
resolvable by picking a side.

Both prose records are retained, because both deltas are present in the merged
surface and neither cancels the other. Only one const survives, and it is
deliberately left at main's approved value rather than either parent's:

  NOT RE-APPROVED. The merge-result fingerprint has not been measured here.

A local render could not produce an authoritative value -- this host has kubectl
v1.36.1 and the ledger's approved renderer is v1.36.2, so the validator refuses
fail-closed rather than emitting an unapproved measurement. The required
`Validate EKS Authorization` job runs the approved toolchain and is the
authoritative renderer; its reported value is what must be approved into the
const, with the usual second independent render agreeing before this merges.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

1 participant