Skip to content

feat(ci): adopt cluster-template pre-commit hooks, scope CI tokens - #4381

Closed
Tanguille wants to merge 19 commits into
fix/security-audit-runner-splitfrom
feat/ci-lefthook-hooks
Closed

feat(ci): adopt cluster-template pre-commit hooks, scope CI tokens#4381
Tanguille wants to merge 19 commits into
fix/security-audit-runner-splitfrom
feat/ci-lefthook-hooks

Conversation

@Tanguille

Copy link
Copy Markdown
Owner

Third layer of the stack: #4378 (simplify) -> #4379 (security) -> this.

Adopts the lefthook pre-commit setup cluster-template ships, plus the shellcheck command from home-operations/.github lefthook.common.toml. Config is inlined rather than pulled from the org remote, so no unpinned external config executes at commit time.

Hooks

Hook Glob Action
format-just *.just, .justfile just --fmt
format-mise .mise.toml mise fmt
format-json *.json, *.json5, *.jsonc oxfmt
format-markdown *.md, *.mdx oxfmt
format-yaml *.yaml, *.yml oxfmt
mise-lock .mise.toml mise lock, stages mise.lock
shellcheck *.sh shellcheck
zizmor .github/workflows/*.yaml zizmor --offline

skip = ["merge", "rebase"] keeps the hooks off conflict resolution. mise postinstall = "lefthook install" wires it up on clone.

zizmor findings fixed

Workflow Finding Fix
agent-pr-review.yaml checkout of untrusted PR code left a usable token in .git/config persist-credentials: false
labeler.yaml app token inherited every permission its installation holds permission-contents: read, permission-pull-requests: write

agent-pr-review is the same workflow #4379 moves off the Talos-credentialed runner. Removing the persisted token closes the other half of that exposure: a prompt injection in PR text can no longer reach a repo-write token from the checkout.

Other

kubernetes/flux/cluster/ks.yaml moves its comment above the list item because oxfmt rewrites the inline form with a trailing space, which .editorconfig forbids. No behaviour change.

Supersedes

#4377 proposed a shell-lint.yaml CI workflow for the same goal. Running shellcheck at pre-commit instead means no CI job and no runner minutes, so #4377 should close unmerged if this lands.

…rceSets

Thirty objects across 42 files, differing only in name, IP, port and homepage
annotations. Adding a service is now one input entry.

flux-operator build resourceset renders all 30 objects identically to the
previous kustomize output, apart from the resourceset.fluxcd.controlplane.io
ownership labels and three targetPort: 443 lines that restate spec.ports.port.
The same four-line dnsConfig sat in five HelmReleases, documented in one. The
two reasons it is there (three NXDOMAIN round-trips per in-cluster FQDN, and an
external name being hijacked by a same-named local Service) now live once.

Merged value trees are identical for all five apps.
HelmChart, Alert, Provider and Receiver emitted gotk_resource_info that no
alert and no dashboard reads: flux-instance/prometheusrule.yaml queries only
HelmRelease and Kustomization, and the two pinned upstream dashboards query
only Kustomization, HelmRelease, GitRepository, HelmRepository, Bucket and
OCIRepository. Their RBAC grants go with them.

The seven surviving kinds share one name/namespace/ready/suspend shape, so
anchors leave each block showing only what differs. 422 lines to 213, rendered
output unchanged.
…ches

The observer proxy read whole bodies into memory before writing a byte.
/observer/shares?limit=10000 measures 9.2MB, fetched every 10s by every open
tab, buffered in a 256Mi container. It now streams with copyfileobj and asks
for gzip, which the same request serves in 3.0MB.

Also: one send_json and one proxy helper replace seven copies of the response
boilerplate, ThreadingHTTPServer replaces ThreadingTCPServer plus a manual
allow_reuse_address, and the monerod get_info request is built once.

Frontend: initialize() refetched /stats_log.json and rebuilt both charts
microseconds before updateStats() did the same; chart options and their four
closures were rebuilt every tick and thrown away on the update path; pool_info
joins the existing Promise.all instead of costing its own round trip.
The pull job refetched a 96.6MiB talosctl on every container PR, roughly 400 a
month over the home uplink, to run a tool that then pulls images. The runner
image ships yq but no talosctl, so the download itself is needed.

An RWX PVC at /tool-cache is shared by every runner pod, keyed by Talos
version, so it is one download per version cluster-wide. fsGroup 1001 because
the runner is uid/gid 1001 and the CephFS mount is otherwise root-owned.
Eleven sites across ten charts. Each verified by pulling the pinned chart and
diffing helm template before and after: rendered output is identical
everywhere (crowdsec and vmstack differ only in per-run generated certs).

Also collapses five repeated gatus endpoint alert blocks onto
alerting.discord.default-alert; enabled: true already restates the gatus
default. Parsed alert state is unchanged for all five endpoints.
…ntics

Four near-identical strategic-merge patches setting one memory limit each
become two regex-targeted ones, matching the idiom already used twice in the
same file.

The cluster patch carried a substitute-from opt-out label with no users
anywhere in the repo. It was a one-word kill switch for sops decryption and
secret substitution across a whole app tree, so it is gone; the note about the
patch replacing substituteFrom wholesale stays.

privileged-movers used a JSON6902 add into /metadata/annotations, which only
resolved because common/namespace.yaml happens to emit an annotations map.
A strategic merge creates the map when absent and drops the ~1 escaping.
opencode Service and EndpointSlice: type ClusterIP, protocol TCP and a
targetPort equal to port. gitleaks: job permissions identical to the
workflow-level block. nextcloud: the fromAddressKey workaround cited chart
9.2.0 while the pin is 9.2.5, rechecked and still required. xmrig resourceset:
a sub-comment claiming fast scale-up above a 60s window, inside a block its own
header calls inert. controller.py: a `not samples` guard unreachable because
max() over an empty set already raised, and a timeout kwarg restating the
signature default.
The mount does not exist until the runner HelmRelease in this PR rolls out, so
the job would fail on its own CI run, and it disappears if CephFS is down.
Falls back to a per-run download in both cases.
DeepSource JS-0116: updateWindowLuck lost its only await when the pool_info
fetch moved into the batch, so it no longer needs to be async.
DeepSource JS-R1004: /observer/pool_info has no interpolation.

The talosctl cache is a shared writable mount, so its contents are untrusted:
every read is now checked against the release sha256 and copied to job-local
storage before going on PATH, a poisoned or truncated entry falls through to a
plain download, and the temporary file uses mktemp rather than $$, which is not
unique across pods sharing the mount. Superseded versions are pruned, which
also bounds growth on the 2Gi share.
Checking the cache entry and then reading it left a window where another job
could swap the file in between, so the local copy reached PATH unverified.
Copying first and hashing what was actually copied closes it, and drops a
verify call.
40 added comment lines down to 25; no comment loses a measured fact.
Two cache-miss jobs pruning the same stale entry make one find call exit
non-zero, which under set -e fails the step. Reproduced at 11/12 rounds with
two concurrent pruners; 0/12 with -ignore_readdir_race.
…ce PSA baseline

cluster-runner mounts a Talos os:admin credential into every pod it schedules, and
agent-pr-review feeds untrusted PR text to a tool-calling model on that pod. Split the
scale set so only flate's image-pull job keeps the credential.

- add review-runner scale set: no talos volume, no talos.dev ServiceAccount
- cluster-runner drops to minRunners 0 / maxRunners 2 (flate fires on kubernetes/** only)
- opencode: automountServiceAccountToken false, no SA/Role/ClusterRole exists for it
- opencode: pin the three npm plugins, they execute in-process in an agent pod
- add psa-baseline component, enforce on 9 namespaces verified to have zero violations
…xists

Flux creates review-runner only after this merges, so pointing the workflow at it in
the same PR leaves the job queued with no runner. Switch follows in a one-line PR.
agent-pr-review still runs on cluster-runner until the follow-up PR moves it, so
dropping to 0/2 now would cut review capacity from 5 to 2 and cold-start every review.
- cap review-runner at 0/3 so combined ceiling stays at today's 5 concurrent runners
- drop containerMode: no workflow declares `container:` and REQUIRE_JOB_CONTAINER is
  false, so the k8s hooks never fired; _work now uses emptyDir, not a 25Gi PVC per pod
- add opencode customManager so the pinned npm plugin versions do not rot
- retarget a topologySpread comment copied verbatim from cluster-runner
… default

Completes the runner split and inverts the PodSecurity default so new namespaces land
enforced rather than silently unprotected.

- agent-pr-review runs on review-runner; cluster-runner keeps os:admin for flate only
- cluster-runner 0/2, review-runner 1/3: ceiling stays at 5 concurrent runners
- components/common sets enforce: baseline; privileged workloads override to privileged
- privileged-movers carries its own override, colocating the exception with its cause
- psa-baseline (opt-in) replaced by psa-privileged (opt-out) for the 4 infra namespaces

Verified: all 16 namespaces render the intended enforce level, and kubectl apply
--dry-run=server reports no PodSecurity violations on the 9 enforced namespaces.
Replaces the proposed shell-lint workflow with the lefthook setup
cluster-template ships (oxfmt, mise fmt/lock, zizmor), plus the
shellcheck command from home-operations/.github lefthook.common.toml.
The config is inlined rather than pulled from the org remote so no
unpinned external config runs at commit time.

zizmor flagged two live issues, both fixed here: the agent PR review
checkout left a usable token in .git/config while checking out
untrusted PR code, and the labeler app token inherited every
permission its installation holds.

ks.yaml moves its comment above the list item because oxfmt rewrites
the inline form with a trailing space, which .editorconfig forbids.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a3f9f18-caf5-40ef-afc3-7470a2536828

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@deepsource-io

deepsource-io Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

DeepSource Code Review

We reviewed changes in a656987...e8b5832 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
JavaScript Aug 6, 2026 7:30p.m. Review ↗
Shell Aug 6, 2026 7:30p.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@Tanguille

Copy link
Copy Markdown
Owner Author

Superseded by #4379, which carries a newer variant of the same work committed directly on fix/security-audit-runner-split (8704dc806), plus the Task -> just migration on top (a95388074).

This branch's actual git base is f4ed79092 on main, not the fix/security-audit-runner-split it declared, so the two never shared history and now conflict in six files (.lefthook.toml, .mise.toml, the review runner helmrelease and kustomization, kubernetes/apps/ai/kustomization.yaml, kubernetes/apps/flux-system/flux-instance/app/helmrelease.yaml).

Nothing is lost. Compared against #4379 the only unique content here is a format-markdown hook that #4379 deliberately removed, because .prettierignore excludes **/*.md and oxfmt exits 2 when every input is ignored. #4379 additionally adds exclude = ["*.sops.yaml"], keeping the formatter away from encrypted secrets.

@Tanguille Tanguille closed this Aug 6, 2026
An error occurred while trying to automatically change base from fix/security-audit-runner-split to main August 8, 2026 14:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant