fix(security): close alternate tenant route-kind hostname bypass - #2713
Conversation
|
|
@cursor review |
Bugbot couldn't run - usage limit reachedBugbot 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) |
|
|
@cursor review |
Bugbot couldn't run - usage limit reachedBugbot 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
left a comment
There was a problem hiding this comment.
🤖 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.
✅
|
…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.
@coderabbitai review New head |
|
|
|
Warning Review limit reached
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 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 configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
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. Comment |
@codex review CodeRabbit is rate-limited at this head ( Focus: this closes a P1 where the tenant hostname policy matches |
…-tenant-hostname-vulnerability # Conflicts: # .github/workflows/ci.yaml
There was a problem hiding this comment.
💡 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.
State at Remaining blocker: the authorization-surface re-approval. 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:
|
… 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.
Authorization surface re-approved in The delta, measured rather than asserted. Rendered all five authorization overlays from
71 RBAC documents on each side, identity sets identical, and exactly ONE differing in content — That one change is a strict narrowing: Both renderer controls pass, so this is verified rather than copied. The existing narrative in Ablated it too: reverting to the old constant fails reporting exactly the new fingerprint, and restoring it passes. |
@coderabbitai review Current head |
|
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 scriptsLength 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 testsLength of output: 187514
I verified these controls:
The reported CI checks succeeded, including GitHub currently reports 🧠 Learnings usedYou 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.
@coderabbitai review |
|
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'
fiLength 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-hostnamesLength of output: 202746
I verified these controls:
GitHub reports the pull request as 🧠 Learnings usedYou are interacting with an AI system. |
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.
So the merge needs a fresh measurement against main Why I did not just write a value. The recorded protocol requires two independent renderers agreeing. I obtained the pinned renderer (kubectl 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 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 |
…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>
This was
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 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 |
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.
Completed the digest follow-up the Diagnosis. Not main. platform#3171, also at an up-to-date head, passes both workflows, so Two independent renderers agree, satisfying the protocol:
Note the two CI jobs are one renderer, not two — both run Conservation. The aggregate is the only thing that moved: both renderers report zero per-identity mismatches against 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 |
CI is now fully green at this head ( @coderabbitai review |
|
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'
fiLength 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' k8sLength of output: 245352
I verified the complete route-kind boundary:
GitHub reports the pull request as 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>
Un-conflicted at Measured value: Two independent renderers agree, and each was first proved against clean
The matched controls are what make this a measurement rather than a guess: both renderers reproduce every already-approved digest in 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 Two notes for whoever reads this next:
The systemic cause — every merge into a fingerprint-bearing branch re-conflicting this constant — is tracked on #2768. |
New head @coderabbitai review |
|
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'
fiLength 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.goLength of output: 66985
I verified the complete route-kind boundary:
GitHub reports You are interacting with an AI system. |
Merge record @
|
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.
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.

Motivation
Platform #2740 now confines tenant-authored
HTTPRoutehostnames 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 acceptsGRPCRouteby 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
gateway-tenant-edittohttproutesandreferencegrants; ordinary built-ineditremains excluded, as established by fix(security): restrict tenant gateway hostnames #2740.allowedRoutes.kinds: [HTTPRoute], covering routes created by principals outside the tenant role as well.restrict-tenant-route-hostnamespolicy 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.shbash scripts/tests/test-tenant-route-hostname-boundary.sh— 4 rendered listeners pinned; tenant RBAC contains only HTTPRoute plus ReferenceGrantkyverno test ./tests/restrict-tenant-route-hostnames— 14 passed, 0 failedgo test ./scripts/validate-eks-ci-role-policy79250b2f64ab9d3798a659d13714b1a07dd72f5baa5fb465beecf30bfd9ad172ksail workload validateksail --config ksail.prod.yaml workload validateUser-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
mainat6d926e42after #2740 landed.