To scan a container image in CI before it is pushed, add one shell step right after docker build that runs safeguard ci scan --image <ref> --environment <name> --wait --fail-on critical. The step reads the image from the runner's local Docker daemon, sends only the results to Safeguard, and fails the job before your push step ever runs. Rescan at deploy time, and deploy the digest you scanned.
This post is a pipeline recipe. Every Safeguard command and YAML snippet below is copied from Safeguard's docs as fetched on 25 October 2026. We did not run these snippets in a live pipeline for this post, so test them on a branch first. Where we add a plain Docker or shell line of our own, such as the push step, we say so.
Key takeaways
- Scan before push, not after. NIST SP 800-204D says a container image should be scanned for vulnerabilities as soon as it is built, even before it is pushed to a registry. The Safeguard CI step does exactly that when it runs on the machine that built the image.
- One step, no plugin. Container image scanning in CI runs the Safeguard CLI (GA) in a plain shell step. Container image scanning is part of Safeguard SCA (GA). The docs say there is no plugin, Action or Pipe to install.
- Two ways to fail the job.
--fail-on <severity>fails on a dependency vulnerability at or above a severity.--fail-on-gate <gate>fails when a Safeguard policy gate is not Passed or Warning. Policies and gates (GA) use three actions: Block, Warn and Notify, with approvals handled through exceptions. - One version per environment. Each
--environmentvalue records its own version, such asprod-1.4.2, so you can fail only where it matters. - Rescan at deploy time. Vulnerability data changes after the build. Run the step again in the deploy job with the production environment name, and keep a registry integration (part of ESSCM, GA) for continuous scanning of what is already out there.
- Promote by digest. Tags move. The digest of the image you scanned is the only reference that proves you deployed the same bytes.
- Signing is yours for now. Attestation and signing is GA in Safeguard, but its docs say there is no CLI command for signing container images yet.
- Fixes come from the repository. Autonomous remediation (GA) can open fix pull requests for application dependencies inside the image. It ships switched off, with every category set to Manual.
Why scan the image before it is pushed?
A container registry is a distribution point. The moment an image lands there, other pipelines, other clusters and other teams can pull it. A scan that runs after the push is a report about something that is already available to deploy. A scan that runs before the push is a decision about whether it should exist in the registry at all.
Three things go wrong when the scan comes late.
Unscanned images get pulled. A nightly registry scan or a scan triggered by the push event races the first deploy. If the deploy wins, an image with a known critical vulnerability is running before anyone has looked at it.
Tags get reused. A team pushes api:1.4.2, the scan fails, someone rebuilds and pushes api:1.4.2 again. Anything that pulled in the gap now runs a different image under the same name. Kubernetes' own docs note that tags can be moved to point to different images, while digests are fixed.
The decision moves away from the person who can fix it. A failed build job lands on the engineer who changed the Dockerfile, with the commit attached. A red registry dashboard a day later means someone has to work backwards.
What NIST SP 800-204D asks for at the deploy step
NIST SP 800-204D, "Strategies for the Integration of Software Supply Chain Security in DevSecOps CI/CD Pipelines" (February 2024), is the most direct federal guidance on this. Section 5.2, on securing workflows in CD pipelines, lists deploy requirements. Two of them describe this post almost word for word:
- DEPLOY-REQ-4 asks for evidence that the container image was scanned for vulnerabilities, and points out that the time of the scan matters, because scanning tools keep learning about new vulnerabilities and a recent result is more reliable than an old one. It also says it should be possible to allow or block image deployment based on organisation defined policies.
- DEPLOY-REQ-5 includes the task that a container image "should be scanned for vulnerabilities as soon as it is built", even before it is pushed to a registry, and notes that the same early scan can be built into local workflows.
Read together, these give you the shape of the pipeline: scan at build, block on policy, scan again close to deploy.
What NIST SP 800-190 says about quality gates
NIST SP 800-190, the Application Container Security Guide (September 2017), is older but still the reference most container policies cite. Section 4.1.1, on image vulnerabilities, asks for tools that understand the pipeline based build and the immutable nature of images. It lists three properties of good tooling: integration across the whole image lifecycle from build to registry to runtime, visibility into vulnerabilities at every layer and not just the base, and policy driven enforcement through "quality gates" at each stage of build and deployment. Its example is a build rule that stops images with vulnerabilities above a chosen CVSS threshold from progressing.
Section 4.1.5, on untrusted images, adds that organisations should keep a set of trusted images and registries and allow only those to run. A pre-push gate is how you keep the trusted registry trusted.
Where the SSDF fits
NIST SP 800-218, the Secure Software Development Framework (SSDF) version 1.1 (February 2022), is not container specific, but three tasks map onto an image scan in CI:
- PW.4.4 asks you to verify that acquired open source and third party components meet your requirements throughout their life cycle. Its examples include building automatic detection of known vulnerabilities into the toolchain. Your base image and every package in it are acquired components.
- PO.4.2 asks for processes and mechanisms that gather the information your security checks depend on. One example is to automate decision making using those criteria. A CI gate is an automated decision.
- PS.3.2 asks you to collect and share provenance data for the components of each release, for example in an SBOM. An image scan produces an SBOM of what is actually in the image.
The OWASP CI/CD risks this touches
Three of the OWASP Top 10 CI/CD Security Risks apply directly to how you wire a scan step:
- CICD-SEC-1, Insufficient Flow Control Mechanisms. If nothing stops an image from moving from build to registry to production without a check, that is this risk. A failing scan step that runs before push is a flow control.
- CICD-SEC-6, Insufficient Credential Hygiene. The scan step needs an API key, and possibly registry credentials. How you scope and store them matters.
- CICD-SEC-9, Improper Artifact Integrity Validation. Scanning one image and deploying another under the same tag is exactly the gap this category describes.
What the pipeline should look like
This is the stage order we recommend; the rest of the post fills in each stage.
| Stage | What happens | Safeguard part |
|---|---|---|
| 1. Build | docker build produces an image in the runner's local daemon | None |
| 2. Scan | safeguard ci scan --image reads the local image, records a version, waits for results | CLI (GA), SCA (GA) |
| 3. Gate | The same step exits non-zero on your severity threshold or a failed policy gate | Policies and gates (GA) |
| 4. Push | Your own docker push, only if stage 3 passed; record the digest | None |
| 5. Deploy | Deploy by digest; rescan with the production environment name first | CLI (GA) |
| 6. Watch | A registry integration keeps scanning the image after it ships | Registry integrations, part of ESSCM (GA) |
Stages 2 and 3 are one command: the scan and the decision happen together, and the exit code is the decision.
How the Safeguard CI image scan works
Safeguard's "Container images" page under CI/CD Integration describes one step that you add after your pipeline builds an image. It scans that image on the runner and records it in Safeguard under the image's repository, for example ghcr.io/acme/api, with one version per environment and tag, such as prod-1.4.2.
The core command from the docs is:
safeguard ci scan --image "ghcr.io/acme/api:$TAG" --environment prod --wait --fail-on critical
Where the image is read from
The docs give two sources:
- The local Docker daemon, when the step runs on the machine that built the image. There is nothing to set up. This is the before-push case.
- A registry, otherwise. You set
SAFEGUARD_REGISTRY_USERNAMEandSAFEGUARD_REGISTRY_PASSWORDas CI secrets so the scan can pull the image. On GitLab's own registry, the docs say to use$CI_REGISTRY_USERand$CI_REGISTRY_PASSWORD.
The docs are explicit that only the results leave the runner, and that no repository integration is needed for an image scan.
This has a practical consequence. If your build and your scan run in different jobs on different runners, the second job cannot see the first job's local daemon, so the scan has to pull from a registry. That means the image has already been pushed somewhere. If you want a strict before-push gate, run the scan in the same job as the build. If you cannot, push first to a separate staging repository that nothing deploys from, scan there, and only copy to the release repository on a pass.
How versions are named
Each environment's build is its own version in Safeguard. The docs give this mapping:
| Image | --environment | Version in Safeguard |
|---|---|---|
ghcr.io/acme/api:1.4.2 | prod | prod-1.4.2 |
ghcr.io/acme/api:1.4.2 | staging | staging-1.4.2 |
ghcr.io/acme/api:latest | dev | dev-<commit> |
A latest tag or a missing tag uses the build's commit instead. Environment names can be dev, staging, prod or any name made of letters, digits and dashes. Anything else returns BAD_ENVIRONMENT.
If you tag images with the commit SHA, as the docs' GitHub Actions example does, the version name carries the SHA too. That makes it easy to go from a version in Safeguard back to the exact commit.
The flags
These are the flags the container images page lists, plus the gate flag from the main CI/CD Integration page:
| Flag | What it does |
|---|---|
--image <ref> | The image to scan. Implies an on-runner scan. |
--environment <name> | The environment this build is for; names the version. |
--wait | Waits for the scan; fails if the scan fails or runs past --timeout (default 30m). |
--fail-on <severity> | With --wait, fails on a dependency vulnerability at or above critical, high, medium or low. |
--fail-on-gate <gate> | With --wait, fails when a policy gate (its name, id, or all) is not Passed or Warning. |
--project <name> | Records the image under another project instead of the image repository. |
Two notes from the docs that matter for image scans. First, --image and --path are two different targets; passing both returns an error telling you to pick one. Second, on the main CI page the docs say SAST findings are scanned and shown in Safeguard but are not part of --fail-on yet. That matters if you share one pipeline file between code and image steps.
The secret key
The step authenticates with a secret key, not with a person's login. The docs say to create one with the scopes scans:trigger and scans:read. Admins create it in the Set up CI/CD sheet or under Settings › Developer; other users ask an admin. You can lock a key to one project, which is a good idea for a key that lives in a CI system.
Store it in your CI as SAFEGUARD_API_KEY. This is specific to safeguard ci scan. The CLI usage page notes that the interactive CLI has no API key variable and authorises a machine by signing in once, so do not expect SAFEGUARD_API_KEY to work for safeguard scan on a laptop.
The in-app shortcut
In the Safeguard app, each container registry card under Integrations (the docs list Amazon ECR, Docker Hub, Azure Container Registry, Google Artifact Registry, Harbor, JFrog, Nexus, Quay and others) has a Set up CI/CD button. You pick your CI system, the image and the environments, and it gives you the steps.
Step by step: GitHub Actions, build then scan then push
This is the most common case, so it gets the full walkthrough. The build and scan steps are from Safeguard's container images page. The push step is ours, and is marked as such.
-
Create the secret key. In Safeguard, go to Settings › Developer and create a key with
scans:triggerandscans:read. Lock it to the project if you already have one; the first run creates the project otherwise. -
Add it to GitHub. Under Settings › Secrets and variables › Actions, add a repository secret named
SAFEGUARD_API_KEY. An organisation secret works too, according to the GitHub Actions guide. -
Add the build and scan steps. From the docs:
- name: Build
run: docker build -t "ghcr.io/acme/api:${{ github.sha }}" .
- name: Safeguard image scan (prod)
env:
SAFEGUARD_API_KEY: ${{ secrets.SAFEGUARD_API_KEY }}
SAFEGUARD_INSTALL_DIR: ${{ runner.temp }}/safeguard-bin
IMAGE: ghcr.io/acme/api:${{ github.sha }}
run: |
set -eu
curl -fsSL https://cli.safeguard.sh/install | bash
"$SAFEGUARD_INSTALL_DIR/safeguard" ci scan --image "$IMAGE" --environment prod --wait --fail-on critical
- Add your push step after the scan. This is not from Safeguard's docs; it is an ordinary Docker push. Because GitHub Actions stops a job at the first failing step by default, a failed scan means this step never runs.
# Our addition, not from Safeguard docs. Runs only if the scan step passed.
- name: Push
run: docker push "ghcr.io/acme/api:${{ github.sha }}"
You will also need a registry login step before the push. Use whatever your registry's own docs recommend.
-
Record the digest. Capture the pushed image's digest in your job output so the deploy job receives a digest, not a tag.
-
Push a commit and read the run. The GitHub Actions guide says the run shows the scan's result and a link to it in Safeguard. If the image has a critical vulnerability, the scan step exits non-zero, the job stops, and the push step is skipped.
-
Decide what happens on pull requests. Scanning pull request builds is a useful early warning, but you probably do not want to push from them. Run build and scan on both, and run the push step only for pushes to
main.
A note on fork pull requests. GitHub's own docs say secrets are not passed to the runner when a workflow is triggered from a forked repository, so the scan cannot authenticate. Safeguard's GitHub Actions guide says the same, and warns that pull_request_target runs with your secrets, so you must never run the fork's code in that job. Building a fork's Dockerfile is running the fork's code. Scan pushes only, or scan fork contributions after a maintainer has merged them to a branch you control.
What changes for staging and development
Copy the scan step and change two things: the step name and --environment. Then choose a threshold per environment. The docs' own suggestion is --fail-on critical in production and no --fail-on in development. Dropping --fail-on makes the step report without failing, which is how you see what a stricter gate would catch before you turn it on.
safeguard ci scan --image "$IMAGE" --environment dev --wait
safeguard ci scan --image "$IMAGE" --environment staging --wait --fail-on high
safeguard ci scan --image "$IMAGE" --environment prod --wait --fail-on critical
These three lines use only flags from the docs, but the combination of thresholds is our example, not a recommendation from Safeguard. Pick thresholds from your own risk policy.
The same flow on GitLab, Jenkins, Azure Pipelines and Bitbucket
Safeguard's container images page tells you to store the key the same way as for code scans and to add the step after the image is built. Each CI system's guide shows how to install the CLI and run safeguard ci scan for code. For an image, you keep the install pattern from the guide and add --image and --environment to the command. We have not run these combinations; they are assembled from the documented install lines and the documented image flags.
GitLab CI/CD
GitLab's guide says to add SAFEGUARD_API_KEY under Settings › CI/CD › Variables with masking on, and to leave protection off if unprotected branches or merge requests should be scanned. GitLab's own docs explain why: protected variables are only passed to pipelines that run on protected branches or protected tags. The guide also offers an include template; this post uses the plain job it shows "without the template", so you can see every line.
The documented job, for code:
safeguard:
stage: test
image: alpine:3.20
variables:
SAFEGUARD_INSTALL_DIR: /usr/local/bin
before_script:
- apk add --no-cache bash curl ca-certificates
- curl -fsSL https://cli.safeguard.sh/install | bash
script:
- safeguard ci scan --wait --fail-on critical
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
For an image, two things change. The command becomes safeguard ci scan --image "$IMAGE" --environment prod --wait --fail-on critical. And the job has to be able to see the image. In GitLab, build and scan usually run in different jobs, so the scan job will not share the build job's daemon. Either install the CLI and scan inside the build job, right after docker build, or let the scan pull from GitLab's registry using $CI_REGISTRY_USER and $CI_REGISTRY_PASSWORD as the registry credentials, which the docs name for exactly this case. If you take the registry route, push to a repository or tag naming scheme that your deploy jobs ignore until the scan passes.
Jenkins
The Jenkins guide stores the key as a Secret text credential with the ID safeguard-api-key and wraps the step in withCredentials. The documented stage exports SAFEGUARD_INSTALL_DIR="$WORKSPACE/.safeguard-bin", installs the CLI with the same curl line as the other guides, and runs ci scan --wait --fail-on critical.
Run it in the stage that builds the image, or in a stage that runs on the same agent, and add --image and --environment to the scan command. The guide says the agent needs Linux or macOS with curl, and that Windows agents are not supported. The when block in the guide's full example needs a multibranch pipeline; in a single branch job, the guide says to delete it.
Azure Pipelines
The Azure Pipelines guide adds SAFEGUARD_API_KEY as a secret pipeline variable and maps it into the script through env, because secret variables only reach a script that way. The step exports SAFEGUARD_INSTALL_DIR under the agent temp directory, installs the CLI and runs ci scan --wait --fail-on critical.
Place it after your Docker build task in the same job and add the image flags. For Azure Repos, the guide notes that the pr trigger block is ignored and you add a Build validation branch policy instead.
Bitbucket Pipelines
The Bitbucket guide uses an alpine:3.20 step that installs bash, curl and ca-certificates, then runs the CLI. For an image scan, the step needs access to the image you built, which in Bitbucket usually means building and scanning in the same step with the Docker service enabled, or scanning from a registry with the registry credential variables. Merge the step into your existing file: Bitbucket allows only one definitions and one pipelines key.
Gate on policy, not only on severity
--fail-on critical is a blunt instrument. It fails on any critical dependency vulnerability in the image, whether or not it is exploited, whether or not a fix exists, and whether or not you have already accepted the risk. Most teams start there and then move to policy gates.
How gates decide
Safeguard's Policies and Gates page draws the line clearly. Policies are rules that evaluate your SBOMs and generate findings. Gates are checkpoints that enforce policies: they block deployments when policies fail, integrate with CI/CD pipelines, and require approvals for exceptions.
Each policy has one Action, which decides what a failed policy does to the gate:
| Action | When the policy fails |
|---|---|
| Block | The gate fails |
| Warn | The gate passes, with a warning naming the policy |
| Notify | The gate passes, and a notification goes to your workspace's channels |
The Policy outcomes page then sets the order in which a gate's outcome is worked out. With no policies linked, the gate is Not evaluated. If any Block policy failed, the gate is Failed. Otherwise, if any policy could not be evaluated, the gate is Error. Otherwise, if any policy has not been evaluated yet, the gate is Not evaluated. Otherwise, if any Warn policy failed, the gate is Warning and passes. Otherwise it is Passed. The docs add that an Error is never counted as a pass, and that a gate that has never been evaluated shows Not evaluated, never Passed.
What the exit code means
With --fail-on-gate <gate name or id | all>, the docs say safeguard ci scan --wait waits for the scan to finish and for the gate's policies to be evaluated on the new data, then:
- exits 0 when the gate is Passed or Warning, and prints the warnings and notified policies;
- exits non-zero when the gate is Failed, and prints the Block policies that failed;
- exits non-zero when the gate is Error or Not evaluated, or when its policies were not evaluated before
--timeout, with a message saying why.
--fail-on can be combined with --fail-on-gate. A common pattern is a severity floor that never changes plus a gate that carries the nuance:
safeguard ci scan --image "$IMAGE" --environment prod --wait --fail-on critical --fail-on-gate all
That line combines documented flags; we have not run it.
Two caveats before you rely on gates for images
First, policy outcomes are switched on per workspace by Safeguard. The docs say that if your workspace does not show an Outcome column on the Gates and Policies tables, you ask your Safeguard contact to turn it on. Until then, --fail-on-gate has nothing to evaluate.
Second, the Policies and Gates page says gates are evaluated by the platform on the scans it runs for your connected repositories. It does not say in so many words that gates evaluate on-runner image scans. Before you make a gate the only thing between an image and production, run the step on a test image with a known Block policy failure and confirm the job fails. Keep --fail-on critical in place until you have seen that happen.
Which conditions you can write
The Policies and Gates page lists conditions such as vulnerability severity, CVSS score, EPSS score, license, component age, maintenance status and SLSA level, with templates such as No Critical Vulnerabilities and No Abandoned Packages. CISA's Known Exploited Vulnerabilities catalog and EPSS are both part of Safeguard's vulnerability management (GA) as enrichment. In the policy conditions the docs list today, EPSS appears as a condition and KEV does not; treat KEV as a filter you use when triaging findings, not as a documented gate condition.
CISA calls KEV the authoritative source of vulnerabilities exploited in the wild; FIRST describes EPSS as estimating the probability a CVE is exploited in the next 30 days. Both help decide which findings should block and which should warn.
When a block is wrong: exceptions
Sometimes the gate is right about the vulnerability and wrong about your situation. The docs describe an exception workflow: a developer clicks Request Exception, selects the policy and finding, gives a business justification and an expiry date, and submits it. Approvers are notified, decide, and an audit trail is recorded. That is the approval path. There is no separate approve action in the CI step itself, and you should not build one by adding || true to the scan line.
Promote the exact bytes you scanned
A scan result is only useful if what you deploy is what you scanned.
The OCI image specification defines a content descriptor as carrying the type of the content, a digest and a size. The digest is a content identifier. The spec notes that if the digest is communicated securely, you can verify content from an untrusted source by recalculating the digest yourself. Kubernetes' documentation says the same thing from the deploy side: digests are hashes of the image's content and are immutable, while tags can be moved.
So the deploy side of your pipeline should:
- Take the digest your push produced, not the tag.
- Deploy
image@sha256:<digest>. - Keep the mapping from digest to Safeguard version (for example
prod-<sha>) in your release record.
Safeguard's Container Image Integration FAQ also recommends digests for production scanning.
There is one gap to know about. The Safeguard version name is built from the environment and the tag, not from the digest. If you reuse a tag for a different build, Safeguard will record it under a version name it has seen before. Use unique tags, such as the commit SHA, for anything you plan to deploy.
Scanning on the runner without sending anything
Some teams cannot send even scan results to a SaaS service from certain pipelines. The CI/CD Integration page documents a second route for that: safeguard scan and safeguard detect run fully on the runner and write files you can keep as build artifacts.
For images, the CLI usage page shows:
safeguard scan --image nginx:latest
and, for a private registry:
safeguard scan --image myregistry.azurecr.io/myapp:v1 \
--registry-username "$REGISTRY_USER" \
--registry-password "$REGISTRY_PASSWORD"
The usage page also says safeguard scan --image reads local images in other forms: podman:<ref>, docker-archive:<file>, oci-archive:<file> and oci-dir:<dir>. That makes a clean before-push pattern possible even when the scan runs somewhere other than the build machine. Save the built image to a tar file with Docker's own docker save -o, pass the file as a build artifact, and scan the archive:
# docker save is Docker's command; the scan line uses the docs' docker-archive form.
docker save -o api.tar "ghcr.io/acme/api:$TAG"
safeguard scan --image docker-archive:api.tar --output out/
We assembled that pair from the two docs pages; it was not run for this post.
The exit code is not a gate
This is the most important difference from ci scan. The usage page says safeguard scan does not fail on findings. It exits 0 when the scan ran, whatever it found; 1 when the command itself failed; and 2 when results were written but the vulnerability scan did not actually run, so a script never mistakes that for a clean pass. There is no severity threshold flag.
To gate on these files, the CI/CD Integration page shows a jq check on the vulnerability output:
safeguard scan --dir . --output out/
jq -e '[.data.matches[].vulnerability.severity | ascii_downcase]
| index("critical") | not' out/*/vuln.json
That example scans a directory, but the output layout is the same for an image scan, with vuln.json, license.json and collection.json among the files in a folder per scan. Swap the first line for the image scan. Handle exit code 2 explicitly in your script: a run where the vulnerability scan did not happen must fail the job.
Scanner tools on a fresh runner
safeguard scan calls open source SBOM and matching tools that the interactive terminal downloads the first time it runs. The docs say that on a fresh CI container, which has never opened the terminal, you must put those tools on the PATH or point TOOLS_DIR at a directory that holds them. Bake them into your runner image so the scan does not depend on a download at build time. The docs do not list this requirement for safeguard ci scan, but check it on a fresh runner before you rely on it.
Rescan at deploy time
NIST SP 800-204D's point about scan timing is easy to skip. An image that passed on Monday can fail on Thursday without a single byte changing, because a new CVE was published against a package inside it.
There are two ways to handle that, and you probably want both.
A deploy-time step. In the job that deploys to production, run the same scan step with --environment prod against the digest you are about to deploy. If the deploy job runs on a different machine from the build, it will pull from the registry, so set SAFEGUARD_REGISTRY_USERNAME and SAFEGUARD_REGISTRY_PASSWORD.
A registry integration. Connecting the registry in Safeguard (Integrations, then the card for your registry) keeps images under continuous scanning after they ship. That catches the Thursday CVE for images that are already running, which a pipeline step cannot do because nothing triggers it. The registry setup is covered in the published post on tracking container images per environment, so we will not repeat it here.
Signing and attestation: what to expect today
Many teams want the CI job that scans the image to sign it too. Here is what Safeguard's Attestation and Signing page says, and where it stops.
Safeguard produces in-toto attestations, including SLSA provenance, CycloneDX and SPDX SBOMs and a vulnerability scan snapshot. Every attestation Safeguard produces is signed with Safeguard's own key, held in a cloud key management service. Signatures are not written to a public transparency log, and you verify them with cosign against Safeguard's published public key. The docs are honest about the trade-off: without a public log, you rely on Safeguard to protect the key.
For container images specifically, the page says there is no CLI command for signing yet, and there is no CLI command for verifying or listing attestations yet. There is also no public attestation API yet; attestation records are shown on the Attestation tab in the app.
So if your policy requires signed images, sign them in your own pipeline with your own tooling after the scan passes. Safeguard's role is the scan, the gate and the record of what was in the image. The published post on attestation and provenance in Safeguard goes deeper on what the Attestation tab shows.
Harden the scan step itself
A security step that can be tampered with produces false confidence. Four things are worth doing.
Pin the CLI version. The installer resolves the current release on every run. The CLI installation page says to pin SAFEGUARD_VERSION when you need a build to stay put, and names CI as the usual reason. Every CI guide shows where to set it. A pinned version means a CLI release cannot change your build's behaviour without a commit.
Know what the installer checks. The installation page says the shell installer downloads the release's checksums.txt, checks the archive against it, and fails closed if the checksums file is missing. Safeguard's own pipeline hardening guidance recommends downloading, verifying, then running installers rather than piping them into a shell. If your policy forbids curl | bash in pipelines, download the installer, review it, commit or cache a reviewed copy, and run that. Do not set SAFEGUARD_SKIP_CHECKSUM=1 outside an air-gapped mirror that genuinely lacks the checksums file.
Scope the key. Use a key with only scans:trigger and scans:read, locked to the project. If the key leaks from a CI log or a misconfigured job, the blast radius is one project's scans.
Do not neutralise the exit code. continue-on-error, allow_failure, || true and similar settings turn a gate into a report. If you want a report, drop --fail-on instead, so the intent is visible in the command.
Decision table: which route for which pipeline
| Your situation | Route | Why |
|---|---|---|
| Build and scan run in the same job | safeguard ci scan --image against the local daemon | True before-push gate, no registry credentials, only results leave the runner |
| Build and scan run on different runners | Push to a staging repository nothing deploys from, scan with registry credentials, promote on pass | The scan cannot see another runner's daemon |
| Results must not leave the pipeline | safeguard scan --image (or docker-archive:) plus a jq gate on vuln.json | Fully on the runner; you own the gate logic |
| You want different thresholds per environment | One step per environment with its own --environment and --fail-on | Each environment gets its own version and threshold |
| Severity alone blocks too much | Add --fail-on-gate all with Block, Warn and Notify policies | Policies carry nuance; exceptions carry approvals |
| Policy requires signed images | Your own signing step after the scan | No Safeguard CLI command signs container images yet |
Requirement vs what Safeguard does vs what you still own
| Requirement | What Safeguard does | What you still own |
|---|---|---|
| Scan an image as soon as it is built, before push (NIST SP 800-204D, DEPLOY-REQ-5) | safeguard ci scan --image reads the local image and waits for results (CLI, GA) | Running the step in the build job, before the push step |
| Evidence the image was scanned, and allow or block on policy (NIST SP 800-204D, DEPLOY-REQ-4) | Records a version per environment; --fail-on and --fail-on-gate fail the job (policies and gates, GA) | Choosing thresholds, writing policies, asking for policy outcomes to be turned on |
| Quality gates at each stage (NIST SP 800-190, 4.1.1) | One step per environment, each with its own threshold | Adding the step to every pipeline that builds or deploys images |
| Visibility into every layer (NIST SP 800-190, 4.1.1) | The image scan covers OS packages and application dependencies in the image (SCA, GA) | Fixing base images and Dockerfiles |
| Automatic detection of known vulnerabilities in the toolchain (SSDF PW.4.4) | Vulnerability matching with EPSS and KEV enrichment (vulnerability management, GA) | Acting on findings, setting exception expiry dates |
| Artifact integrity between scan and deploy (OWASP CICD-SEC-9) | Version naming tied to environment and tag | Deploying by digest; signing images |
Troubleshooting the step
The docs list the messages you are most likely to see. The ones that matter for image scans:
| Message | What it means | What to do |
|---|---|---|
reading image … | The image is not in the local daemon and could not be pulled | Set the registry credentials, or run the step where the image was built |
BAD_ENVIRONMENT | The environment name has characters other than letters, digits and dashes | Use a name like prod |
--image and --path are two different targets | Both targets were passed | Pass --image for an image, --local --path for code |
MISSING_SCOPE | The key lacks scans:trigger or scans:read | Create a key with both |
If the step never runs, the docs point to the usual cause: the branch is not in the workflow's branch list, or the build came from a fork and received no secrets.
What Safeguard does not do
- It does not sign or verify your container images from the CLI today. The Attestation and Signing docs say there is no CLI command for signing container images, and none for verifying or listing attestations, yet. Use your own signing tooling.
- It does not stop a push on its own. The scan step fails the job; your CI system's step ordering is what keeps the push from running. If you mark the scan step as allowed to fail, or run push in a parallel job, the gate is gone.
- It does not see an image that is neither local nor pullable. The image must be in the runner's local daemon or in a registry the step can authenticate to. A builder that only exports to a remote cache is invisible to the step.
- It does not include SAST findings in
--fail-on. The docs say SAST findings are scanned and shown in Safeguard but are not part of--fail-onyet. For images this rarely matters, but do not assume one flag covers every finding type. - It does not turn on policy outcomes for you.
--fail-on-gatedepends on policy outcomes being enabled for your workspace, which Safeguard does on request. The docs also do not state that gates evaluate on-runner image scans, so test it. - It does not fix the image. The scan tells you what is wrong. Base image findings need a rebuild on a fresher base; OS packages you install need a Dockerfile change; application dependencies need a lockfile change. Autonomous remediation can open pull requests for the last group, but it ships switched off.
- It does not replace runtime controls. A pre-push scan checks what is in the image. It says nothing about how the container is configured when it runs, which admission rules your cluster applies, or what the process does once it starts.
Common mistakes
Scanning in a separate job and assuming it is before push. If the scan job pulls from a registry, the image was pushed already. Either scan in the build job or push to a quarantine repository first.
Pushing in parallel with the scan. Two jobs that both depend on the build, one scanning and one pushing, race each other. Make the push depend on the scan.
Scanning the tag and deploying the tag. Between the two, the tag can move. Deploy the digest.
Reusing tags. A rebuilt 1.4.2 is a different image. Safeguard records versions by environment and tag, so reused tags blur your history. Use the commit SHA or a unique build number.
Making the step non-blocking to get a release out. || true and allow_failure hide the decision. Use an exception with an expiry date so the decision is recorded and comes back.
Treating safeguard scan exit code 0 as clean. It exits 0 whenever the scan ran, whatever it found. Gate on the output files, and fail on exit code 2.
FAQ
How do I scan a Docker image in CI before pushing it?
Run safeguard ci scan --image <ref> --environment <name> --wait --fail-on <severity> in the same job, right after docker build and before docker push. The step reads the image from the local Docker daemon, sends only results to Safeguard, and exits non-zero on a finding at or above the severity, so the push step does not run.
Do I need to push the image to a registry before Safeguard can scan it?
No, not when the step runs on the machine that built the image. Safeguard's docs say the local Docker daemon is used in that case, with nothing to set up. Registry credentials are only needed when the image is not local.
How do I fail the build only on critical vulnerabilities?
Add --wait --fail-on critical to the scan command. Use high, medium or low for a stricter threshold, or drop --fail-on to report without failing.
Can I fail the build on a Safeguard policy instead of a severity?
Yes. Add --fail-on-gate <gate name or id> or --fail-on-gate all. The job passes when the gate is Passed or Warning and fails when it is Failed, Error or Not evaluated. Policy outcomes must be turned on for your workspace, which you ask Safeguard to do.
What permissions does the Safeguard key need?
scans:trigger and scans:read, per the docs. Lock the key to one project and store it as SAFEGUARD_API_KEY in your CI's secret store.
Can Safeguard scan an image without sending results to the cloud?
safeguard scan --image runs fully on the runner and writes JSON results to an output folder. It does not fail on findings, so you gate on the vuln.json file yourself, as the docs show with jq.
Does Safeguard sign container images in CI?
Not from the CLI today. Safeguard's docs say there is no CLI command for signing container images yet. Attestations Safeguard produces are signed with Safeguard's key and verifiable with cosign, with no public transparency log.
Why does the same image pass in CI and fail a week later?
Because new vulnerabilities are published after the build. NIST SP 800-204D makes this point about scan timing. Rescan at deploy time and keep the registry connected for continuous scanning.
Closing the gap between a finding and a fixed image
A pre-push gate is good at saying no; getting to yes means a fresher base, a Dockerfile fix or a lockfile update. The published posts on scanning container images in Safeguard and fixing vulnerabilities in container base images cover the fixing side, and plain English policies to release gates covers writing the policies your gate will enforce. The section below explains what Safeguard does when the fix is in a dependency, or when upstream has not shipped one yet.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.
3. Gold Registry: start from components that are already fixed
Gold Registry provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. Ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | Safeguard policies and gates in CI |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
The first demo is a real Safeguard CLI session. It installs the CLI, opens the interactive terminal and asks it to fix a lodash CVE in a repository. The assistant reads the finding and advisory, asks before any command that changes something, edits package.json to lodash 4.17.21, runs the install and tests, prints a summary and pushes nothing. When your image gate fails on an application dependency, this is the flow that fixes the lockfile so the next build passes.
Click through it at your own pace, or open it full screen.
The second demo sets up the local runner on your own machine, approves it with its device code, and then scans from the terminal. Before you wire a gate into a shared pipeline, scanning the same image locally is a quick way to see what the gate will catch.
Click through it at your own pace, or open it full screen.
The third demo connects Claude to Safeguard. You add Safeguard from Claude's connector directory, where it has a community listing, sign in with OAuth, and choose which tools Claude can use. Safeguard's docs say the MCP server has read-only tools for gate outcomes, policy outcomes and evaluation history when policy outcomes are on for your workspace, so you can ask why a production gate failed without opening the pipeline log. The connector directory post covers the install.
Click through it at your own pace, or open it full screen.
To add the step to your own pipeline, install the CLI from safeguard.sh/download and keep the Safeguard docs open on the CI/CD Integration and Container images pages. For an earlier walkthrough of the CLI side, see scanning a container image in CI with the Safeguard CLI.
Quick check
Your build job runs docker build, and a separate scan job on another runner runs safeguard ci scan --image with the same tag. Is that a before-push gate?
No. The scan job cannot see the build runner's local Docker daemon, so it has to pull the image from a registry, which means the image was already pushed. Move the scan into the build job, or push to a staging repository that nothing deploys from and promote only after the scan passes.
You switched to safeguard scan --image so results stay on the runner, and every build now passes. What is wrong?
safeguard scan does not fail on findings. It exits 0 whenever the scan ran and 2 when the vulnerability scan did not run. Add a check on the vuln.json output, such as the jq example in Safeguard's docs, and fail the job on exit code 2.
Sources
- Safeguard docs: Container images (CI/CD Integration)
- Safeguard docs: CI/CD Integration
- Safeguard docs: GitHub Actions
- Safeguard docs: GitLab CI/CD
- Safeguard docs: Jenkins
- Safeguard docs: Azure Pipelines
- Safeguard docs: Bitbucket Pipelines
- Safeguard docs: Policies and Gates
- Safeguard docs: Policy outcomes and actions
- Safeguard docs: CLI usage
- Safeguard docs: CLI installation
- Safeguard docs: Attestation and Signing
- Safeguard docs: CI/CD Security
- Safeguard docs: Container Image Integration FAQ
- NIST SP 800-204D: Strategies for the Integration of Software Supply Chain Security in DevSecOps CI/CD Pipelines
- NIST SP 800-190: Application Container Security Guide
- NIST SP 800-218: Secure Software Development Framework version 1.1
- OWASP Top 10 CI/CD Security Risks
- OCI Image Specification: Descriptors
- SLSA v1.0: Security levels
- CISA: Known Exploited Vulnerabilities Catalog
- FIRST: Exploit Prediction Scoring System
- Kubernetes: Images
- GitHub Docs: Using secrets in GitHub Actions
- GitLab Docs: CI/CD variables
- Docker Docs: docker image save
Last reviewed: 24 October 2026. Product capabilities change; check docs.safeguard.sh for the current state.