Skip to content

fix(cozystack-basics): normalize tenant hostname VAP comparisons with lowerAscii - #3208

Merged
myasnikovdaniil merged 1 commit into
mainfrom
fix/hostname-vap-case-insensitive
Aug 12, 2026
Merged

fix(cozystack-basics): normalize tenant hostname VAP comparisons with lowerAscii#3208
myasnikovdaniil merged 1 commit into
mainfrom
fix/hostname-vap-case-insensitive

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

What this PR does

The Ingress, Gateway, and Route hostname ValidatingAdmissionPolicy objects compare tenant hostnames against the tenant apex with case-sensitive CEL. The resource-side host is always lowercase — the apiserver's hostname validation permits nothing else — but both apex operands can carry uppercase: the namespace.cozystack.io/host label value, which Kubernetes permits to carry uppercase and which the tenant chart writes verbatim from tenant.spec.host, and the templated _cluster.root-host, a free-form platform value that nothing validates for case.

A mixed-case apex label wrongly denies a tenant its own legitimate lowercase in-apex hostname. An uppercase _cluster.root-host is the dangerous direction: the Ingress outside-root exclusion compares a lowercase host against the uppercase literal, never matches, so the exclusion stops excluding and the policy admits any non-wildcard host — every non-wildcard platform-apex name a tenant asks for, including a hostname belonging to another tenant and the root apex itself. Only the anti-wildcard gate still holds.

This normalizes both operands of every hostname comparison with .lowerAscii() (host, apex label, and the templated root-host literal) across all three policies, so the match follows DNS case semantics. There is no behavior change in the default all-lowercase configuration.

The Gateway policy's fail-open on an absent or empty host label is deliberately left as it was: that is the platform's one documented fail-open exception in docs/security/threat-model.md, so changing the posture is a security-model decision rather than a side effect of normalizing case. Its current shape is pinned by a test.

Test coverage:

  • helm-unittest pins each comparison as a whole operand pair — equality and endsWith, across the rule-host and tls-host branches — so dropping the normalization from any single operand is caught. A pattern matching a lone .lowerAscii() would not: the apex appears four times in the Ingress expression, so stripping one operand still leaves three matches.
  • A rendering case with _cluster.root-host: Example.ORG pins the outside-root escape shut; it is red pre-fix.
  • e2e probes set a mixed-case apex on an isolated throwaway namespace and assert both halves of the match: a lowercase in-apex HTTPRoute and Ingress are still admitted (red pre-fix, green post-fix), while a foreign apex and a hostname ending with the apex without a dot boundary are still denied. Vacuous-pass guards re-read the persisted label and assert the probe apex really is mixed-case.

Normalizing the apex at the write site — the tenant namespace template and _cluster.root-host — is tracked separately in #3328. It would not remove the need for this change: the host label is settable out-of-band by a trusted caller, so these policies must not assume the apex arrives normalized.

Closes #3205.

Screenshots

N/A (no UI changes).

Release note

fix(cozystack-basics): tenant hostname admission policies now compare against the tenant apex and the platform root apex case-insensitively. This closes a bypass where an uppercase `_cluster.root-host` made the Ingress policy's outside-root exclusion stop matching, so the policy admitted any non-wildcard host — including a hostname under the platform apex belonging to another tenant, and the root apex itself.

Summary by CodeRabbit

  • Bug Fixes

    • Hostname validation now consistently matches case-insensitively across Gateway, Ingress, HTTPRoute, and TLSRoute.
    • Mixed-case tenant and apex hostnames are accepted when they meet the intended domain boundaries.
    • Foreign domains and prefix-adjacent hostnames remain correctly rejected; validation behaves as documented when the tenant host is empty.
  • Tests

    • Expanded unit and e2e coverage for case-insensitive hostname rendering and boundary rejections across the supported resources.

@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug labels Jul 5, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses an issue where ValidatingAdmissionPolicy (VAP) objects incorrectly rejected legitimate tenant hostnames when the associated namespace apex label contained uppercase characters. By normalizing both the resource-side host and the apex operand using .lowerAscii(), the platform now correctly handles DNS case semantics regardless of the casing provided in the namespace labels. This change ensures robust validation without impacting existing all-lowercase configurations.

Highlights

  • Case-Insensitive Hostname Validation: Updated ValidatingAdmissionPolicy (VAP) objects for Ingress, Gateway, and Route to use .lowerAscii() when comparing tenant hostnames against the tenant apex, ensuring compatibility with mixed-case labels.
  • Defense-in-Depth: Implemented case normalization on both operands across all hostname comparison logic to prevent accidental rejection of valid hostnames when the apex label contains uppercase characters.
  • Enhanced Test Coverage: Added new e2e tests in gateway.bats to verify that mixed-case apex labels do not block legitimate lowercase hostnames, and updated helm-unittest assertions to enforce the use of .lowerAscii() in policies.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@dosubot dosubot Bot added the area/networking Issues or PRs related to networking (ingress, gateway, vpn, metallb, cilium, kube-ovn) label Jul 5, 2026
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1636ab58-1c88-4400-9772-5cb52f68e143

📥 Commits

Reviewing files that changed from the base of the PR and between e94481c and bccbdb5.

📒 Files selected for processing (7)
  • hack/e2e-chainsaw/gateway/chainsaw-test.yaml
  • packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml
  • packages/system/cozystack-basics/templates/ingress-hostname-policy.yaml
  • packages/system/cozystack-basics/templates/route-hostname-policy.yaml
  • packages/system/cozystack-basics/tests/gateway-hostname-policy_test.yaml
  • packages/system/cozystack-basics/tests/ingress-hostname-policy_test.yaml
  • packages/system/cozystack-basics/tests/route-hostname-policy_test.yaml
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml
  • packages/system/cozystack-basics/tests/route-hostname-policy_test.yaml
  • packages/system/cozystack-basics/templates/ingress-hostname-policy.yaml
  • packages/system/cozystack-basics/templates/route-hostname-policy.yaml
  • packages/system/cozystack-basics/tests/ingress-hostname-policy_test.yaml
  • hack/e2e-chainsaw/gateway/chainsaw-test.yaml

📝 Walkthrough

Walkthrough

Hostname admission policies for Gateway, Ingress, HTTPRoute, and TLSRoute now normalize hostname comparisons with lowerAscii(). Unit tests and Chainsaw end-to-end scenarios cover mixed-case tenant apex labels, accepted in-apex hosts, and rejected foreign or prefix-adjacent hosts.

Changes

Case-insensitive hostname policy hardening

Layer / File(s) Summary
Gateway hostname policy
packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml, packages/system/cozystack-basics/tests/gateway-hostname-policy_test.yaml
Gateway listener and tenant hostname comparisons now lowercase both operands, with updated expression and fail-open assertions.
Route hostname policy
packages/system/cozystack-basics/templates/route-hostname-policy.yaml, packages/system/cozystack-basics/tests/route-hostname-policy_test.yaml
Shared HTTPRoute and TLSRoute validation lowercases route hostnames and namespace host labels before equality and suffix checks.
Ingress hostname policy
packages/system/cozystack-basics/templates/ingress-hostname-policy.yaml, packages/system/cozystack-basics/tests/ingress-hostname-policy_test.yaml
Ingress exact, suffix, and wildcard validation lowercases rule hosts, TLS hosts, namespace labels, and root-host operands.
Mixed-case apex end-to-end coverage
hack/e2e-chainsaw/gateway/chainsaw-test.yaml
Adds route and ingress scenarios for accepted in-apex hosts and rejected foreign or prefix-adjacent hosts under mixed-case tenant apex labels.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: area/testing, area/uncategorized

Suggested reviewers: androndo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: lowercasing tenant hostname VAP comparisons for case-insensitive matching.
Linked Issues check ✅ Passed The PR updates ingress, gateway, and route policies plus matching unit/e2e coverage for case-insensitive hostname comparison as requested.
Out of Scope Changes check ✅ Passed The changes stay focused on hostname policy hardening and its tests, with no obvious unrelated edits.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/hostname-vap-case-insensitive

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.

@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/hostname-vap-case-insensitive branch from dc1007c to f83e751 Compare July 5, 2026 14:11

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces case-insensitive matching for tenant apex domains in ValidatingAdmissionPolicies (VAPs) across Gateway, Ingress, and Route hostname policies. By utilizing the CEL .lowerAscii() function, both the resource hostnames and the namespace-derived tenant host labels are normalized to lowercase, preventing false denials when mixed-case labels are used. The changes are accompanied by comprehensive updates to unit tests and new end-to-end BATS tests to ensure robust validation. I have no further feedback to provide as the implementation is complete and well-tested.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@lexfrei Aleksei Sviridkin (lexfrei) removed the area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review label Jul 5, 2026
IvanHunters
IvanHunters previously approved these changes Jul 7, 2026

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict

LGTM

The change makes every hostname comparison in all three tenant hostname VAPs case-fold symmetrically, addresses a real false-deny (and a config-dependent isolation bypass), and is a no-op in the default all-lowercase configuration.

Notes (non-blocking)

Verification performed, no defects found:

  • CEL semantics verified. lowerAscii() is provided by the Kubernetes CEL strings extension (available since 1.25) and folds ASCII A-Z only, leaving non-ASCII bytes unchanged. That is the correct semantics for DNS hostname case-insensitivity, which is defined over ASCII. Confirmed against the Kubernetes CEL reference, not the PR body.
  • Symmetry verified in rendered output. helm template ... --set _cluster.root-host=example.org on templates/ingress-hostname-policy.yaml shows every operand lowered on both sides across the own-apex, subdomain, outside-root-exclusion, and wildcard-gate branches, for both rules[].host and tls[].hosts[]. Gateway (gateway-hostname-policy.yaml:37-38) and Route (route-hostname-policy.yaml:45, shared by HTTPRoute doc 0 and TLSRoute doc 2) are likewise symmetric.
  • Real bug confirmed. packages/apps/tenant/templates/namespace.yaml:91 writes namespace.cozystack.io/host verbatim from $computedHost (derived from user-supplied .Values.host, no lowercasing), and packages/system/cozystack-basics/templates/tenant-root.yaml:8 writes it from _cluster.root-host. A mixed-case apex therefore reaches the label unchanged, and pre-fix the case-sensitive endsWith wrongly denied a tenant's own lowercase in-apex hostname. The uppercase-root-host outside-root-exclusion bypass described in the PR body also traces cleanly.
  • Tests pass. helm unittest packages/system/cozystack-basics → 48/48 passing. New assertions pin .lowerAscii() on each operand/branch, guarding against silent regression.

Claim mismatches

None. All PR-body claims verified [OK].

Caveats

  • lowerAscii() does not fold non-ASCII / IDN characters. This is not a gap: spec.rules[].host, spec.tls[].hosts[], Gateway listener hostnames, and Route hostnames are all validated as ASCII (DNS-1123 / punycode) before reaching these policies, so ASCII-only folding is complete for the inputs that can occur.
  • The sibling mutability VAPs cozystack-tenant-host-policy and cozystack-namespace-host-label-policy (gateway-hostname-policy.yaml:56-160) intentionally remain case-sensitive — they detect change to a host value, where folding would be incorrect (it would let Foo.comfoo.com pass as "unchanged"). This is correct and out of scope; no sibling one-sided-normalization pattern remains after this PR.

Phase 5b — Upgrade & Fresh-Install Impact

