Skip to content

fix(security): restrict doggy-countdown route hostname - #2866

Draft
devantler wants to merge 3 commits into
mainfrom
codex/propose-fix-for-tenant-hostname-vulnerability-bgwbhh
Draft

fix(security): restrict doggy-countdown route hostname#2866
devantler wants to merge 3 commits into
mainfrom
codex/propose-fix-for-tenant-hostname-vulnerability-bgwbhh

Conversation

@devantler

Copy link
Copy Markdown
Contributor

Motivation

  • Prevent the remotely-sourced doggy-countdown tenant from claiming arbitrary hostnames on the shared platform Gateway, which combined with external-dns could publish attacker-controlled names in the platform zone.
  • The change enforces a hostname constraint for the tenant identity that Flux currently reconciles from a public OCI artifact so a compromised publishing path cannot create phishing endpoints under trusted platform hostnames.

Description

  • Add a Kyverno ClusterPolicy restrict-doggy-countdown-hostnames that enforces HTTPRoute objects in namespace doggy-countdown to declare exactly one hostname: simba.${domain}, and denies hostname-less, mixed, or other hostnames.
  • Register the new policy in k8s/bases/infrastructure/cluster-policies/kustomization.yaml so it is applied with the platform cluster policies.
  • Add offline Kyverno regression fixtures under tests/restrict-doggy-countdown-hostnames/ including resources.yaml (good and exploit variants) and kyverno-test.yaml to assert the intended hostname is allowed while exploit variants are rejected.

Testing

  • Ran python3 scripts/validate-naming.py which passed and reported naming conventions satisfied.
  • Ran python3 scripts/validate-embedded-json.py which parsed embedded JSON blobs cleanly.
  • Ran the Kyverno CLI test suite (kyverno test ./tests, Kyverno v1.18.2) which executed the new fixtures plus existing ones and reported all tests passed (17 tests).
  • ksail workload validate could not be executed in the environment because KSail is not installed, so KSail-backed static validation was not run.

Codex Task

…propose-fix-for-tenant-hostname-vulnerability-bgwbhh

# Conflicts:
#	k8s/bases/infrastructure/cluster-policies/kustomization.yaml
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Picked this up as a long-idle actionable PR (last push 2026-08-01, ~14 days; no active-work signal on
any surface) and cleared its DIRTY merge state.

Conflict — one file, resolved as a union. cluster-policies/kustomization.yaml: this branch adds
restrict-doggy-countdown-hostnames.yaml, and main has since added
restrict-tenant-issuer-refs.yaml in the adjacent line. Both entries belong, so both are kept in
alphabetical position.

Taking either side alone would have been wrong, and the incoming side is the dangerous one to drop:
it is itself a merged security fix (#2833, "enforce the tenant ClusterIssuer boundary at
admission"
), so resolving in favour of this branch would have silently reverted an admission policy
already live on main.

Verified: both referenced policy files exist, no conflict markers remain, the policy base builds
(1848 lines), and both policies appear in the built output.

No behaviour on this branch changed — the merge only integrates main. CI is re-running at the new
head, and the review gate still applies.

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

⚠️MegaLinter analysis: Success with warnings

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

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

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

