Task · SKT-0012.03

skcapture: stop the zero-secret default leaking through annotations

Description

Found by the SKT-0012 validation run, 2026-08-27. Latent on the lab cluster, not triggered there — which is the reason to fix it rather than to deprioritise it.

--include-secret-data and --include-configmap-data both default false, and that half genuinely holds: a capture of the lab cluster contained no Secret or ConfigMap values, and a scan for credential-shaped strings found only a Helm config checksum.

But internal/capture/k8s.go:556 copies item.Metadata.Annotations wholesale, and there is no annotation filter anywhere in internal/capture. On any cluster managed with kubectl apply, kubectl.kubernetes.io/last-applied-configuration carries the full serialised object spec — for a Deployment that includes every container env var value, which routinely carries credentials in exactly the deployments a user would want to capture.

The lab cluster is ArgoCD and Helm managed, so it does not carry that annotation. The zero-secret claim is therefore currently untested against precisely the case that would break it, and the tool documentation and RBAC design both present that claim as though it were established.

kubectl.kubernetes.io/last-applied-configuration is the known offender but should not be the whole fix — a denylist of one name fails the next annotation that embeds a spec. An allowlist of the annotation keys the mapper actually consumes is the shape that holds, since the consumer set is small and known (Helm release name, ArgoCD tracking, deployment revision, scrape hints).

Acceptance Criteria

Definition of Done

Implementation Notes

FIXED 2026-08-27 (lane L10), each with a failing test first and a live before/after against the EKS lab cluster.

Annotations now pass through filterAnnotations against an explicit allowlist of literal keys. A test fails the build if anyone turns an entry into a prefix or glob.

ROOT DECISION on allowlist scope: keep all four key groups (Helm release identity, ArgoCD tracking-id, deployment revision, and the two scrape-hint families). The lane offered cutting to the two meta.helm.sh keys, since those are the only ones anything in-tree actually reads. Rejected: on an ArgoCD-managed cluster the tracking-id is the ONLY ownership annotation — 49 lab workloads carry it against 11 with Helm annotations — so cutting it would lose ownership detection on exactly the clusters most likely to be captured. Every kept key is a bounded scalar that cannot embed a spec.

CORRECTION TO THIS TASK PREMISE: it said to find the consumer set by reading internal/forge. internal/forge and cmd/skforge read NO annotations at all — the one hit is a string literal in a test fixture. The sole in-tree consumer is detectAddons in the capture package itself.

LIVE: dropped keys include the config checksum that the original validation run flagged as the one credential-shaped string, a daemonset generation annotation, and ten vendor language-detection annotations. Addon detection is unaffected — 9 addons before and after. No unallowlisted key remains.

THE CASE THE LAB CLUSTER CANNOT PROVE was closed two other ways. The cluster carries ZERO kubectl.kubernetes.io/last-applied-configuration annotations, exactly as this task predicted, so live traffic cannot exercise the defect. So: a test asserts against the SERIALISED WIRE FORM rather than the struct, since the wire form is what leaves the cluster; and an end-to-end run at the BINARY level with a stub kubectl replaying a real kubectl-apply-managed Deployment whose last-applied-configuration embeds a container env value confirmed the credential is present in the before output and absent in the after.

Final Summary

Annotations pass through an explicit allowlist of literal keys, with a test that fails the build if anyone turns an entry into a prefix or glob.

The root kept all four key groups rather than cutting to the two the code actually reads: on an ArgoCD-managed cluster the tracking-id is the ONLY ownership annotation — 49 lab workloads against 11 with Helm annotations — so cutting it would lose ownership detection on exactly the clusters most likely to be captured. Every kept key is a bounded scalar that cannot embed a spec.

The case the lab cluster cannot prove was closed two other ways, because the cluster carries ZERO last-applied-configuration annotations exactly as predicted: a test asserts against the SERIALISED WIRE FORM rather than the struct, since the wire form is what leaves the cluster, and an end-to-end binary run with a stub kubectl replaying a real kubectl-apply-managed Deployment carrying a credential in an env var confirmed it present before and absent after.

Live: the dropped keys include the config checksum the original validation run flagged as its one credential-shaped string. Addon detection is unaffected. A correction worth keeping: internal/forge reads no annotations at all — the sole in-tree consumer is the capture package itself.

View the source file on GitHub