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
75 changes: 71 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -542,13 +542,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 scheduledblocking 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-vulnerabilitiesbecause 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 @@ -903,6 +967,7 @@ jobs:
kyverno,
fork-safety,
helm-render,
image-vulnerabilities,
policy-admission,
appsets,
appset-render,
Expand Down Expand Up @@ -934,6 +999,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 @@ -989,6 +1055,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 @@ -105,14 +105,16 @@ task validate:fork-safety # No hardcoded catalog repoURL in applied App
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:empty-corpus # No gate reports success over a corpus that is not there
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 + manager-default drift** —
Expand Down Expand Up @@ -164,6 +166,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