✅ [SUCCESS] scripts/refresh-flux-ghcr-auth.sh
✅ [SUCCESS] scripts/report-cilium-rollout-gate-suppression.sh
✅ [SUCCESS] scripts/run-ksail-prod-with-pull-auth.sh
✅ [SUCCESS] scripts/summarize-sarif-findings.sh
✅ [SUCCESS] scripts/tests/test-check-megalinter-version-drift.sh
✅ [SUCCESS] scripts/tests/test-cilium-bandwidth-manager-component.sh
✅ [SUCCESS] scripts/tests/test-cilium-homogeneous-devices-activation.sh
✅ [SUCCESS] scripts/tests/test-cilium-homogeneous-devices-autoscaler-gate.sh
✅ [SUCCESS] scripts/tests/test-cilium-homogeneous-devices-flux-wait.sh
✅ [SUCCESS] scripts/tests/test-cilium-mutual-auth-policy-regressions.sh
✅ [SUCCESS] scripts/tests/test-cilium-mutual-auth-policy.sh
✅ [SUCCESS] scripts/tests/test-cilium-rollout-gate-suppression-signal.sh
✅ [SUCCESS] scripts/tests/test-cnpg-degraded-alert.sh
✅ [SUCCESS] scripts/tests/test-crossplane-sync-exporter.sh
✅ [SUCCESS] scripts/tests/test-dr-rebuild-supersession-guard.sh
✅ [SUCCESS] scripts/tests/test-github-config-role-activation-parity.sh
✅ [SUCCESS] scripts/tests/test-kubescape-gate-frameworks-guard.sh
✅ [SUCCESS] scripts/tests/test-kyverno-admission-vpa.sh
✅ [SUCCESS] scripts/tests/test-megalinter-scan-counts-ignorefile.sh
✅ [SUCCESS] scripts/tests/test-normalize-sarif-paths.sh
✅ [SUCCESS] scripts/tests/test-openbao-oidc-role.sh
✅ [SUCCESS] scripts/tests/test-opencost-usage-scraper.sh
✅ [SUCCESS] scripts/tests/test-refresh-flux-ghcr-auth-safety.sh
✅ [SUCCESS] scripts/tests/test-restrict-tenant-secret-stores.sh
✅ [SUCCESS] scripts/tests/test-setup-ksail.sh
✅ [SUCCESS] scripts/tests/test-shared-publish-workflow-pin-guard.sh
✅ [SUCCESS] scripts/tests/test-summarize-sarif-findings.sh
✅ [SUCCESS] scripts/tests/test-use-prod-stable-api-endpoint.sh
✅ [SUCCESS] scripts/tests/test-validate-image-verifier-liveness.sh
✅ [SUCCESS] scripts/tests/test-verify-published-evidence.sh
✅ [SUCCESS] scripts/update-vendored-operators.sh
✅ [SUCCESS] scripts/use-prod-stable-api-endpoint.sh
✅ [SUCCESS] scripts/validate-alert-coverage.sh
✅ [SUCCESS] scripts/validate-image-verifier-liveness.sh
✅ [SUCCESS] scripts/verify-published-evidence.sh
✅ [SUCCESS] scripts/wait-for-platform-flux-revision.sh
⚠️ REPOSITORY / checkov - 2 errors
2026-08-15 06:25:36,329 [MainThread  ] [ERROR]  YAML error parsing k8s/bases/infrastructure/controllers/kubevirt/kubevirt-operator.yaml: expected a single document in the stream
  in "<unicode string>", line 2, column 1
but found another document
  in "<unicode string>", line 9, column 1
cloudformation scan results:

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

kubernetes scan results:

Passed checks: 2063, Failed checks: 2, Skipped checks: 35

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

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

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

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


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

(Truncated to last 5714 characters out of 621740)
⚠️ COPYPASTE / jscpd - 191 errors
ines, 57 tokens)
   scripts/tests/test-cilium-mutual-auth-policy.sh [118:32 - 127:54]
Clone found (bash)
 - scripts/tests/test-cnpg-degraded-alert.sh [270:33 - 280:2] (11 lines, 56 tokens)
   scripts/tests/test-cnpg-degraded-alert.sh [404:51 - 414:2]
Clone found (bash)
 - scripts/tests/test-crossplane-sync-exporter.sh [186:1 - 237:11] (52 lines, 303 tokens)
   scripts/tests/test-opencost-usage-scraper.sh [33:1 - 84:11]
Clone found (bash)
 - scripts/tests/test-refresh-flux-ghcr-auth-safety.sh [348:5 - 356:51] (9 lines, 76 tokens)
   scripts/tests/test-refresh-flux-ghcr-auth-safety.sh [357:5 - 365:51]
Clone found (python)
 - scripts/tests/test_validate_homepage_bookmarks.py [46:57 - 54:54] (9 lines, 58 tokens)
   scripts/tests/test_validate_homepage_bookmarks.py [100:53 - 109:54]
