Skip to content

feat(seal-check): enumeration label contract + storage-assertions test hook (RFC-0003 §8.2–8.4)#378

Open
LukasWodka wants to merge 2 commits into
developfrom
security/seal-check-suite-1184
Open

feat(seal-check): enumeration label contract + storage-assertions test hook (RFC-0003 §8.2–8.4)#378
LukasWodka wants to merge 2 commits into
developfrom
security/seal-check-suite-1184

Conversation

@LukasWodka

@LukasWodka LukasWodka commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Chart half of the RFC-0003 D12 seal check (§8.2–8.4): establishes the seal-check enumeration contract and fills the storage-assertions gap, without touching scripts/ (installer work stays with #368).

1. The enumeration contract (consumed by tracebloc/cli#393)

Every runnable conformance check is a helm.sh/hook: test Job carrying, on the Job and its pod template:

Label Value
tracebloc.io/seal-check "true"
tracebloc.io/seal-check-name egress-enforcement | backend-reachability | storage-assertions

Enumerate without running: helm get hooks <release>. While running: kubectl get jobs,pods -l tracebloc.io/seal-check=true. Label keys + existing names are public API (never renamed; new checks = new names). Auxiliary hook resources (the new SA/RBAC) deliberately do not carry the labels — only runnable checks are enumerable. Applied to the existing egress-enforcement probe + backend-reachability check via a new tracebloc.sealCheckLabels helper.

2. New check: storage-assertions (sealCheck.storageAssertions.*, default on)

kubectl-image test-hook Job (same alpine/k8s:1.30.5 the image-refresh CronJob uses) asserting from in-cluster:

  • pvc-boundclient-pvc / client-logs-pvc / mysql-pvc exist and are Bound (bounded wait, default 120s, for WaitForFirstConsumer + first-install pulls)
  • pvc-storageclass — every release PVC is on the expected class (tracebloc.storageClassName)
  • pv-hostpath (dynamic mode, hostPath.enabled=false, only) — no release PVC backed by an unmanaged hostPath PV. Catches the RFC-0003 D3/D4 leftover-PV claimRef capture (a stale <release>-{data,logs,mysql}-pv grabs the claim even in dynamic mode). k3s local-path node-local paths (nodeLocalPathPrefixes, default /var/lib/rancher/ + /opt/local-path-provisioner) are tolerated and reported — they live inside the cluster node and die with it (Option C); host-visibility of those paths is a cluster-creation fact verified at install level. In hostPath mode the sub-check SKIPs (declared model).

RBAC: dedicated least-privilege SA as negative-weight test hooks — PVC get/list in the namespace; PV get/list only when clusterScope allows a ClusterRole. With clusterScope=false the PV scan degrades to the name/claimRef check and prints an explicit WARNING naming what was not verified.

UNSEALED philosophy (documented in docs/SEAL-CHECK.md): a guarantee that cannot be verified fails loudly — never silently claimed sealed. Disabling a check is a values-visible declaration, not a runtime fallback.

3. Docs

docs/SEAL-CHECK.md — contract, coverage today, philosophy, per-substrate coverage table (chart-side input to the RFC §8.3 matrix), and the k3d/k3s NetworkPolicy egress verification runbook (§8.4) using the existing enforcement probe — documented honestly as not yet executed; the k3d cell stays "verification run pending" until a run is recorded. Cross-linked from docs/SECURITY.md §7.

Compatibility

  • All new values fully nil-guarded (default dict / dig) — helm upgrade --reuse-values from releases predating sealCheck renders identically (unit-tested with sealCheck: null).
  • Test hooks never run at install/upgrade — cannot block them or the auto-upgrade.
  • Forward-compatible with the Option C storage flip (feat(installer): node-local storage prototype (RFC-0003 Option C, flag-gated) #368): gates on hostPath.enabled, verifies whichever model the install declares.

Test plan

  • helm lint --strict PASS on all 4 platform values (aks / bm / eks / oc)
  • helm template clean: 4 CI platforms + near-default values + node-local values (hostPath.enabled=false, local-path class) + flag off + nil sealCheck / nil storageAssertions
  • helm unittest: 287/287 (20 new cases: rendering gates, hook annotations, label contract on Job+pod, RBAC split under clusterScope, env baking, PSA-restricted fields, image pin, nil-block survival)
  • Embedded script: sh -n + shellcheck clean; exercised with a mocked kubectl across 9 scenarios (happy dynamic / node-local PV / unmanaged hostPath / legacy-PV capture / wrong class / PV read error / namespace-scoped partial / hostPath mode / never-Bound) — verdicts as designed
  • scripts/tests/check-drift.sh: no drift (run only, not modified)

Deliberately left for follow-ups (tracked in backend#1184)

  • The live k3d verification run (§8.4) — runbook shipped, execution + recorded result pending
  • CLI surfacing of the aggregated sealed/unsealed verdict (status: surface environment conformance (seal check) result cli#393)
  • ~/.tracebloc host-tree check — not observable in-cluster; CLI/installer offboard-verify lineage (cli#389)
  • e2e-harness wiring; precise RFC §8.3 matrix fill in the RFC itself
  • Pre-existing (untouched): helm template --set hostPath=null crashes on develop too (shared-images-pvc.yaml unguarded deref) — orthogonal to this PR

Part of tracebloc/backend#1184

🤖 Generated with Claude Code


Note

Low Risk
Changes are confined to optional helm test hooks and docs; install/upgrade paths are unchanged. The storage check may create cluster-scoped PV read RBAC only when clusterScope=true, matching existing chart RBAC patterns.

Overview
Introduces the seal-check conformance suite for helm test: a stable label contract so tooling (e.g. tracebloc CLI) can enumerate checks without hardcoding job names, plus a new storage-assertions hook and documentation.

A shared tracebloc.sealCheckLabels helper applies tracebloc.io/seal-check / tracebloc.io/seal-check-name on hook Jobs and pod templates for egress-enforcement, backend-reachability, and the new storage-assertions check. Auxiliary RBAC for storage is intentionally not labelled.

storage-assertions (default on via sealCheck.storageAssertions) is a kubectl-based test hook that asserts release PVCs are Bound, on the expected StorageClass, and—in dynamic PVC mode—are not backed by unmanaged hostPath (with legacy chart PV name detection, node-local path allowlists, and degraded PV inspection when clusterScope=false). It ships least-privilege test-hook SA/RBAC and follows the “unverified ⇒ fail loudly” philosophy documented in docs/SEAL-CHECK.md, with a cross-link from docs/SECURITY.md.

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

…0003 S8.2 / backend#1184)

Establish the seal-check enumeration contract and fill the storage gap in
the chart conformance suite:

- Label every conformance helm-test Job (and its pods) with
  tracebloc.io/seal-check="true" + tracebloc.io/seal-check-name=<check> --
  the contract the tracebloc CLI consumes (cli#393). Applied to the
  existing egress-enforcement probe and backend-reachability check via a
  new tracebloc.sealCheckLabels helper.
- New storage-assertions helm-test Job (sealCheck.storageAssertions.*,
  default on, fully nil-guarded for --reuse-values upgrades): release PVCs
  Bound on the expected StorageClass; in dynamic-PVC mode
  (hostPath.enabled=false) no release PVC backed by an unmanaged hostPath
  PV -- catches the RFC-0003 D3/D4 leftover-PV claimRef capture. k3s
  local-path node-local paths tolerated and reported (Option C);
  clusterScope=false degrades the PV scan to a name check with an explicit
  WARNING, never silently.
- Least-privilege SA/RBAC as negative-weight test hooks (PVC read in the
  namespace; PV read only when clusterScope allows a ClusterRole).
- docs/SEAL-CHECK.md: label contract, coverage today, UNSEALED philosophy,
  per-substrate coverage view, and the k3d/k3s NetworkPolicy egress
  verification runbook (documented honestly as not yet executed).
- 20 new helm-unittest cases; 287/287 pass, helm lint --strict clean on
  all four platform values, check-drift clean.

Part of tracebloc/backend#1184

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@LukasWodka
LukasWodka requested a review from saadqbal as a code owner July 22, 2026 17:00
@LukasWodka LukasWodka self-assigned this Jul 22, 2026
@LukasWodka
LukasWodka requested a review from aptracebloc July 22, 2026 17:00
@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread client/templates/storage-assertions-check.yaml
@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread client/templates/storage-assertions-check.yaml
@LukasWodka
LukasWodka requested review from saqlainsyed007 and removed request for aptracebloc July 23, 2026 06:24
… WARNING after legacy FAIL

Two Bugbot findings on the storage-assertions check (PR #378):

- The pv-hostpath allowlist matched entries with a bare "$prefix"* glob,
  and the default /opt/local-path-provisioner entry carries no trailing
  slash, so sibling trees like /opt/local-path-provisioner-evil/... were
  accepted as node-local and could pass the storage seal. Entries are now
  normalized (trailing slash stripped) and matched on whole path
  segments: a prefix admits exactly itself or paths under it, never
  siblings. The default entry in values.yaml / template dig fallback /
  schema / docs is normalized to the trailing-slash style; either entry
  style now behaves identically.

- With clusterScope=false (no PV read), a claim captured by a chart
  legacy hostPath PV printed FAIL and then still printed the PARTIAL
  verification WARNING claiming the claim was "verified not bound" to a
  legacy PV. The WARNING is now suppressed for a claim whose legacy-name
  check just FAILed - the FAIL alone is the honest verdict there; claims
  that pass the name check keep the WARNING unchanged.

Validated: helm lint --strict on the 4 CI platform values; helm template
(near-default / node-local / flag off / sealCheck: null); helm unittest
289/289 (2 new cases pin both guards); sh -n + shellcheck on the rendered
script; mocked-kubectl runs covering sibling-path FAIL (both entry
styles), exact-prefix-root OK, legacy FAIL without WARNING, no-legacy
WARNING x3, unmanaged-tree FAIL, PV-read-error FAIL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 8c37038. Configure here.

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