Scan container images for misplaced secrets
Maintainer thường phản hồi trong vòng 1 ngày
@khuswant18 đang làm issue này rồi.
Từ ngày 27/9/2026.
Đánh giá
Issue này chưa được đánh giá.
Mô tả
Summary
The ROADMAP lists "Scanning for misplaced secrets in container images (Monitoring phase)" under Planning, but I couldn't find a tracking issue for it. This issue proposes an approach, shares some measurements from a prototype, and asks a few design questions. If the direction looks right, I'll write a design doc in designs-and-proposals before any implementation.
Background
- kubescape/kubevuln#206 (@slashben, Jan 2024) prototyped regex-based detection in kubevuln. @matthyx commented: "this will be implemented in node-agent".
- kubescape/kubevuln#296 (Dive + TruffleHog) was closed. TruffleHog is AGPL-3.0, the PR pulled the image a second time, and it stored raw secret values.
- node-agent already has every image layer on disk (overlay
lowerdir) when it builds SBOMs, so scanning here needs no registry pull and no credentials.
Problem
Credentials often end up baked into images: a copied .env, an SSH key used during the build, or ENV API_TOKEN=... in a Dockerfile. Anyone who can pull the image can read them, and deleting the file in a later layer doesn't remove it, because the earlier layer still contains it.
Kubescape doesn't detect any of this today:
- C-0012 (Applications credentials in configuration files) only checks pod-spec env vars and ConfigMaps. It never looks inside the image.
- The SBOM pipeline already reads every file in every image layer, but only to catalogue packages.
This proposal follows @matthyx's direction (node-agent) and builds on the #206 rule set.
Why node-agent
With nodeSbomGeneration enabled (the chart default), node-agent already has the image layers on disk when it builds the SBOM:
getMountedVolumesreads the container's overlaylowerdirlist, which is one directory per image layer.NodeResolverexposes those layers to Syft, and each file location carries its layer diff-ID.
So a secret scan here needs no registry pull, no pull credentials, and no second copy of the image. It's one extra pass over files node-agent already reads.
Measurements
I wrote a throwaway spike that walks every layer of an image, runs the #206 rules on regular text files (≤10 MB, NUL-byte binaries skipped) and prints only rule/path/line (never values). I ran it once more with the gitleaks v8 library (default config) for comparison.
| Image | Text scanned | #206 rules | #206 rules + keyword prefilter | gitleaks defaults |
|---|---|---|---|---|
| python:3.12-slim | 33 MB | 10.5 s, 0 hits | — | 12.6 s, 0 hits |
| nginx:1.27 | 15 MB | 4.7 s, 0 hits | — | 6.3 s, 0 hits |
| node:22-slim | 36 MB | 12.0 s, 3 hits | 3.0 s, 3 hits | 30.7 s, 10 hits |
| postgres:16 | 66 MB | 22.6 s, 1 hit | 4.7 s, 1 hit | 22.9 s, 2 hits |
| golang:1.23-alpine | 108 MB | 34.9 s, 4 hits | 5.2 s, 4 hits | 40.7 s, 61 hits |
| quay.io/kubescape/node-agent | 1 MB | 0.4 s, 0 hits | — | — |
Observations:
- No hit was a real leak. Every hit was a false positive (npm docs, Go stdlib test keys, entropy matches in source) or intentional (
/etc/ssl/private/ssl-cert-snakeoil.keyfrom Debian'sssl-certpackage). False-positive control has to be part of the core design, not a later add-on. - Regex is the whole cost. Reading the files takes under 0.6 s even for the Go image. A simple per-rule keyword check before running each regex gave identical findings 4–7× faster.
- gitleaks' defaults are tuned for git repositories. Its
generic-api-keyrule produced 57 of the 61 hits on the Go image.
I also built a small fixture image with crane that has a private key added in one layer and whited-out in the next, a .env overwritten in a later layer, a directory hidden by an opaque whiteout, and ENV GH_TOKEN=ghp_... in the image config.
- The layer-aware scan found the removed key, the overwritten
.envand the hidden file, and labelled them as not visible at runtime. - It missed the
ENVtoken, because image config isn't a file. This is a separate, cheap check.
How the spike ran
- Images were pulled with
syft.GetSource(kubevuln's registry path), linux/amd64, on a laptop. node-agent would read the same layers from overlay directories instead, so pull time isn't relevant there. The "walk" times above exclude the pull. - Layer visibility was taken from Syft's
SquashedScopevsAllLayersScope. - Happy to share the spike code if useful. It's a measurement tool, not a proposed implementation.
Proposal (phase 1)
- Detector package (
secretscan): a rule set with per-rule keywords (prefilter), path allowlists for known test/doc locations, stop-words for documented example values (e.g.AKIAIOSFODNN7EXAMPLE), size and binary limits, and streaming reads. - Image config scan:
Env,History[].created_byandLabels. node-agent already has these from the CRIImageStatus. - node-agent integration next to
SbomManager(in-process and sidecar):- layer-aware visibility: "visible at runtime" vs "only in an earlier layer"
- its own scanner-version key, so images that already have an SBOM still get scanned, and rules can be updated later
- CPU, byte and time budgets; a partial scan is reported as incomplete, never as clean
- Storage: findings stored per image (like the SBOM, once per image rather than per node).
- Exceptions: a
spec.secrets[]section inSecurityExceptionso users can accept known findings such as the snakeoil key.match.imagesalready covers image selection. - Operations: a Helm capability flag (off by default at first) and metrics (files scanned, files skipped as binary or too large, findings by rule, duration).
Findings never contain the secret value, not even a partial one. Only rule, path, layer, line and a fingerprint that can't be reversed. A plain hash of a weak password can be brute-forced, so the fingerprint should be either rule + path + layer + line, or an HMAC with a per-cluster key.
Illustrative finding (the shape is up for discussion):
- ruleID: github-pat
severity: Critical
source: file # file | config.env | config.history | config.label
path: /app/config/settings.py
line: 12
layer: sha256:ed87f7… # diff-ID of the layer that contains it
visibleAtRuntime: false # removed or overwritten by a later layer
fingerprint: 3f9c… # not derived from the value alone
Edge cases the design needs to cover
Layers
- A file deleted by a whiteout, overwritten by a later layer, or hidden by an opaque directory: still reported, marked not visible at runtime.
- On the node, whiteouts are 0/0 character devices and opaque directories are marked with
trusted.overlay.opaque/user.overlay.opaquexattrs. Nothing in node-agent handles these today, so visibility has to be computed explicitly.NodeResolver.AllLocationsdoes return files from every layer, which is what the scan needs. - The same file in several layers: one finding with a list of layers.
- Symlinks and hardlinks: scan regular files only, and don't count them twice.
Content
- Binaries, files over the size limit, archives (
.tgz,.jar,.whl): skipped in phase 1 and counted in metrics. - Documented example values, runtime test data (
/usr/local/go/src/**/testdata, Pythontests/) and CA bundles and public certificates: allowlisted or never matched. The path allowlist must be configurable, because users' own applications can also live under paths likesite-packages. - Intentional keys such as
ssl-cert-snakeoil.key: reported, but can be accepted withSecurityException.
Node and runtime
- Without overlayfs, node-agent falls back to
/proc/<pid>/root, which is the merged live filesystem. It has no layer history and likely includes the container's mounted volumes (Secret volumes, service-account tokens). Syft's mount skipping uses node-agent's own mount table, not the container's. I haven't verified this on a ZFS/btrfs node yet (see question 4). - The scan must stay within node-agent's resource budget and must not delay SBOM generation.
Test plan
- A fixture image built in CI with
crane, with known answers for each case above: whiteout, overwrite, opaque directory, configENV, test-fixture path, binary, large file, symlink. - Unit tests for each rule (true and false examples), the prefilter and the visibility logic, including whiteout and opaque cases on a directory tree that mimics overlay lowerdirs.
- A false-positive check against a few popular images (like the table above) to catch rule regressions.
- A node-agent component test on kind: deploy the fixture image, then assert the stored findings and that no secret value appears in storage, logs or metrics.
Later (not phase 1)
- Runtime relevancy: join findings with
ContainerProfile.spec.opensto flag secrets that the container actually reads, similar to how relevancy works for CVEs. - Reuse the package in kubevuln (registry scans) and
kubescape scan image. - prometheus-exporter counters.
Out of scope: archives and binaries, host filesystem scanning, verifying secrets against live APIs, and secret rotation or revocation.
Questions
- Should the detector live only in node-agent, or in a shared module so kubevuln and the CLI can reuse it later?
- Storage: a new kind, or an extension of an existing per-image object?
- Rules: evolve the #206 set (Apache-2.0-compatible, no new dependencies), or use the gitleaks library (MIT, but it brings a wasm-based RE2 dependency and needed image-specific tuning)?
- On nodes without overlayfs (
/proc/<pid>/rootfallback): skip secret scanning, or filter out the container's mounts? - Default on or off for the first release?
Next steps
If this direction makes sense, I'll write the design doc in designs-and-proposals based on the answers here, and I'm happy to take on the implementation afterwards. I can also present the measurements at a community meeting.
- Ngôn ngữ chính
- Go
- Star
- 39
- Fork
- 42
- Merge trung bình
- 1 ngày 18 giờ
- Pull request đã merge (30 ngày)
- 40
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của kubescape/node-agent
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 72/100
kubescape/node-agent#1099 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
kubescape/node-agent#1093 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Upgrade Inspektor Gadget to v0.57.0 via kubescape fork and preserve exec/path semanticsCó thể đã có người làm @ANAMASGARD đã nhận 18 ngày trước. Đang mởenhancement
kubescape/node-agent#1050 · 2 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
refactor: consider migrating from hashicorp/golang-lru/v2/expirable to jellydator/ttlcache/v3Đang mở
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
kubescape/node-agent#1014 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 55/100
kubescape/node-agent#963 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của kubescape/node-agent
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
-
evener-transcript-v2-upgrade tool still scans transcript files through bufio's 4 KiB default bufferĐang mở
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 78/100
prime-radiant-inc/evener#6370 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
ManageSieve proxy: a backend's NO/BYE greeting is skipped as a capability line and reported as EOFĐang mởbug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Mend: dependency security vulnerability
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
-
Mend: dependency security vulnerability
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
ManageIQ/manageiq-pods#1476 ·
Maintainer thường phản hồi trong vòng 1 ngày