Skip to content
Merged
75 changes: 71 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -494,13 +494,77 @@ jobs:

- name: Render every addon across every environment
run: ./scripts/render-addons.py
# Tag immutability is a fact about the commit under test, so it blocks.
# Whether a CVE landed in one of these images overnight is not, and that
# half stays scheduled — blocking a merge on the world changing is how a
# gate teaches people to route around it.
# Tag immutability is a fact about the commit under test, so it blocks
# here. What the image CONTAINS is a separate question with a separate
# job — image-vulnerabilities — because it has to pull every image and is
# slow enough to be worth running in parallel with this one.
- name: Every rendered image carries an immutable reference
run: ./scripts/check-image-pins.py

# ── Image vulnerability gate ─────────────────────────────────────────
# Whoever moves a chart pin owns what the image behind it carries, and this
# repo pins independently of any cluster. trivy-operator reports the same
# findings at runtime, but to an operator who cannot change the pin without a
# pull request here — so the decision belongs at the pin, which is this job.
#
# Blocking on a CRITICAL with a published fix that image-advisories.yaml does
# not acknowledge. A new advisory can therefore turn a pull request red for a
# reason the pull request did not cause; that is deliberate and the advisory
# file is the release valve, because the alternative is a scan whose findings
# nobody has to answer. HIGH is counted and printed, not gated.
#
# The run scans a digest-pinned end-of-life image first and requires its known
# CRITICALs back. A scanner with no database returns a clean result for every
# image, which is indistinguishable from a healthy fleet by exit code — so
# without the canary a green run here would prove nothing at all.
image-vulnerabilities:
name: Rendered images carry no unacknowledged CRITICAL
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Install Helm
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version-file: .python-version
- name: Install Python dependencies
run: pip install --require-hashes -r requirements.txt

# Same guard the validate job carries: setup-trivy reads an empty version
# as "install the latest release", so an expansion failure would scan with
# an unpinned trivy and pass.
- name: Assert the pinned trivy version resolved
run: |
if [ -z "${{ env.TRIVY_VERSION }}" ]; then
echo "TRIVY_VERSION expanded empty. setup-trivy reads that as 'latest',"
echo "so this job would scan with an unpinned trivy and pass. Restore"
echo "the TRIVY_VERSION key in this workflow's env block."
exit 1
fi

- name: Install trivy
uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1
with:
cache: true
version: ${{ env.TRIVY_VERSION }} # zizmor: ignore[unpinned-tools]

- name: Cache Helm charts
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ~/.cache/helm
key: helm-charts-${{ hashFiles('applicationsets/*.yaml') }}
restore-keys: |
helm-charts-

- name: Every fixed CRITICAL is acknowledged
run: ./scripts/check-image-vulnerabilities.py

# ── Policy-admission gate ────────────────────────────────────────────
# The best-practice / pod-security policies run in Enforce mode on staging and
# production (the overlays flip validationFailureAction to Enforce), so a
Expand Down Expand Up @@ -855,6 +919,7 @@ jobs:
kyverno,
fork-safety,
helm-render,
image-vulnerabilities,
policy-admission,
appsets,
appset-render,
Expand Down Expand Up @@ -886,6 +951,7 @@ jobs:
'Kyverno policy tests (+ verify-images contract)': '${{ needs.kyverno.result }}',
'Fork-safety gate (no hardcoded org, blocking)': '${{ needs.fork-safety.result }}',
'Helm-render (every addon × every env)': '${{ needs.helm-render.result }}',
'Rendered images carry no unacknowledged CRITICAL': '${{ needs.image-vulnerabilities.result }}',
'Policy-admission (Enforce-tier Kyverno vs the fleet)': '${{ needs.policy-admission.result }}',
'ApplicationSet schema + sync waves': '${{ needs.appsets.result }}',
'Appset render (Karpenter subnet selector)': '${{ needs.appset-render.result }}',
Expand Down Expand Up @@ -941,6 +1007,7 @@ jobs:
kyverno,
fork-safety,
helm-render,
image-vulnerabilities,
policy-admission,
appsets,
appset-render,
Expand Down
12 changes: 11 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,16 @@ task validate:athena-panel-columns # Every column a CUR panel names is one the e
task validate:fork-safety # No hardcoded catalog repoURL in applied ApplicationSets (report-only locally)
task validate:log-volume-budget # Loki declares the fraction at which it stops ingesting, and the alert leads it
task validate:falco-rule-floor # Every Falco rule set installed on a node is one Falco actually loads
task validate:image-vulnerabilities # Every fixed CRITICAL in a rendered image is acknowledged (not in `task validate` — see below)
```

### Local `task validate` is a subset of CI

`task validate` runs the structural gates (lint, kustomize build, helm-render,
ApplicationSet schema, sync-wave ordering, appset render, policy-admission,
dashboards, fork-safety). CI runs those plus several gates that have **no local
`task` target**, so a clean `task validate` is necessary but not sufficient:
`task` target**, and one that has a target but is deliberately outside the
aggregate, so a clean `task validate` is necessary but not sufficient:

- **Zero-placeholder gate** — `scripts/no-placeholders.sh` (CI job `placeholders`)
- **Renovate config schema** — `renovate-config-validator`, CI-only because it
Expand All @@ -134,6 +136,14 @@ dashboards, fork-safety). CI runs those plus several gates that have **no local
`scripts/check-catalog-revision.py` (CI job `catalog-revision`)
- **Alert coverage** — `scripts/check-alert-coverage.py`, which runs inside the
`dashboards` job alongside the locally-available dashboard and Athena gates
- **Image vulnerabilities** — `scripts/check-image-vulnerabilities.py` (CI job
`image-vulnerabilities`). The one with a target of its own,
`task validate:image-vulnerabilities`, kept out of the aggregate because it
pulls every image the pinned charts render. Blocking on a CRITICAL
with a published fix that `image-advisories.yaml` does not acknowledge; HIGH is
counted and printed. Every run scans a digest-pinned end-of-life image first
and requires its known CRITICALs back, so a clean result is a result rather
than a scanner with no database
- **Render → render-assert → kubeconform → `trivy config`** (CI job `validate`);
locally this is `task render` then `task scan`, not part of `task validate`

Expand Down
8 changes: 8 additions & 0 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ tasks:
cmds:
- ./scripts/check-image-pins.py

validate:image-vulnerabilities:
desc: "Image-CVE gate — every fixed CRITICAL in a rendered image is acknowledged in image-advisories.yaml"
cmds:
# Pulls and scans every image the pinned charts render, so it is slower
# than the structural gates and is NOT in `task validate`. CI runs it as
# its own blocking job; run it here before moving a chart pin.
- ./scripts/check-image-vulnerabilities.py

validate:workflows:
desc: "Workflow supply-chain gate — zizmor over .github/workflows, offline, MEDIUM+ blocks"
cmds:
Expand Down
Loading
Loading