Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/TENANTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,11 @@ here** — **hostnames**, **`gethomepage.dev/*` dashboard annotations**, routes,
alongside the canonical Kyverno policy fixtures, so neither half can widen silently.
- The platform's `homepage` app discovers `gethomepage.dev/*` annotations on the tenant's
HTTPRoute cluster-wide, so the tenant authors them in its own artifact — they are tenant
self-presentation, not platform config.
self-presentation, not platform config. Homepage links the card to the route's first
hostname plus its first rule's first path, so a route whose first rule matches a sub-path
(an `/api` or `/mcp` bypass, say) must set `gethomepage.dev/href`; admission denies it
otherwise
([`require-homepage-href-for-non-root-routes.yaml`](../k8s/bases/infrastructure/cluster-policies/best-practices/require-homepage-href-for-non-root-routes.yaml)).

Use an existing tenant's `deploy/httproute.yaml` (e.g. `ascoachingogvaner`, which owns its
hostnames *and* its dashboard annotations) as the reference.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Keeps every Homepage card pointing at the service's front page.
#
# Why: unless a route sets gethomepage.dev/href, Homepage builds the card link
# itself, from the route's first hostname plus the path of its FIRST rule's
# FIRST match. Rule order is routing-neutral in the Gateway API, where the most
# specific match wins whatever its position, so nothing in a route reads as
# wrong when a narrow rule is listed first. Homepage is the only consumer of
# that order, and it fails silently: the card still renders, healthy, and links
# somewhere else.
#
# That is how the Coroot card broke. platform#5881 put an `Exact /mcp` rule
# (the agents' MCP endpoint, which bypasses SSO) ahead of the catch-all, so the
# card linked to /mcp, and a browser without a bearer token got Coroot's 401
# "unauthorized" instead of the UI. A RegularExpression first path or a route
# without hostnames is worse: Homepage builds no link at all.
#
# So a route that opts into the dashboard may leave the link to Homepage only
# when the derived link is the site root; any other route must name its link
# explicitly with gethomepage.dev/href. The API server defaults an omitted rule
# list, match list or path to `PathPrefix /`, so an omitted one reads as the
# root here as well.
#
# Scope: HTTPRoutes in every namespace, platform and tenant alike, with
# background scanning on so existing drift surfaces in the PolicyReports.
# CREATE and UPDATE only, never DELETE, so a route can always be removed
# (#4732). Ingress is deliberately not matched: neither this repository nor the
# cluster serves one, and the tenant role grants only HTTPRoutes. Reading an
# Ingress's link needs its `http` field, and `ksail workload validate` skips
# any policy whose expressions name `http` (it cannot tell the field from the
# CEL http library), so covering Ingress would cost the offline evaluation that
# lets a pull request fail on a violating route before admission does.
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: require-homepage-href-for-non-root-routes
annotations:
policies.kyverno.io/title: Require Homepage Href For Non-Root Routes
policies.kyverno.io/category: Best Practices
policies.kyverno.io/severity: low
policies.kyverno.io/subject: HTTPRoute
policies.kyverno.io/minversion: 1.17.0
policies.kyverno.io/description: >-
Requires every HTTPRoute that opts into Homepage discovery
(gethomepage.dev/enabled: "true") to set gethomepage.dev/href unless the
link Homepage would derive from it is the site root, so reordering a
route's rules cannot silently move its dashboard card to a sub-path.
spec:
failurePolicy: Fail
validationActions:
- Deny
evaluation:
background:
enabled: true
matchConstraints:
resourceRules:
- apiGroups: ["gateway.networking.k8s.io"]
apiVersions: ["*"]
operations: ["CREATE", "UPDATE"]
resources: ["httproutes"]
matchConditions:
- name: opted-into-homepage
expression: >-
object.metadata.?annotations.orValue({})[?'gethomepage.dev/enabled'].orValue('') == 'true'
variables:
# Homepage treats an empty href as absent, so this does too.
- name: href
expression: >-
object.metadata.?annotations.orValue({})[?'gethomepage.dev/href'].orValue('')
# https://<hostnames[0]><rules[0].matches[0].path.value>, and no link at all
# without a hostname or for a RegularExpression path. A wildcard first
# hostname (`*.example.com`) yields a link no browser can open. The scheme
# comes from the Gateway named by parentRefs[0], and Homepage throws reading
# that entry when parentRefs is omitted or empty. The API server defaults
# only an OMITTED rule list or match list; an explicitly empty one stays
# empty, and Homepage then fails reading `[0]` of it, so omitted reads as
# the root and empty does not.
- name: linksToRoot
expression: >-
size(object.spec.?parentRefs.orValue([])) > 0 &&
size(object.spec.?hostnames.orValue([])) > 0 &&
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
!object.spec.hostnames[0].startsWith('*') &&
(
!object.spec.?rules.hasValue() ||
(
size(object.spec.rules) > 0 &&
(
!object.spec.rules[0].?matches.hasValue() ||
(
size(object.spec.rules[0].matches) > 0 &&
object.spec.rules[0].matches[0].?path.?value.orValue('/') == '/' &&
object.spec.rules[0].matches[0].?path.?type.orValue('PathPrefix') != 'RegularExpression'
)
)
)
)
validations:
- expression: >-
variables.href != '' || variables.linksToRoot
message: >-
An HTTPRoute with gethomepage.dev/enabled="true" must set
gethomepage.dev/href unless its first hostname and its first rule's
first path already form the site root. Homepage builds the card link
from exactly those two fields, so a route whose first rule matches a
sub-path (such as an /mcp or /api bypass) sends the card there, and a
missing or wildcard hostname, a missing parentRef, a RegularExpression
path, or an explicitly empty rule or match list leaves the card with no
usable link.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ resources:
- best-practices/disable-default-sa-automount.yaml
- best-practices/disallow-latest-tag.yaml
- best-practices/propagate-reloader-to-flagger-primary.yaml
- best-practices/require-homepage-href-for-non-root-routes.yaml
- best-practices/restrict-github-team-external-identity.yaml
- best-practices/restrict-github-team-management.yaml
- best-practices/restrict-arc-openbao-listener.yaml
Expand Down
7 changes: 7 additions & 0 deletions k8s/bases/infrastructure/controllers/coroot/http-route.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ metadata:
# uses for the Cilium and OpenBao routes. Homepage fetches icons client-side,
# so the cluster egress NetworkPolicy doesn't block it.
gethomepage.dev/icon: https://raw.githubusercontent.com/coroot/coroot/main/front/public/icon.svg
# Card link. Without it Homepage builds the link from the hostname plus the
# FIRST rule's first path match, and the first rule below is the /mcp
# bypass, so the card opened Coroot's MCP endpoint, which answers a browser
# without a bearer token with 401 "unauthorized" (#5881 added that rule).
# Pin the UI root so rule order cannot move the link again; the
# require-homepage-href-for-non-root-routes policy enforces this.
gethomepage.dev/href: https://observability.${domain}
spec:
parentRefs:
- name: platform
Expand Down
1 change: 1 addition & 0 deletions tests/policy-failure-actions.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"audit-privileged-rbac": "Enforce",
"disallow-latest-tag": "Audit",
"restrict-homepage-service-groups": "Enforce",
"require-homepage-href-for-non-root-routes": "Enforce",
"restrict-tenant-issuer-refs": "Enforce",
"restrict-tenant-network-policies/cnp-ingress-sources-must-be-constrained": "Enforce",
"restrict-tenant-network-policies/cnp-egress-destinations-must-be-constrained": "Enforce",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
apiVersion: cli.kyverno.io/v1alpha1
kind: Test
metadata:
name: require-homepage-href-for-non-root-routes
policies:
- >-
../../k8s/bases/infrastructure/cluster-policies/best-practices/require-homepage-href-for-non-root-routes.yaml
resources:
- resources.yaml
# Exercise the shipped Coroot route, whose /mcp rule comes first: it passes
# only while it keeps its explicit href.
- ../../k8s/bases/infrastructure/controllers/coroot/http-route.yaml
results:
# --- derived link is the site root, or an explicit href is set ---
- policy: require-homepage-href-for-non-root-routes
resources:
- whoami/root-prefix-first
- whoami/exact-root-first
- whoami/omitted-matches
- whoami/omitted-rules
- whoami/omitted-path
- observability/subpath-first-with-href
- whoami/wildcard-hostname-with-href
- whoami/empty-matches-with-href
- whoami/no-parent-refs-with-href
- observability/coroot
kind: HTTPRoute
result: pass
# --- a sub-path, a regex, a missing or wildcard hostname, no parentRef, or an empty list, with no href ---
- policy: require-homepage-href-for-non-root-routes
resources:
- observability/subpath-first-without-href
- whoami/prefix-subpath-first
- whoami/root-is-second-match
- whoami/regex-root-first
- whoami/no-hostnames
- whoami/empty-href
- whoami/wildcard-hostname
- whoami/empty-matches
- whoami/empty-rules
- whoami/no-parent-refs
- whoami/empty-parent-refs
kind: HTTPRoute
result: fail
# --- the precondition's negative control: routes that never opted in ---
- policy: require-homepage-href-for-non-root-routes
resources:
- whoami/not-opted-in
- whoami/discovery-disabled
kind: HTTPRoute
result: skip
Loading
Loading