Skip to content
Open
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
44 changes: 26 additions & 18 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ jobs:
- 'talos/**'
- 'scripts/validate-naming.py'
- 'scripts/validate-embedded-json.py'
- 'scripts/generate-kubescape-exceptions.py'
- '.github/workflows/ci.yaml'
talos:
- 'talos/**'
Expand Down Expand Up @@ -152,25 +153,32 @@ jobs:
# FAILS if a change drops the posture below the threshold, so new
# findings must be fixed (or justified) before merge.
#
# The threshold (85) is a regression FLOOR, not the live score. The
# Kubescape compliance score is **environment-dependent** β€” the same
# binary on the same manifests scores higher locally (macOS, β‰ˆ94%) than on
# the Linux CI runner (β‰ˆ87%), a gap that isn't the render, the framework
# cache, or the merge content β€” and the absolute value also shifts with the
# ksail render, so it can't be reproduced exactly offline. CI is the source
# of truth; the gate sits a few points under the observed CI value to catch
# real regressions without flaking on that variance. (NB the floor may need
# re-baselining after a ksail version bump.)
# The scan consumes the platform's justified exceptions: the
# ClusterSecurityException CRs (k8s/bases/infrastructure/
# cluster-security-exceptions/ β€” the single source of truth, also
# consumed in-cluster by the kubescape-operator) are converted at scan
# time into Kubescape's native exceptions format by
# scripts/generate-kubescape-exceptions.py (#2264). With runtime-
# enforced and except-only findings suppressed, the threshold gates the
# residual REAL posture instead of sitting ~15 points under it.
#
# It is not higher because the residual findings are either runtime-
# enforced (Kyverno securityContext/limits, CiliumNetworkPolicy) or
# genuinely unfixable β€” notably C-0002, the KubeVirt operator's pods/exec
# RBAC, which it needs to manage VMs and can only be excepted. Native scan
# exceptions have now shipped (devantler-tech/ksail#5369); wiring the
# platform's ClusterSecurityException CRs into the scan and ratcheting the
# threshold toward 100 is tracked in #2264. Until then, ratchet up as
# genuine gaps are fixed; never lower it.
run: ksail workload scan --framework nsa --compliance-threshold 85
# The threshold is a regression FLOOR, not the live score. The
# Kubescape compliance score has historically been environment-
# dependent (Linux CI runner vs macOS) and shifts with the ksail
# render, so CI is the source of truth: the observed CI score with
# exceptions applied is 98.87% (2026-07-11, ksail 7.165.2 β€” matching
# the local value on that date), and the gate sits a few points under
# it to catch real regressions without flaking on run-to-run
# variance. (NB the floor may need re-baselining after a ksail
# version bump.)
#
# Known un-excepted residual: C-0012 on the local-dev MinIO Deployment
# (deliberate dev-only credential β€” tracked for a root-cause fix, not
# an exception). Ratchet the floor up as genuine gaps close; never
# lower it.
run: |
python3 scripts/generate-kubescape-exceptions.py -o /tmp/kubescape-exceptions.json
ksail workload scan --framework nsa --exceptions /tmp/kubescape-exceptions.json --compliance-threshold 95

naming:
name: 🏷️ Validate Naming Conventions
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,12 @@ kubectl apply --dry-run=client -f <file>

`flux check` and other cluster-dependent checks require a running cluster β€” they are **not** part of static validation and should not be run during maintenance.

