Skip to content

test(seal-check): run the egress-enforcement probe live in k3d CI (#1184) - #541

Merged
LukasWodka merged 8 commits into
developfrom
feat/1184-live-egress-seal-check
Aug 3, 2026
Merged

test(seal-check): run the egress-enforcement probe live in k3d CI (#1184)#541
LukasWodka merged 8 commits into
developfrom
feat/1184-live-egress-seal-check

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What & why — RFC-0003 D12 seal check (backend#1184)

The chart already ships its enforcement probes as helm.sh/hook: test Jobs (egress-enforcement, backend-reachability, storage-assertions), but helm test runs nowhere in CI — so the probes have never actually executed in a pipeline. SEAL-CHECK §8.4 records the k3s NetworkPolicy substrate as verified (#504, a manual throwaway-cluster check) while the full-chart egress-enforcement probe run is still marked pending.

This PR makes that probe run for real, on every CI push.

Changes

  • scripts/tests/e2e-seal-check.sh — installs the local working-tree chart on a real k3d cluster (reusing the installer's create_cluster path) with public images and the egress lockdown engaged (allowExternalHttps=false), then runs only the egress-enforcement seal-check via helm test --filter. A training-labelled pod's direct TCP egress to 1.1.1.1:443 must be blocked by k3s's CNI.
  • .github/workflows/helm-ci.yaml — a seal-check-e2e job mirroring upgrade-e2e: stock ubuntu-latest, zero secrets (public curl image, no backend, no private images).
  • client/tests/egress_enforcement_check_test.yaml — pins the probe Job's metadata.name so the e2e --filter can never silently drift off it.

The subtleties worth a reviewer's eye

  • helm test --filterexits 0 when the filter matches nothing — a silent false-pass. The script guards this by asserting the probe hook is in helm get hooksbefore testing, then drives the verdict off helm test's exit code (the probe Job exits 0 only when egress is verified blocked). It does not grep --logs (unreliable for Job hooks). The name-pin unittest is belt-and-braces on the filter name.
  • Positive control: a pass is only trusted if a non-policied pod first proves the host is reachable — so a block is attributable to the NetworkPolicy, not a runner/target issue.

Why only egress-enforcement

It's the single seal-check that needs no credentials. backend-reachability needs a real backend + proxy, and a bound-PVC storage-assertions pass needs the dev harness — both belong to the dev-environment e2e run (the remaining #1184 subtask), not a stock runner.

Verified locally

shellcheck clean · helm template renders the probe Job as <release>-egress-enforcement-check · helm unittest 27 suites / 320 tests green. The live k3d run is exercised by the new CI job itself (like the other e2e scripts, it isn't runnable on a dev laptop without docker+k3d).

Notes

  • Targets develop.
  • Advances backend#1184 (consolidate/execute the seal-check); the §8.3 matrix fill is the sibling cli#449. Epic #1151 — Lukas drives, Saqlain reviews.

🤖 Generated with Claude Code


Note

Low Risk
Changes are CI, test automation, and a helm-unittest assertion; they do not alter runtime chart behavior or production install paths.

Overview
Adds live CI coverage for the chart’s egress-enforcement helm test hook, closing the SEAL-CHECK gap where probes existed but helm test never ran in the pipeline.

A new scripts/tests/e2e-seal-check.sh brings up an isolated k3d cluster, installs the local chart with allowExternalHttps=false (and a longer enforcement probe timeout for slow CNI reconcile on GHA), runs a positive control curl from an unpolicy’d pod to 1.1.1.1, then asserts the probe hook is present and runs helm test --filter on <release>-egress-enforcement-check, failing on exit code (not vacuous filter matches or unreliable --logs).

.github/workflows/helm-ci.yaml gains a seal-check-e2e job (30m timeout, no secrets). Path filters and shellcheck lists in helm-ci, installer-tests, and standard-checks include the new script (and scripts/lib/** for helm-ci triggers).

client/tests/egress_enforcement_check_test.yaml pins the probe Job metadata.name to RELEASE-NAME-egress-enforcement-check so the e2e filter cannot silently match nothing and pass.

Reviewed by Cursor Bugbot for commit 068bd9f. Bugbot is set up for automated code reviews on this repo. Configure here.

…184)
RFC-0003 D12: the chart's enforcement probes ship as `helm.sh/hook: test`
Jobs, but `helm test` ran nowhere in CI — SEAL-CHECK §8.4 recorded the k3s
NetworkPolicy substrate as verified (#504) while the full-chart egress probe
run stayed "pending". This closes that gap.
- scripts/tests/e2e-seal-check.sh: install the local chart on a real k3d
cluster with public images + the egress lockdown engaged
(allowExternalHttps=false), then `helm test --filter` the
egress-enforcement seal-check. Requires BOTH a zero exit AND the probe's
`OK egress lockdown verified` marker in the logs — guarding the
helm-test-`--filter`-matches-nothing silent-pass trap.
- .github/workflows/helm-ci.yaml: a `seal-check-e2e` job mirroring
upgrade-e2e (stock ubuntu runner, zero secrets — public curl vs 1.1.1.1).
- client/tests/egress_enforcement_check_test.yaml: pin the probe Job's
metadata.name so the e2e --filter can never silently drift off it.
Local: shellcheck clean · helm template renders the probe Job · helm-unittest
27 suites / 320 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodkaLukasWodka self-assigned this Aug 3, 2026
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

… 127)
The sourced libs define install_kubectl/install_k3d/install_helm but do not
call them; create_cluster + helm need the binaries on PATH first, and a stock
runner has none preinstalled. Mirror e2e-auto-upgrade.sh's prerequisite block
+ the post-create node-ready wait.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

The egress-enforcement probe is a Job-type test hook with hook-delete-policy
hook-succeeded. `helm test --logs` looks up the pod by the Job's bare name,
but a Job's pod has a generated suffix ("pods not found"), and Helm deletes
the Job on success anyway — so --logs errored even though the probe passed.
Drive off the exit code instead (the probe exits 0 only when egress is
verified blocked), guard the --filter-silent-pass by asserting the hook is in
`helm get hooks` first, and dump the pod log via kubectl only on failure
(the Job persists when it fails).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

Comment threadscripts/tests/e2e-seal-check.sh Outdated
Comment thread.github/workflows/helm-ci.yaml
Comment threadscripts/tests/e2e-seal-check.sh Outdated
Comment threadscripts/tests/e2e-seal-check.sh
Comment threadscripts/tests/e2e-seal-check.sh
Comment thread.github/workflows/helm-ci.yaml
Three Bugbot findings on the new e2e seal-check:
- Medium: set TRACEBLOC_NO_AUTOSTART=1 before create_cluster (like the sibling
e2e-*.sh) so it never mutates the host's Docker restart policy / runs
`systemctl enable docker`.
- Medium: add scripts/tests/e2e-seal-check.sh to helm-ci.yaml on.push/
on.pull_request paths so script-only edits re-trigger the k3d job.
- Low: enumerate the script in the installer-tests ShellCheck gate (both the
error and warning passes), matching the other e2e entrypoints.
The High finding ("Job logs never reach marker check") was already resolved in
the prior commit — the script drives off `helm test`'s exit code and no longer
greps --logs output.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

…bot)
`helm get hooks | grep -q` lets grep close the pipe on first match, SIGPIPE-ing
helm mid-write; under set -o pipefail that false-fails the guard even when the
hook exists. Capture helm output to a var, then grep a here-string — no pipe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

Comment thread.github/workflows/helm-ci.yaml Outdated
The script's own bounds (create_cluster up to 15m + helm test 360s + tool
install + helm install) can exceed a 20m GHA cap on a slow cluster bring-up,
false-failing even while each component is inside its own timeout. Match the
sibling k3d job (upgrade-e2e = 30m).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@saqlainsyed007saqlainsyed007 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review — the mechanism is well thought through (the helm test --filter silent-pass trap is genuinely closed by the hook-existence guard + the name-pin unittest, and driving off exit code rather than --logs is the right call for a Job hook). A few things worth addressing before this becomes a trusted gate, the first being a real soundness gap.

Also cleared while reviewing: the missing export USER vs. the sibling script is safe (every $USER ref in the sourced libs is guarded), and the new script correctly does not need a scripts/manifest.sha256 entry (test scripts aren't in gen-manifest.sh's FILES list).

One doc nit: the PR description says the script requires both a zero exit and the OK egress lockdown verified log marker — but the implemented script drives off the exit code only (its comments correctly explain why). The code is internally consistent; the description overstates it.

Comment threadscripts/tests/e2e-seal-check.sh
Comment threadscripts/tests/e2e-seal-check.sh Outdated
Comment thread.github/workflows/helm-ci.yaml
Comment threadscripts/tests/e2e-seal-check.sh
Comment threadscripts/tests/e2e-seal-check.sh Outdated
…aqlain)
- Positive control (blocking): before trusting a BLOCKED probe, prove the
cluster can REACH the host from a non-policied pod (in `default`, ungoverned
by the namespace-scoped training-egress policy). Without it, egress failing
for an unrelated reason (runner firewall / target outage / rate-limit) would
make the probe print OK and pass green while the NetworkPolicy did nothing.
A reachable positive + a blocked training pod = the block is attributable to
the policy. Positive-control failure now fails the seal-check as inconclusive.
- Bump enforcementProbeTimeoutSeconds 60s->240s (blocking): on a cold GHA
runner k3s can take >60s to program the pod iptables while the chart installs;
the probe is single-shot (backoffLimit 0), so 60s false-fails. 240s is well
inside the 360s helm-test budget.
- Add scripts/lib/** to helm-ci paths (blocking): the script sources
scripts/lib/{common,setup-linux,cluster,preflight}.sh, so a lib-only edit must
re-trigger seal-check-e2e + upgrade-e2e (both depend on it).
- Derive NS from CLUSTER_NAME so a CLUSTER_NAME override isolates a run under
one name instead of desyncing cluster vs release/namespace.
shellcheck clean; workflow parses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

All five review points addressed in the latest push (positive control + 240s probe timeout + scripts/lib/** paths + NS derivation; prelude-dedup deferred to a follow-up), and the PR description corrected to match the exit-code-driven implementation. bugbot run

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 08f169c. Configure here.

Comment thread.github/workflows/installer-tests.yaml
Comment threadscripts/tests/e2e-seal-check.sh Outdated
…t (Bugbot)
- Add --tlsv1.2 to the positive-control curl — the curl-tls house rule (the
required quality/house-rules gate) rejects a curl that could negotiate a
downgraded TLS version.
- Wait for the default ServiceAccount before the positive-control kubectl run:
a fast runner can schedule the pod before the SA exists, aborting under set
-e with 'serviceaccount default not found' before the attribution message.
- Add e2e-seal-check.sh to the standard-checks.yml Lint shellcheck list (the
REQUIRED branch-protection gate) — installer-tests had it, the required Lint
did not, so a shellcheck regression in the new script could miss the gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

Also fixed the quality/house-rules failure (added --tlsv1.2 to the positive-control curl per the curl-tls rule). bugbot run

@saqlainsyed007saqlainsyed007 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — round-2 asks are all resolved and the seal-check is now sound.

I re-verified the mechanism end-to-end against the chart, not just the diff:

  • The probe fails loudly when egress is NOT sealed.templates/egress-enforcement-check.yaml exits 0 only on curl exit 7/28 (TCP connect refused/timed out), retries up to the timeout, and exits 1 with a loud banner if the connect ever succeeds (rc=0 or any TLS-layer code 35/52/56/60, all latched via reached=1) or if every attempt is DNS-only (exit 6). No swallowed timeout, no || true, no grep-matches-nothing pass. The verdict is keyed on curl's exit code, not HTTP status.
  • No false-green path. For the gate to pass green, the positive control must Succeed AND helm test must exit 0; the probe Job exits 0 only when egress is blocked. An open egress makes the probe exit 1. Image-pull / timeout / DNS failures all fail safe (inconclusive), never green.
  • The helm test --filter silent-pass trap is genuinely closed by the helm get hooks existence guard (now a here-string, no SIGPIPE under pipefail) plus the name-pin unittest — belt and braces.
  • Netpol scoping confirms attribution.network-policy-training.yaml is namespace-scoped to the release ns and selects tracebloc.io/workload=training; with allowExternalHttps=false rule 2 (0.0.0.0/0:443) is dropped, so the training pod's 1.1.1.1:443 is denied. The positive control runs in default (ungoverned) — a genuine, non-tautological control that can fail if the runner/target blocks egress, exactly ruling out the environmental false-block the prior review flagged. clusterCidrs defaults (10/8, 172.16/12, 192.168/16) cover k3d's 10.42/10.43 CIDRs, so the fail guard won't trip on the credential-free install.

Round-2 items — all landed: positive control (added), enforcementProbeTimeoutSeconds=240 inner deadline, scripts/lib/** trigger paths, NS derived from CLUSTER_NAME, TRACEBLOC_NO_AUTOSTART=1, SA-wait race guard, 30m job timeout, required standard-checks Lint + installer-tests shellcheck lists. Prelude duplication was correctly deferred as an out-of-scope follow-up.

One non-blocking nit inline. shellcheck is reported clean; bash -n passes here.

Comment threadscripts/tests/e2e-seal-check.sh
@LukasWodka
LukasWodka merged commit 5a236e4 into developAug 3, 2026
47 checks passed
LukasWodka added a commit that referenced this pull request Aug 3, 2026
Saqlain flagged (PR #541, thread on e2e-seal-check.sh:36) that the bring-up
prelude is copy-pasted near-verbatim across scripts/tests/e2e-*.sh, and
multiple Bugbot rounds have had to edit every copy in lockstep. Extract the two
truly-identical, drift-prone blocks into scripts/tests/lib/e2e-common.sh:
- e2e_isolate_env <name> — USER + CLUSTER_NAME default + TRACEBLOC_NO_AUTOSTART
- e2e_install_prereqs — has docker + umask + install_{kubectl,k3d,helm}
e2e-cluster / e2e-proxy / e2e-journey / e2e-auto-upgrade now source the lib and
call these; each keeps its own CLUSTER_NAME default (passed as the arg) and its
distinct logic. auto-upgrade keeps its extra `has jq` guard before the call.
Deliberately NOT unified (would change behavior): the sub-lib `source` set
(proxy/journey source 3 libs, not preflight — a pre-existing inconsistency,
flagged not fixed), the cleanup/trap bodies (each reaps its own squid/work
dirs), and CHART_DIR (only the chart-installing scripts). e2e-seal-check.sh
(open on #541) adopts the lib as a fast-follow once both land — kept non-stacked.
Added the lib to both shellcheck gates (installer-tests + the required
standard-checks Lint). shellcheck --severity=error/warning clean; bash -n ok.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LukasWodka added a commit that referenced this pull request Aug 3, 2026
…550)
Saqlain flagged (PR #541, thread on e2e-seal-check.sh:36) that the bring-up
prelude is copy-pasted near-verbatim across scripts/tests/e2e-*.sh, and
multiple Bugbot rounds have had to edit every copy in lockstep. Extract the two
truly-identical, drift-prone blocks into scripts/tests/lib/e2e-common.sh:
- e2e_isolate_env <name> — USER + CLUSTER_NAME default + TRACEBLOC_NO_AUTOSTART
- e2e_install_prereqs — has docker + umask + install_{kubectl,k3d,helm}
e2e-cluster / e2e-proxy / e2e-journey / e2e-auto-upgrade now source the lib and
call these; each keeps its own CLUSTER_NAME default (passed as the arg) and its
distinct logic. auto-upgrade keeps its extra `has jq` guard before the call.
Deliberately NOT unified (would change behavior): the sub-lib `source` set
(proxy/journey source 3 libs, not preflight — a pre-existing inconsistency,
flagged not fixed), the cleanup/trap bodies (each reaps its own squid/work
dirs), and CHART_DIR (only the chart-installing scripts). e2e-seal-check.sh
(open on #541) adopts the lib as a fast-follow once both land — kept non-stacked.
Added the lib to both shellcheck gates (installer-tests + the required
standard-checks Lint). shellcheck --severity=error/warning clean; bash -n ok.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
LukasWodka added a commit that referenced this pull request Aug 3, 2026
…ollow) (#566)
Promised on the #541 review threads, now that #550 (the shared lib) and #541
(the seal-check script) are both on develop:
- e2e-seal-check.sh now sources scripts/tests/lib/e2e-common.sh and uses
e2e_isolate_env / e2e_install_prereqs, matching the other e2e-*.sh (drops the
inlined isolation-env + install block). Keeps its own NS=$CLUSTER_NAME and
local fail() for its assertions.
- Harden the positive control (Saqlain nit): pin networkPolicy.training.
enforcementProbeHost to a single $HOST var the install passes AND the positive
control targets, so the probe and the control can never drift onto different
hosts (was: HOST hardcoded while the probe used the chart default).
shellcheck --severity=error/warning clean; helm template renders the probe Job
with HOST=1.1.1.1 pinned.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

/fr-pass

LukasWodka added a commit that referenced this pull request Aug 5, 2026
…ow) (#598)
* test(e2e): full seal suite vs the dev backend — backend#1184 deferred fast-follow
New e2e-full-seal.sh installs the working-tree chart on real k3d as the
dedicated dev e2e-test-agent (real credentials, CLIENT_ENV=dev), waits for
every release PVC to Bind and jobs-manager to hold a real backend session,
then runs helm test UNFILTERED — egress-enforcement + backend-reachability
+ storage-assertions in one release, hook-presence-guarded so a regated
check can't vanish silently. New helm-ci job full-seal-e2e runs it on
push/dispatch only and skips green with a notice until the e2e-test-agent
secrets (TB_E2E_CLIENT_ID / TB_E2E_CLIENT_PASSWORD) are provisioned.
The egress positive control moves verbatim into e2e-common.sh (one copy,
shared with e2e-seal-check.sh). SEAL-CHECK.md gains the CI-coverage map and
drops three follow-ups that shipped since (#541 live probe, cli#393
verdict, cli#449 matrix).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(e2e): address Bugbot findings on the full-seal harness
- credentials travel in a mode-0600 temp values file, never on argv
(process-list exposure on a shared runner + helm --set comma/brace
mangling); removed on every exit path — installer parity
- the PVC wait does one guarded fetch per iteration, so a transient
kubectl failure retries until the deadline instead of aborting under
set -euo pipefail
- full-seal-e2e job timeout 30m -> 45m: the script stacks a 300s PVC wait,
two 300s rollouts and a 600s unfiltered helm test on create_cluster's
15m bound — GHA must not kill a slow-but-healthy run
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(e2e): local-path provisions RWO only — pin pvcAccessMode on the full-seal install (Bugbot)
The chart's PVC default is ReadWriteMany; rancher.io/local-path never
provisions it, so every claim sat Pending and both the Bound pre-wait and
storage-assertions were guaranteed to fail once the job activates. The
installer writes pvcAccessMode: ReadWriteOnce for this exact storage path
— the harness now installs with the same value.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@LukasWodka
LukasWodka deleted the feat/1184-live-egress-seal-check branch August 14, 2026 13:53
Sign up for freeto 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.

2 participants

@LukasWodka@saqlainsyed007