Clone found (go)
 - scripts/validate-dr-signing/main_test.go [1007:13 - 1015:2] (9 lines, 51 tokens)
   scripts/validate-dr-signing/main_test.go [1075:12 - 1083:2]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [244:50 - 249:24] (6 lines, 103 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [756:31 - 761:24]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [276:14 - 282:4] (7 lines, 115 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [603:43 - 609:4]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [302:13 - 311:7] (10 lines, 158 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [828:18 - 837:7]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [311:1 - 316:8] (6 lines, 93 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [838:1 - 843:8]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [388:5 - 393:2] (6 lines, 82 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [574:8 - 579:9]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [389:1 - 399:23] (11 lines, 220 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [666:60 - 677:3]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [393:15 - 404:2] (12 lines, 185 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [494:17 - 505:2]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [440:15 - 451:12] (12 lines, 264 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [649:1 - 662:3]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [503:2 - 509:19] (7 lines, 89 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [688:7 - 694:19]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [503:2 - 509:33] (7 lines, 103 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1013:58 - 1019:33]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [503:2 - 509:4] (7 lines, 74 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1126:5 - 1132:4]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [596:30 - 601:8] (6 lines, 50 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [623:44 - 628:8]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [710:22 - 716:2] (7 lines, 135 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1212:22 - 1218:2]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [951:33 - 959:11] (9 lines, 118 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [963:130 - 971:11]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [1158:47 - 1163:2] (6 lines, 166 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1238:46 - 1243:2]
Clone found (go)
 - scripts/validate-flux-verify/instance_test.go [109:62 - 126:31] (18 lines, 57 tokens)
   scripts/validate-flux-verify/instance_test.go [161:57 - 178:31]
Clone found (go)
 - scripts/validate-flux-verify/instance_test.go [109:62 - 128:35] (20 lines, 62 tokens)
   scripts/validate-flux-verify/instance_test.go [198:55 - 217:26]
Clone found (python)
 - scripts/validate-naming.py [126:52 - 132:25] (7 lines, 53 tokens)
   scripts/validate-naming.py [171:82 - 177:29]
┌────────┬────────────────┬─────────────┬──────────────┬──────────────┬──────────────────┬───────────────────┐
│ Format │ Files analyzed │ Total lines │ Total tokens │ Clones found │ Duplicated lines │ Duplicated tokens │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ bash   │ 49             │ 15993       │ 60235        │ 21           │ 292 (1.83%)      │ 1803 (2.99%)      │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ go     │ 36             │ 30680       │ 184345       │ 104          │ 860 (2.80%)      │ 7992 (4.34%)      │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ python │ 4              │ 807         │ 5606         │ 2            │ 14 (1.73%)       │ 111 (1.98%)       │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ txt    │ 57             │ 3378        │ 109125       │ 64           │ 2417 (71.55%)    │ 80219 (73.51%)    │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ Total: │ 146            │ 50858       │ 359311       │ 191          │ 3583 (7.05%)     │ 90125 (25.08%)    │
└────────┴────────────────┴─────────────┴──────────────┴──────────────┴──────────────────┴───────────────────┘
Found 191 clones.
HTML report saved to megalinter-reports/copy-paste/jscpd-report.html
ERROR: jscpd found too many duplicates (7.0%) over threshold (0.0%)
time: 869.985ms

(Truncated to last 5714 characters out of 40523)
⚠️ SPELL / lychee - 2 errors
📝 Summary
---------------------
🔍 Total..........249
🔗 Unique.........160
✅ Successful.....218
⏳ Timeouts.........0
🔀 Redirected.......8
👻 Excluded........29
❓ Unknown..........0
🚫 Errors...........2
⛔ Unsupported......2

Errors in docs/oidc-kubectl.md
[ERROR] https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens (at 204:3) | Connection failed. Check network connectivity and firewall settings

Errors in talos/cluster/gc-terminated-pods-sooner.yaml
[ERROR] https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-garbage-collection (at 24:14) | Connection failed. Check network connectivity and firewall settings

Hint: Followed 8 redirects. You might want to consider replacing redirecting URLs with the resolved URLs. Use verbose mode (`-v`/`-vv`) to see redirection details.
⚠️ MARKDOWN / markdownlint - 65 errors
Actual: 468]
AGENTS.md:287:401 error MD013/line-length Line length [Expected: 400; Actual: 532]
AGENTS.md:289:401 error MD013/line-length Line length [Expected: 400; Actual: 523]
AGENTS.md:292:401 error MD013/line-length Line length [Expected: 400; Actual: 613]
AGENTS.md:293:401 error MD013/line-length Line length [Expected: 400; Actual: 714]
AGENTS.md:297:401 error MD013/line-length Line length [Expected: 400; Actual: 502]
AGENTS.md:301:401 error MD013/line-length Line length [Expected: 400; Actual: 441]
AGENTS.md:306:401 error MD013/line-length Line length [Expected: 400; Actual: 427]
AGENTS.md:409:401 error MD013/line-length Line length [Expected: 400; Actual: 1139]
AGENTS.md:411:401 error MD013/line-length Line length [Expected: 400; Actual: 1240]
AGENTS.md:427:401 error MD013/line-length Line length [Expected: 400; Actual: 1137]
AGENTS.md:438:401 error MD013/line-length Line length [Expected: 400; Actual: 790]
AGENTS.md:443:401 error MD013/line-length Line length [Expected: 400; Actual: 515]
CLAUDE.md:1 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "@AGENTS.md"]
docs/dr/alerting.md:226:28 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:22:389 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:23:264 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:27:35 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:27:161 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:27:239 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:114 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "Custody recommendations"]
docs/dr/crypto-custody.md:245 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "Custody recommendations"]
docs/dr/crypto-custody.md:251 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "What to do if it leaks"]
docs/dr/crypto-custody.md:258 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "What to do if it is *lost* (no..."]
docs/dr/restore-drill.md:42 error MD028/no-blanks-blockquote Blank line inside blockquote
docs/dr/runbook.md:23:102 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/runbook.md:23:487 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/runbook.md:34 error MD028/no-blanks-blockquote Blank line inside blockquote
docs/dr/runbook.md:41 error MD028/no-blanks-blockquote Blank line inside blockquote
docs/dr/runbook.md:50 error MD028/no-blanks-blockquote Blank line inside blockquote
docs/dr/runbook.md:489:92 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/spire-server-ha.md:93 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/dr/velero-cnpg.md:11 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/dr/velero-cnpg.md:56:78 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/velero-cnpg.md:56:166 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/velero-cnpg.md:57:78 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/velero-cnpg.md:57:227 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/velero-cnpg.md:58:78 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/velero-cnpg.md:58:166 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/github-management.md:38:401 error MD013/line-length Line length [Expected: 400; Actual: 419]
docs/github-management.md:40:401 error MD013/line-length Line length [Expected: 400; Actual: 522]
docs/node-autoscaling.md:14 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/oidc-kubectl.md:95 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/runtime-security.md:114 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/rwx-storage.md:9 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/unifi-management.md:14 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/unifi-management.md:62 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
README.md:116:401 error MD013/line-length Line length [Expected: 400; Actual: 540]
README.md:237:32 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
README.md:237:36 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]

(Truncated to last 5714 characters out of 7160)
⚠️ REPOSITORY / trivy - 1 error
in cronjob longhorn-stale-node-cleanup (namespace: longhorn-system) uses an image from an untrusted registry.
════════════════════════════════════════
Ensure that all containers use images only from trusted registry domains.

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



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

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

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



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

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

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



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

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

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



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

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

(Truncated to last 5714 characters out of 289532)

✅ Linters with no issues

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

Notices

📣 MegaLinter 9.5.0 is out! Discover the new features and security recommendations in the release announcement. (Skip this info by defining SECURITY_SUGGESTIONS: false)

See detailed reports in MegaLinter artifacts

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

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

CI now runs 33 checks instead of 5, and one fails. Root-caused — the 36 "unresolved Flux
substitution" lines are diagnostic noise, not the failure.

Bringing the branch up to date widened the changed-path set, so 🔐 Validate EKS Authorization runs
for the first time on this PR. It fails, and the log's 36 substitution lines are misleading: the
validator states in main.go that they are "DIAGNOSTIC, not a control", emitted only to explain a
hash that moved. Promoting them to errors was measured on 2026-07-21 to fail the approved tree on
thirty-plus HelmReleases, because post-build substitution is the platform's normal mechanism.

The actual error is one line:

unapproved rendered authorization surface fingerprint:
e02856de56721b06897d6c151974796b08ecd73c23b461edde4c4f1efb44636b

Why this PR moves it. restrict-doggy-countdown-hostnames.yaml is authorization-capable and
contains ${domain} (lines 36 and 43). A resource carrying an unresolved substitution is deliberately
forced into the aggregate authorization surface so its ${…} literal is covered by the fingerprint —
so adding this policy legitimately changes surface membership, and the approved constant
(expectedRenderedSurfaceSHA, main.go:366) must move with it.

Confirmed it is this PR and not main: the same test passes on a tree that is main plus a
comment-only change, and fails here. This branch differs from main by only the four files it adds.

What is needed — and why I have not just pasted the new hash. That constant's own comment sets the
approval bar: measure the rendered membership by apiVersion|kind|namespace|name set difference
across the production render trees, confirm exactly which entries move, and record that in the
rationale beside the value. Approving a security fingerprint without that measurement would defeat the
control. The delta here is expected to be exactly the one new ClusterPolicy, but expected is not
measured.

So this is the remaining blocker, and it is a single well-scoped step: run that membership diff, and if
it shows only the new policy, update expectedRenderedSurfaceSHA to the value above with the
measurement recorded alongside it.

Separately: the DIRTY merge state is cleared (see the previous comment) and every other check is
green — 0 failures across the other 31.

The added Kyverno ClusterPolicy enters the fingerprinted authorization
surface as an indirect authorization policy, so the aggregate value moved.

Measured against main 44f14fa: 524 -> 525 documents, membership differing
by exactly the one added ClusterPolicy, zero removed or renamed, and the
remaining 524 byte-identical (negative control fired). The policy is a
validate.deny rule that can only refuse admission; it creates no identity,
binding, role, or policy document.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

expectedRenderedSurfaceSHA updated to e02856de… — the only blocker on this PR.

The new Kyverno ClusterPolicy is selected into the fingerprinted authorization surface (Kyverno ClusterPolicies count as indirect authorization policies), so the aggregate value had to move. Approving it deliberately rather than mechanically:

Membership — measured against main 44f14fab, rendering all five production roots on both sides: 524 documents on main, 525 here, differing by set difference in both directions over the complete apiVersion|kind|namespace|name identity by exactly one entry:

kyverno.io/v1  ClusterPolicy  restrict-doggy-countdown-hostnames

Zero removed, zero renamed. After removing just that entry the remaining 524 documents are byte-identical to main's render — checked with a negative control that fired on a one-line perturbation, so the identical result is not vacuous. Nothing added, removed or renamed touches a Role, ClusterRole, RoleBinding, ClusterRoleBinding, ServiceAccount or IAM document.

Direction of the change — the added policy is a single validate.deny rule matching HTTPRoute in the doggy-countdown namespace and requiring exactly the tenant's assigned hostname, with validationFailureAction: Enforce. It creates no identity, binding, role or policy document and grants nothing to the aws/aws service account; it can only refuse admission, so it moves the surface strictly in the restrictive direction.

Corroboration — two independent renderers agree on the value: the required CI job on the approved toolchain, and a local render on kubectl v1.36.1 with the same embedded kustomize v5.8.1. Agreement across a kubectl patch difference is what rules out a renderer-version artifact.

One trap recorded in the code comment while verifying this: TestValidateAuthorizationAcceptsCommittedPolicy calls validateAuthorization directly and so never passes through validateRendererVersion — only run() does. A passing go test is therefore not evidence that the local toolchain matches the pin.

Ablation on the approved constant: flipping one hex digit fails the test; restoring it passes.

@github-code-quality

Copy link
Copy Markdown

Code Coverage Overview

Languages: Go

Go / code-coverage/go

The overall coverage in commit 2991e54 in the codex/propose-fix-fo... branch is 80%. Coverage data for the main branch is not yet available.

Show a code coverage summary of the most covered files.
File main codex/propose-fix-fo... 2991e54 +/-
scripts/kubesca...ridge/issues.go 99%
scripts/validat...-verify/main.go 97%
scripts/kubesca...e/exceptions.go 96%
scripts/kubesca...-bridge/main.go 93%
scripts/validat...ify/instance.go 87%
scripts/validat...signing/main.go 85%
scripts/validat...-policy/main.go 80%
scripts/generat...eptions/main.go 79%
scripts/kubesca...ridge/client.go 74%
scripts/annotat...checkov/main.go 54%

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

🔐 Validate EKS Authorization now passes at this head — the surface fingerprint was approved with a full membership and byte-identity measurement (see the comment above). Checks green, no unresolved threads, mergeStateStatus: CLEAN. No review has been requested at this head.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

@devantler I will review the changes at the requested head.

⚠️ Action not completed

Review rate limited.

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

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

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

Next review available in: 54 minutes

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

How can I continue?

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

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

How do review limits work?

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

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

Please refer docs for additional details.

Review details
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6fbf755e-9ffa-41ca-920f-73f7fa64de3e

📥 Commits

Reviewing files that changed from the base of the PR and between 44f14fa and 2991e54.

📒 Files selected for processing (5)
  • k8s/bases/infrastructure/cluster-policies/best-practices/restrict-doggy-countdown-hostnames.yaml
  • k8s/bases/infrastructure/cluster-policies/kustomization.yaml
  • scripts/validate-eks-ci-role-policy/main.go
  • tests/restrict-doggy-countdown-hostnames/kyverno-test.yaml
  • tests/restrict-doggy-countdown-hostnames/resources.yaml

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

❤️ Share

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

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Design collision with #2740 — flagging rather than acting, since both approaches are defensible and
only one should land.

#2740 was opened to close the same class of gap and carried an open [P1] asking for the boundary to
cover every tenant-edit reconciler namespace, not one tenant. I resolved that P1 in ed8dee15 by
generalising the existing restrict-wedding-app-route-hostnames policy into
restrict-tenant-route-hostnames: one policy with a per-tenant allow-list rule for wedding-app,
ascoachingogvaner and doggy-countdown, plus a catch-all that denies HTTPRoutes in any other
ksail tenant namespace so a newly onboarded tenant fails closed.

That overlaps this PR, which adds a separate restrict-doggy-countdown-hostnames policy for the third
tenant. If both merge, two Enforce policies govern doggy-countdown routes and a route must satisfy
both.

One concrete incompatibility, worth checking either way. This PR allows simba.${domain}, which
resolves per-cluster; #2740 allows the literal simba.platform.devantler.tech. The tenant's own
deploy/httproute.yaml declares exactly one hostname — simba.platform.devantler.tech — with a
comment tying it to external-dns's devantler.tech domain filter, and no .lan variant. So on a
local cluster ${domain} resolves to platform.lan and this policy would deny the tenant's actual
route. That is latent today because apps are opt-in locally, but it is a real difference in behaviour
rather than a stylistic one.

Suggestion, not a decision: consolidate on the single generalised policy and close this PR, since
one policy covering all tenants plus a fail-closed catch-all is what keeps the next tenant from
being missed — which is exactly how ascoachingogvaner and doggy-countdown came to be uncovered.
The counter-argument is that this PR is further along in one respect: it updates
scripts/validate-eks-ci-role-policy/main.go and ships kyverno fixtures, and #2740 currently fails the
🔐 Validate EKS Authorization check.

I have not closed anything here — the gap this PR fixes is real until #2740 merges, and picking
between them is a judgement call worth making explicitly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 🫴 Ready

Development

Successfully merging this pull request may close these issues.

1 participant