A. Existing-customer upgrade. No behavior change in the default all-lowercase configuration: for lowercase inputs lowerAscii() is the identity, so re-admission of existing tenant Ingresses/Gateways/Routes yields identical decisions. No values/schema key renamed or removed (files touched: 3 templates + 3 test files + 1 bats file only — no values.yaml / values.schema.json / Chart.yaml, so no make generate artifacts required). No default flip, no RBAC change. Clusters that previously carried a mixed-case apex become less restrictive in the correct direction (a tenant's own hostname is now admitted rather than wrongly denied) and more correct on the isolation-bypass direction (uppercase root-host no longer lets a host under the platform apex slip the exclusion). No resource that was valid pre-fix becomes rejected post-fix.

B. Fresh install. VAPs render and lint clean (helm lint 0 failures; ingress policy still gated behind _cluster.root-host being set). No new CRD/webhook install-time dependency introduced. failurePolicy: Fail and validationActions: [Deny] unchanged; matchConstraints (CREATE/UPDATE on the relevant resources) and matchConditions (tenant-* namespace gate) unchanged.

@myasnikovdaniil

Copy link
Copy Markdown
Contributor

Thanks for the thorough fix — the three template changes and the helm-unittest coverage are solid (both operands + the templated root-host literal normalized, fail-closed guard preserved, no behavior change in the default all-lowercase config).

One thing blocks it though: this branch needs a rebase onto current main. The e2e additions land in hack/e2e-apps/gateway.bats, but that whole BATS app suite was removed by #2826 and replaced with hack/e2e-chainsaw/ — CI now runs chainsaw test hack/e2e-chainsaw/ and no longer executes hack/e2e-apps/*.bats. This branch's merge-base predates that migration, so merging as-is would resurrect gateway.bats and add two tests that nothing runs (the e2e portion of the fix would be silently dead).

Suggested path:

  1. Rebase onto current main.
  2. Drop the hack/e2e-apps/gateway.bats hunk (the file is gone).
  3. Re-author both mixed-case probes as Chainsaw tests under hack/e2e-chainsaw/gateway/ — the suite already has an allow-probe step that reads the apex from the namespace label and an ingress-external-allow.yaml to build on; a script step with check: ($error == null): true mirrors the existing pattern.

The template + unittest changes are unaffected by the migration and look good to me as-is. While porting, worth a quick sanity-check that the mixed-case namespace CREATE (host label) is still admitted under the identity Chainsaw script steps run as.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hack/e2e-chainsaw/gateway/chainsaw-test.yaml`:
- Around line 209-241: Move the cleanup function and its trap registration
before the corresponding RBAC kubectl apply block, so partial Role or
RoleBinding creation is cleaned up if apply fails. Apply this ordering
consistently to the RBAC setup around vap-probe-tenant-create and the analogous
blocks at the other referenced locations, preserving the existing cleanup
commands and behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 631964d3-5b7c-41dd-8ded-5e8329aa621f

📥 Commits

Reviewing files that changed from the base of the PR and between f83e751 and e94481c.

📒 Files selected for processing (1)
  • hack/e2e-chainsaw/gateway/chainsaw-test.yaml

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hack/e2e-chainsaw/gateway/chainsaw-test.yaml`:
- Around line 209-241: Move the cleanup function and its trap registration
before the corresponding RBAC kubectl apply block, so partial Role or
RoleBinding creation is cleaned up if apply fails. Apply this ordering
consistently to the RBAC setup around vap-probe-tenant-create and the analogous
blocks at the other referenced locations, preserving the existing cleanup
commands and behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 631964d3-5b7c-41dd-8ded-5e8329aa621f

📥 Commits

Reviewing files that changed from the base of the PR and between f83e751 and e94481c.

📒 Files selected for processing (1)
  • hack/e2e-chainsaw/gateway/chainsaw-test.yaml
🛑 Comments failed to post (1)
hack/e2e-chainsaw/gateway/chainsaw-test.yaml (1)

209-241: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Install cleanup traps before creating temporary RBAC grants.

Each multi-resource kubectl apply runs before its EXIT trap is registered. Because apply is non-transactional, an error or client timeout can leave a RoleBinding or ClusterRoleBinding active—most seriously granting the default service account namespace patch or creation privileges.

Move each cleanup definition and trap cleanup EXIT before its corresponding RBAC apply.

Also applies to: 275-306, 323-353

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/e2e-chainsaw/gateway/chainsaw-test.yaml` around lines 209 - 241, Move
the cleanup function and its trap registration before the corresponding RBAC
kubectl apply block, so partial Role or RoleBinding creation is cleaned up if
apply fails. Apply this ordering consistently to the RBAC setup around
vap-probe-tenant-create and the analogous blocks at the other referenced
locations, preserving the existing cleanup commands and behavior.

@lexfrei Aleksei Sviridkin (lexfrei) removed the area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review label Jul 16, 2026

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed the current HEAD hermetically. LGTM with non-blocking notes.

Verified:

  • helm template of default and an uppercase-host corner; helm unittest 55/55; mutation test flips 7 assertions red when the templates revert to the pre-fix shape, so the tests are non-vacuous.
  • The commits since the last review added test hardening (mixed-case allow/deny probes, the gateway fail-open pin, the uppercase-root-host test) rather than changing policy logic.

Non-blocking:

  • PR body omits the Downstream Repositories checklist section; walking the trigger map shows no downstream repo affected, but it should be stated.
  • A VAP only fires on CREATE/UPDATE, so objects already admitted under the old case-sensitive comparison are not retroactively re-validated. Inherent, not fixable here.
  • Follow-up for a separate PR: the same case-sensitivity root cause exists in Go, internal/controller/tenantgateway/reconciler.go and renderers.go dedup apex strings via case-sensitive map keys, so a case-variant duplicate defeats the dedup. A companion strings.ToLower would close it.

@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/hostname-vap-case-insensitive branch from e94481c to bccbdb5 Compare July 27, 2026 19:53
@github-actions github-actions Bot added the area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review label Jul 27, 2026
… lowerAscii

The Ingress, Gateway, and Route hostname ValidatingAdmissionPolicies
compare tenant hostnames against the tenant apex with case-sensitive CEL.
The resource-side host is already lowercase — the apiserver's hostname
validation permits nothing else — but the apex operand
is the namespace.cozystack.io/host label value, which Kubernetes permits to
carry uppercase, and the Ingress policy additionally compares against the
templated root host, a free-form platform value that nothing validates for
case.

A mixed-case apex label wrongly denies a tenant its own legitimate
lowercase in-apex hostname. An uppercase root host is the dangerous
direction: the Ingress outside-root exclusion compares a lowercase host
against the uppercase literal, never matches, so the exclusion stops
excluding and the policy degenerates into admitting any non-wildcard host
— every non-wildcard platform-apex name a tenant asks for, including the
root apex itself. Only the anti-wildcard gate still holds.

Wrap both operands of every comparison in .lowerAscii() so the match
follows DNS case semantics — no behavior change for the all-lowercase
default. Each policy reads a label it does not own: the namespace host
label is settable out-of-band by a trusted caller, so none of them may
assume the apex arrives normalized.

The Gateway policy admits any listener hostname when the host label is
absent or empty, unlike its two siblings. That is the platform's one
documented fail-open exception, recorded in the threat model together with
its compensating control, so changing the posture is a security-model
decision rather than a side effect of normalizing case. This leaves the
behavior untouched and pins its current shape in the tests.

Covered by helm-unittest assertions pinning each comparison as a whole
operand pair, so dropping the normalization from any single operand is
caught; a rendering case that pins the uppercase-root-host escape shut; and
e2e probes that set a mixed-case apex on an isolated namespace and assert
both halves of the match — a lowercase in-apex HTTPRoute and Ingress are
still admitted, while a foreign apex and a hostname that ends with the apex
without a dot boundary are still denied.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/hostname-vap-case-insensitive branch from e5b1502 to ba15b38 Compare August 9, 2026 22:15

@myasnikovdaniil myasnikovdaniil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Compiled both old and new expressions in the real apiserver CEL env (k8s.io/apiserver v0.35.0, StrictCostOpt) and ran ~35 hostname shapes through them. lowerAscii() is on both operands of all 15 comparisons, nothing one sided.

Fix is real. With _cluster.root-host: Example.ORG and tenant apex tenant-a.example.org the old expression admits victim-tenant.example.org, dashboard.example.org and bare example.org, all three denied now. With apex label Tenant-A.example.org the old form denies the tenant own app.tenant-a.example.org, now admitted. 21 mutants each dropping one .lowerAscii(), all 21 caught by helm unittest alone.

Cost is a non issue, VAP has no static gate and measured runtime cost goes 83 to 103 for one rule plus one tls host. Capability gate from #3409 intact in all three templates.

One thing before merge, please add the full-e2e label. This PR touches hack/e2e-chainsaw/gateway/** as well as basics, so final_apps is non empty and only the gateway suite runs, while a basics only diff would escalate to all 21 suites through the empty selection net in hack/select-e2e.sh:346-348. Right now the combined PR tests less than the basics change alone would.

set -eu
root=$(kubectl get namespace tenant-root -o jsonpath='{.metadata.labels.namespace\.cozystack\.io/host}')
[ -n "$root" ] || { echo "SETUP FAILURE: tenant-root lacks the namespace.cozystack.io/host label" >&2; exit 1; }
apex="Case-Probe.${root}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Case-Probe. eats 11 chars and label values cap at 63, so a platform root apex over 52 chars fails the namespace CREATE with a label length error instead of the assertion. Not reachable in CI. [ ${#apex} -le 63 ] next to the two vacuous pass guards already there would close it.

name: ingress-case-foreign-probe
spec:
rules:
- host: "dashboard.${root}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

dashboard.${root} is the live platform dashboard hostname. Benign because ingress-nginx resolves host conflicts oldest wins and the probe is deleted before the step fails, but probe-does-not-exist.${root} proves the same thing with no live host overlap.

@myasnikovdaniil
myasnikovdaniil merged commit 516be4a into main Aug 12, 2026
18 checks passed
@myasnikovdaniil
myasnikovdaniil deleted the fix/hostname-vap-case-insensitive branch August 12, 2026 10:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/networking Issues or PRs related to networking (ingress, gateway, vpn, metallb, cilium, kube-ovn) area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Harden tenant hostname ValidatingAdmissionPolicies with case-insensitive comparison (defense-in-depth)

3 participants