CI runs **static manifest validation** on PRs that touch k8s-related paths (`k8s/**`, `ksail*.yaml`, `.sops.yaml`, `talos*/**`, or `ci.yaml`) β€” the `validate` job in `.github/workflows/ci.yaml` first json-parses every registered embedded-JSON ConfigMap key via [`scripts/validate-embedded-json.py`](scripts/validate-embedded-json.py) (keys listed in the script's `REGISTERED_KEYS` or ending in `.json` β€” schema validation treats such blobs as opaque strings, so a stray comma would otherwise ship silently; run it locally when touching one), then runs `ksail workload validate` for both the local and prod overlays plus a Kubescape `ksail workload scan --framework nsa --compliance-threshold 85`. It is fast, needs no secrets (so it runs on fork PRs too), and starts no cluster. PRs touching `talos/**` or `talos-local/**` additionally run the `validate-talos` job: it renders the machine config with every patch applied (placeholder values stand in for env-expanded secrets like `${WG_SERVER_PRIVATE_KEY}`) and `talosctl validate`s the result, so a broken patch or an empty env expansion fails the PR event instead of the merge group's deploy (#2477). There is **no longer a full-cluster system test**: the local Docker cluster is a thin manual test-bed (see [Local Development Cluster](#local-development-cluster)), not a CI prod stand-in.
CI runs **static manifest validation** on PRs that touch k8s-related paths (`k8s/**`, `ksail*.yaml`, `.sops.yaml`, `talos*/**`, the validation scripts `scripts/validate-naming.py` / `scripts/validate-embedded-json.py` / `scripts/generate-kubescape-exceptions.py`, or `ci.yaml` β€” the authoritative list is the `k8s` filter in `.github/workflows/ci.yaml`) β€” the `validate` job in `.github/workflows/ci.yaml` first json-parses every registered embedded-JSON ConfigMap key via [`scripts/validate-embedded-json.py`](scripts/validate-embedded-json.py) (keys listed in the script's `REGISTERED_KEYS` or ending in `.json` β€” schema validation treats such blobs as opaque strings, so a stray comma would otherwise ship silently; run it locally when touching one), then runs `ksail workload validate` for both the local and prod overlays plus a Kubescape scan (`scripts/generate-kubescape-exceptions.py` converts the `ClusterSecurityException` CRs into Kubescape's exceptions format, then `ksail workload scan --framework nsa --exceptions <generated> --compliance-threshold <floor>` gates on the score β€” the exact floor lives in `ci.yaml`). It is fast, needs no secrets (so it runs on fork PRs too), and starts no cluster. PRs touching `talos/**` or `talos-local/**` additionally run the `validate-talos` job: it renders the machine config with every patch applied (placeholder values stand in for env-expanded secrets like `${WG_SERVER_PRIVATE_KEY}`) and `talosctl validate`s the result, so a broken patch or an empty env expansion fails the PR event instead of the merge group's deploy (#2477). There is **no longer a full-cluster system test**: the local Docker cluster is a thin manual test-bed (see [Local Development Cluster](#local-development-cluster)), not a CI prod stand-in.

The scan is a **hard gate**: it fails the PR if the NSA compliance score drops below the threshold, so new findings must be fixed or justified before merge. Two non-obvious limits:

- **ksail is Renovate-managed** (the Setup step, grouped `ksail` with the deploy pins). It was previously frozen at 7.65.0 because 7.66.x parallelised the in-process Helm render and made it racy β€” `ksail workload validate` threw varying YAML parse errors and the scan score swung run-to-run. That race is resolved upstream ([devantler-tech/ksail#5371](https://github.com/devantler-tech/ksail/issues/5371), closed), so the pin is lifted back onto the latest release. Tripwire: if `validate`/`scan` swing run-to-run again, re-pin to a known-good version and reopen #5371.
- **The threshold (85) is a regression floor, not the actual score.** The Kubescape compliance score is **environment-dependent**: the same ksail binary on the same manifests reports β‰ˆ**87%** on the Linux CI runner but β‰ˆ**94%** locally (macOS) β€” a gap that is *not* the render mode, the framework cache, or PR-merge content (all ruled out) β€” and the absolute value also shifts with the ksail render, so **CI is the source of truth and the score can't be reproduced exactly offline** (re-baseline the floor after a ksail bump). It is below 100 because the residual findings are either runtime-enforced (Kyverno securityContext/limits mutation, `CiliumNetworkPolicy`) β€” invisible to a static scan β€” or genuinely unfixable, notably **C-0002** (the KubeVirt operator's `pods/exec` RBAC, which it needs to manage VMs and can only be excepted). The platform documents these as kubescape `ClusterSecurityException` CRs (`k8s/bases/infrastructure/cluster-security-exceptions/`); native scan exceptions have now shipped ([ksail#5369](https://github.com/devantler-tech/ksail/issues/5369)), and wiring them in to ratchet the threshold toward 100 is tracked in [#2264](https://github.com/devantler-tech/platform/issues/2264). Until then, **ratchet up** as genuine gaps are fixed; never lower it.
- **ksail is Renovate-managed** (the Setup step, grouped `ksail` with the deploy pins). It was previously frozen at 7.65.0 because 7.66.x parallelised the in-process Helm render and made it racy β€” two distinct symptoms of the same regression: `ksail workload validate` non-deterministically corrupted the render with varying YAML parse errors ([devantler-tech/ksail#5362](https://github.com/devantler-tech/ksail/issues/5362), closed β€” contained since KSail v7.163.1 by the [ksail#5978](https://github.com/devantler-tech/ksail/issues/5978) stream-splitting fix, which is what let the temporary `--skip-helm-render` workaround be removed), and the scan's compliance score swung run-to-run ([devantler-tech/ksail#5371](https://github.com/devantler-tech/ksail/issues/5371), closed). Both are resolved upstream, so the pin is lifted back onto the latest release. Tripwire (kept in sync with the comments in `.github/workflows/ci.yaml`): if `validate` output or the `scan` score varies run-to-run again, re-add `--skip-helm-render` and reopen ksail#5362 (or re-pin to a known-good version, reopening #5371 if only the score swings).
- **The threshold is a regression floor, not the actual score β€” and the scan runs WITH the platform's justified exceptions applied.** The `ClusterSecurityException` CRs (`k8s/bases/infrastructure/cluster-security-exceptions/` β€” the single source of truth, consumed in-cluster by the kubescape-operator) are converted at scan time into Kubescape's native exceptions format by [`scripts/generate-kubescape-exceptions.py`](scripts/generate-kubescape-exceptions.py) (fail-closed: an unrecognised CR shape aborts the scan step rather than silently dropping or widening an exception; unit tests in `scripts/tests/`), so runtime-enforced (Kyverno mutation, `CiliumNetworkPolicy`) and except-only findings (e.g. **C-0002**, the KubeVirt operator's `pods/exec` RBAC) no longer depress the score and the floor gates the residual REAL posture (#2264). The score has historically been **environment-dependent** (Linux CI runner vs macOS β€” a gap that is *not* the render mode, the framework cache, or PR-merge content, all ruled out) and shifts with the ksail render, so **CI is the source of truth** (re-baseline the floor after a ksail bump); the observed CI reference with exceptions applied is **β‰ˆ98.9%** (2026-07-11, ksail 7.165.2), with the floor a few points under it. A new justified exception is added as a CSE CR (kind+name-scoped, minimal β€” see the existing CRs' conventions), never by lowering the floor: **ratchet up** as genuine gaps close; never lower it.

## Local Development Cluster

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
# - Flux β€” kustomize-controller and helm-controller (both bound via
# cluster-reconciler-flux-system) and flux-operator hold cluster-admin;
# arbitrary-manifest reconciliation is the GitOps trust model.
# - KubeVirt β€” the kubevirt-operator ClusterRole explicitly grants
# `pods/exec` (vendored upstream bundle); virt-operator needs it to manage
# VM lifecycles. AGENTS.md documents this finding as except-only.
#
# NOT covered (intentionally): the two tenant Flux-impersonation SAs
# (wedding-app, ascoachingogvaner) inherit pods/exec only via the built-in `edit`
Expand All @@ -41,11 +44,13 @@ metadata:
spec:
reason: >-
Velero (all-or-nothing backup/restore RBAC + cluster-admin), CloudNativePG
(operator execs into Postgres pods β€” explicit pods/exec grant), and the Flux
(operator execs into Postgres pods β€” explicit pods/exec grant), the Flux
GitOps reconcilers kustomize-controller, helm-controller (both via the
cluster-reconciler-flux-system binding), and flux-operator (cluster-admin
trust model) legitimately hold pods/exec by design. The specific RBAC bindings are
matched by kind+name so C-0002 still catches any new exec grant.
trust model), and KubeVirt's virt-operator (explicit pods/exec grant to
manage VM lifecycles) legitimately hold pods/exec by design. The specific
RBAC bindings are matched by kind+name so C-0002 still catches any new
exec grant.
posture:
- controlID: C-0002
action: ignore
Expand Down Expand Up @@ -73,3 +78,10 @@ spec:
- apiGroup: rbac.authorization.k8s.io
kind: ClusterRoleBinding
name: ^flux-operator$
# KubeVirt β€” virt-operator manages VM lifecycles via an explicit
# pods/exec grant in the kubevirt-operator ClusterRole (vendored
# upstream operator bundle), bound by this CRB. Documented as
# except-only in AGENTS.md's scan-threshold rationale.
- apiGroup: rbac.authorization.k8s.io
kind: ClusterRoleBinding
name: ^kubevirt-operator$
Loading
Loading