-
Notifications
You must be signed in to change notification settings - Fork 4
feat(homepage): declare Infrastructure as Code and enforce declared groups #3173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+416
−3
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
a83478b
feat(homepage): declare Infrastructure as Code and enforce declared g…
devantler 1d75f39
fix(policies): use validate.failureAction and pin the allow-list to t…
devantler 5aa0327
chore: Apply megalinter fixes
devantler f988bfb
fix(tests): write the ConfigMap guards as explicit if-statements
devantler 2198781
Merge remote-tracking branch 'origin/claude/homepage-groups' into cla…
devantler 9d3e72c
test(policies): pin the deny message to the allow-list too
devantler File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
...ases/infrastructure/cluster-policies/best-practices/restrict-homepage-service-groups.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| # Keeps every Homepage-discovered service inside a group the dashboard actually | ||
| # declares. | ||
| # | ||
| # Why: Homepage's group ORDER, icon and position come from the `layout` block in | ||
| # k8s/bases/apps/homepage/config-map.yaml, but the group NAME is chosen by an | ||
| # annotation on a route — often in another repository entirely. Nothing bound the | ||
| # two surfaces, so a route could invent a group, and Homepage would silently | ||
| # accept it: an undeclared group still renders, but falls to the BOTTOM of the | ||
| # page (after every declared group, bookmarks included) with a generic icon. | ||
| # The failure is cosmetic-looking and completely silent, which is why it went | ||
| # unnoticed in two places at once — `Fun` and `Infrastructure as Code` both | ||
| # rendered below the bookmark rail because neither was declared. | ||
| # | ||
| # Homepage has no native way to restrict discovery to declared groups (its | ||
| # Kubernetes config offers only `gethomepage.dev/instance` filtering), so the | ||
| # binding has to happen at admission. This is that binding: a route may only | ||
| # name a group the dashboard declares. | ||
| # | ||
| # Scope: every namespace, and deliberately NOT limited to tenant-applied | ||
| # resources — the platform's own routes are just as able to invent a group, and | ||
| # `Infrastructure as Code` (a platform route, in this repo) is exactly how this | ||
| # was found. There is no userInfo matching here, so background scanning stays on | ||
| # and existing drift surfaces in the PolicyReports rather than only at admission. | ||
| # | ||
| # SERVICE groups only. The bookmark groups in the same layout block (GitHub, | ||
| # Media, Banking, …) are intentionally absent: Homepage renders services and | ||
| # bookmarks as separate sections, so the config-map keeps the two name sets | ||
| # distinct, and a service claiming a bookmark group's name would paint that | ||
| # heading twice with different contents under each. | ||
| # | ||
| # KEEP IN SYNC with the `layout` block in k8s/bases/apps/homepage/config-map.yaml | ||
| # — adding a group there means adding it here, in the same PR. That is a | ||
| # reviewable, fail-loud edit: a route naming an undeclared group is rejected at | ||
| # admission with the list below, instead of silently sinking to the bottom of the | ||
| # dashboard. The list is duplicated rather than read from the ConfigMap on | ||
| # purpose: a Kyverno context lookup would make every annotated route in the | ||
| # cluster un-appliable if that ConfigMap ever became unreadable, trading a silent | ||
| # cosmetic bug for a reconciliation outage. | ||
| apiVersion: kyverno.io/v1 | ||
| kind: ClusterPolicy | ||
| metadata: | ||
| name: restrict-homepage-service-groups | ||
| annotations: | ||
| policies.kyverno.io/title: Restrict Homepage Service Groups | ||
| policies.kyverno.io/category: Best Practices | ||
| policies.kyverno.io/severity: low | ||
| policies.kyverno.io/subject: HTTPRoute, Ingress | ||
| policies.kyverno.io/minversion: 1.13.0 | ||
| policies.kyverno.io/description: >- | ||
| Requires every route that opts into Homepage discovery | ||
| (gethomepage.dev/enabled: "true") to name a service group declared in the | ||
| dashboard's layout, so a discovered service cannot invent a group and sink | ||
| to the bottom of the page with a generic icon. | ||
| spec: | ||
| # Rule-level `validate.failureAction` below, not the spec-level | ||
| # `validationFailureAction`: Kyverno's own v1 CRD describes that one as | ||
| # "Deprecated, use validationFailureAction under the validate rule instead." | ||
| # The eleven older policies here still use the deprecated form — it is honoured | ||
| # on the deployed v1.18.2 — and migrating them is tracked separately; a new | ||
| # policy should not add to that debt. | ||
| rules: | ||
| - name: declared-service-group | ||
| match: | ||
| any: | ||
| - resources: | ||
| kinds: | ||
| # HTTPRoute is what this platform actually uses. Ingress is | ||
| # Homepage's other discovery source and is included so a future | ||
| # Ingress cannot bypass the guard. | ||
| - HTTPRoute | ||
| - Ingress | ||
| preconditions: | ||
| all: | ||
| # Only routes that opt into the dashboard. Everything else is none of | ||
| # this policy's business. | ||
| - key: "{{ request.object.metadata.annotations.\"gethomepage.dev/enabled\" || 'false' }}" | ||
| operator: Equals | ||
| value: "true" | ||
| validate: | ||
| failureAction: Enforce | ||
| message: >- | ||
| A route with gethomepage.dev/enabled="true" must set | ||
| gethomepage.dev/group to a service group declared in the Homepage | ||
| layout (k8s/bases/apps/homepage/config-map.yaml): Internal Developer | ||
| Platform, Kubernetes, Observability, Security, Diagnostics, Device | ||
| Management, Infrastructure as Code, Cloud, Network, Finance, Analytics, | ||
| Personal Sites, or Customer Sites. An undeclared group still renders, | ||
| but sorts below every declared group with a generic icon. Bookmark | ||
| group names are deliberately not accepted here. | ||
| deny: | ||
| conditions: | ||
| any: | ||
| # The 'MISSING' sentinel is not in the allow-list, so an absent | ||
| # annotation is denied by the same condition as a misspelled one. | ||
| - key: "{{ request.object.metadata.annotations.\"gethomepage.dev/group\" || 'MISSING' }}" | ||
| operator: NotIn | ||
| value: | ||
| - Internal Developer Platform | ||
| - Kubernetes | ||
| - Observability | ||
| - Security | ||
| - Diagnostics | ||
| - Device Management | ||
| - Infrastructure as Code | ||
| - Cloud | ||
| - Network | ||
| - Finance | ||
| - Analytics | ||
| - Personal Sites | ||
| - Customer Sites | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| #!/usr/bin/env bash | ||
| # The Homepage layout and the Kyverno allow-list are two lists that must agree. | ||
| # The policy exists because an undeclared group fails silently; a duplicated | ||
| # allow-list that drifts would fail just as silently, so this pins them together. | ||
| # | ||
| # Service groups are derived STRUCTURALLY — every layout key that is not a | ||
| # bookmark group — rather than by reading the section comment, so moving or | ||
| # rewording that comment cannot quietly change what this test believes. | ||
| set -euo pipefail | ||
|
|
||
| script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| repo_root="$(cd "${script_dir}/../.." && pwd)" | ||
| config_map="${repo_root}/k8s/bases/apps/homepage/config-map.yaml" | ||
| policy="${repo_root}/k8s/bases/infrastructure/cluster-policies/best-practices/restrict-homepage-service-groups.yaml" | ||
|
|
||
| for f in "${config_map}" "${policy}"; do | ||
| [ -r "$f" ] || { | ||
| echo "::error::missing or unreadable: $f" | ||
| exit 1 | ||
| } | ||
| done | ||
|
|
||
| # --- 1. the rule must ENFORCE, via the non-deprecated field ------------------- | ||
| action="$(yq -r '.spec.rules[] | select(.name == "declared-service-group") | .validate.failureAction // "unset"' "${policy}")" | ||
| if [ "${action}" != "Enforce" ]; then | ||
| echo "::error::declared-service-group must set validate.failureAction: Enforce (found: ${action})" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Kyverno's v1 CRD calls the spec-level field deprecated. A new policy using it | ||
| # would still work, but this keeps the modern form from silently regressing. | ||
| if [ "$(yq -r '.spec.validationFailureAction // "unset"' "${policy}")" != "unset" ]; then | ||
| echo "::error::policy uses the deprecated spec.validationFailureAction; use validate.failureAction" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # --- 2. allow-list == the layout's SERVICE groups ---------------------------- | ||
| settings="$(yq -r '.data."settings.yaml"' "${config_map}")" | ||
| bookmarks="$(yq -r '.data."bookmarks.yaml"' "${config_map}")" | ||
| if [ -z "${settings}" ] || [ "${settings}" = "null" ]; then | ||
| echo "::error::settings.yaml missing from the ConfigMap" | ||
| exit 1 | ||
| fi | ||
| if [ -z "${bookmarks}" ] || [ "${bookmarks}" = "null" ]; then | ||
| echo "::error::bookmarks.yaml missing from the ConfigMap" | ||
| exit 1 | ||
| fi | ||
|
|
||
| layout_groups="$(printf '%s\n' "${settings}" | yq -r '.layout | keys | .[]' | sort -u)" | ||
| bookmark_groups="$(printf '%s\n' "${bookmarks}" | yq -r '.[] | keys | .[0]' | sort -u)" | ||
| service_groups="$(comm -23 <(printf '%s\n' "${layout_groups}") <(printf '%s\n' "${bookmark_groups}"))" | ||
|
|
||
| policy_groups="$(yq -r ' | ||
| .spec.rules[] | select(.name == "declared-service-group") | ||
| | .validate.deny.conditions.any[] | .value[]' "${policy}" | sort -u)" | ||
|
|
||
| # Proof-of-life: an empty side would make the comparison pass vacuously. | ||
| for pair in "layout:${layout_groups}" "bookmark:${bookmark_groups}" "service:${service_groups}" "policy:${policy_groups}"; do | ||
| name="${pair%%:*}" | ||
| body="${pair#*:}" | ||
| n="$(printf '%s\n' "${body}" | grep -c . || true)" | ||
| if [ "${n}" -lt 1 ]; then | ||
| echo "::error::extracted zero ${name} groups — the parse broke, so this check would pass vacuously" | ||
| exit 1 | ||
| fi | ||
| done | ||
|
|
||
| if ! diff_out="$(diff <(printf '%s\n' "${service_groups}") <(printf '%s\n' "${policy_groups}"))"; then | ||
| echo "::error::Homepage layout service groups and the Kyverno allow-list disagree." | ||
| echo " '<' = declared in the layout but not allowed by the policy" | ||
| echo " '>' = allowed by the policy but not declared in the layout" | ||
| printf '%s\n' "${diff_out}" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # A bookmark group must never be accepted for a service: Homepage renders the two | ||
| # as separate sections, so a shared name paints one heading twice. | ||
| while IFS= read -r b; do | ||
| [ -n "$b" ] || continue | ||
| if printf '%s\n' "${policy_groups}" | grep -qxF -- "$b"; then | ||
| echo "::error::policy allows bookmark group '${b}' as a service group" | ||
| exit 1 | ||
| fi | ||
| done <<EOF | ||
| ${bookmark_groups} | ||
| EOF | ||
|
|
||
| # The deny message names the valid groups so a rejected apply does not have to go | ||
| # read the layout. That is a THIRD copy of the list, so pin it too — otherwise the | ||
| # admission error goes stale exactly when a group is added, which is when someone | ||
| # is most likely to be reading it. | ||
| message="$(yq -r ' | ||
| .spec.rules[] | select(.name == "declared-service-group") | .validate.message' "${policy}")" | ||
| if [ -z "${message}" ] || [ "${message}" = "null" ]; then | ||
| echo "::error::the rule has no validate.message" | ||
| exit 1 | ||
| fi | ||
| while IFS= read -r g; do | ||
| [ -n "${g}" ] || continue | ||
| if ! printf '%s' "${message}" | tr '\n' ' ' | grep -qF -- "${g}"; then | ||
| echo "::error::deny message does not name the allowed group '${g}'" | ||
| exit 1 | ||
| fi | ||
| done <<EOF | ||
| ${service_groups} | ||
| EOF | ||
|
|
||
| count="$(printf '%s\n' "${service_groups}" | grep -c .)" | ||
| echo "Homepage layout, Kyverno allow-list and deny message agree on ${count} service groups; rule enforces." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| --- | ||
| apiVersion: cli.kyverno.io/v1alpha1 | ||
| kind: Test | ||
| metadata: | ||
| name: restrict-homepage-service-groups | ||
| policies: | ||
| - >- | ||
| ../../k8s/bases/infrastructure/cluster-policies/best-practices/restrict-homepage-service-groups.yaml | ||
| resources: | ||
| - resources.yaml | ||
| results: | ||
| # --- declared service groups pass --- | ||
| - policy: restrict-homepage-service-groups | ||
| rule: declared-service-group | ||
| resources: | ||
| - observability/declared-group | ||
| - crossview/infrastructure-as-code-group | ||
| kind: HTTPRoute | ||
| result: pass | ||
|
|
||
| # --- undeclared, missing, bookmark-named and mis-cased groups are denied --- | ||
| - policy: restrict-homepage-service-groups | ||
| rule: declared-service-group | ||
| resources: | ||
| - doggy-countdown/undeclared-group | ||
| - whoami/missing-group | ||
| - whoami/bookmark-group-name | ||
| - whoami/wrong-case-group | ||
| kind: HTTPRoute | ||
| result: fail | ||
|
|
||
| # --- the precondition's negative control: routes that never opted in --- | ||
| - policy: restrict-homepage-service-groups | ||
| rule: declared-service-group | ||
| resources: | ||
| - whoami/not-opted-in | ||
| - whoami/discovery-disabled | ||
| kind: HTTPRoute | ||
| result: skip | ||
|
|
||
| # --- Ingress is guarded identically --- | ||
| - policy: restrict-homepage-service-groups | ||
| rule: declared-service-group | ||
| resources: | ||
| - whoami/ingress-undeclared-group | ||
| kind: Ingress | ||
| result: fail |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.