image-scan
Scan a Docker/OCI container image for secrets and credentials baked into its filesystem layers.
greengate image-scan <image> [OPTIONS]Why scan image layers?
Build pipelines often copy secrets into images inadvertently:
COPY .env /app/— the.envfile (with itsDATABASE_URL,STRIPE_SECRET_KEY, etc.) is now in the image.ARGvalues written to config files —ARG API_KEYfollowed byRUN echo "key=$API_KEY" > /etc/app/configbakes the key into a layer.- Build tools that write credentials to
~/.aws/credentialsor~/.ssh/during the build stage and are thenCOPY-ed into the final image. ENVvariables set in aDockerfilelayer — they persist in the image manifest and are readable by anyone with image access.
These secrets never touch your source repository, so git history scanning won't find them. image-scan catches them at the layer level.
How it works
docker save <image> -o /tmp/greengate-image-<nanos>.tar
│
▼
Unpack outer tar → manifest.json + <hash>/layer.tar files
│
▼ (per layer)
Unpack layer.tar → full filesystem snapshot
│
▼
Walk all text files (skip binary, > 1 MB, whiteout files)
│
▼
Run 26 secret patterns + Shannon entropy scan
│
▼
Deduplicate across layers → emit findings
│
▼
RAII cleanup — temp directory removed on exitFindings include the layer digest and path inside the layer:
nginx:latest::layer:a1b2c3d4e5f6::etc/environment:3 HIGH AWS_ACCESS_KEY_IDOptions
| Flag | Default | Description |
|---|---|---|
<image> | (required) | Image reference: nginx:latest, ghcr.io/org/app:sha-abc123, etc. |
--format | text | Output format: text, json, sarif, junit, gitlab |
--no-fail | false | Report findings but exit 0 (audit mode) |
--profile | (none) | Apply a preset: strict, relaxed, ci |
Sensitive path promotion
Files in paths that commonly contain credentials have their finding severity automatically promoted one level (medium → high):
| Path pattern | Common contents |
|---|---|
*.env, .env.* | Application secrets, API keys |
.aws/credentials | AWS access keys |
.ssh/ | SSH private keys |
etc/environment | System-wide environment variables |
*.pem, *.key, *.p12, *.pfx | TLS/code signing certificates |
docker-compose*.yml | Service credentials, bind mounts |
Examples
# Scan a public image
greengate image-scan nginx:latest
# Scan a private registry image
greengate image-scan ghcr.io/my-org/my-app:sha-abc123
# JSON output for downstream tooling
greengate image-scan my-app:v1.2.3 --format json
# Audit mode — report but don't fail
greengate image-scan my-app:v1.2.3 --no-fail
# Strict profile — lower entropy threshold, more patterns
greengate image-scan my-app:v1.2.3 --profile strictGitHub Actions
Scan the built image before pushing it to the registry:
jobs:
build-and-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download greengate
run: |
curl -sSfL https://github.com/thinkgrid-labs/greengate/releases/latest/download/greengate-linux-amd64 \
-o /usr/local/bin/greengate && chmod +x /usr/local/bin/greengate
- name: Build image
run: docker build -t my-app:${{ github.sha }} .
- name: Scan image layers for secrets
run: greengate image-scan my-app:${{ github.sha }}
- name: Push image
if: success()
run: docker push my-app:${{ github.sha }}The if: success() guard ensures the image is only pushed when the scan passes.
SARIF output for Code Scanning
- name: Scan image (SARIF)
run: greengate image-scan my-app:${{ github.sha }} --format sarif > image-scan.sarif
continue-on-error: true
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: image-scan.sarifRequirements
- Docker must be running and
dockermust be inPATH. greengate prints an error with installation instructions if Docker is not found. - The image must be pullable by the Docker daemon — either already cached locally or accessible from a configured registry.
- No root or elevated privileges required.
Performance
Image scanning time is proportional to image size and the number of layers. A typical 200 MB application image (10–15 layers) scans in 5–15 seconds on modern hardware. Base images like alpine or distroless scan in under 2 seconds.
The extracted layer data is written to a temporary directory and removed automatically when the command exits — even if the scan fails mid-way.
Deduplication
OCI images use a layered filesystem: unchanged files from earlier layers are copied forward into later layers. Without deduplication, a secret in /app/.env that was present since layer 2 would be reported once per remaining layer.
greengate deduplicates findings by (path, rule_id, line_number) before reporting, so each unique finding is shown exactly once regardless of how many layers it appears in.
Skipped files
The following are silently skipped to keep scan output noise-free:
| Skip condition | Reason |
|---|---|
| Files > 1 MB | Likely build artifacts, compiled binaries, or data files |
| Binary files (contain null bytes) | Not text — can't contain inline secrets |
Whiteout files (.wh. prefix) | OCI layer deletion markers, not real files |
| Symlinks | Followed by the layer extraction tool; skipped at scan time to avoid cycles |