SIP: Five Immediate Software Supply Chain Controls
SIP is a five-step emergency plan for reducing software supply chain risk across AI agents, dependencies, containers, attestations, and releases.
I talk about software supply chain security in different capacities, and I often get the same question: "If we can only do a few things, what should we do first?"

This is why I compiled a list of five immediate controls that a technical leader can implement in an hour:
SIP: Security Immediate Plan
0 of 5 checked
That's it. Five controls. And because I thought people would give it to their AI coding agents, I have also created a portable SIP Agent Skill. Tell your agent to use it:
Install the SIP skill: https://github.com/ContainerSecurity-dev/sip-skill
The SIP Framework
SIP follows the software supply chain in order:
AI agent → dependencies → container build → attestations → vulnerability gate
Here are the five controls, with more detail on each one, and how to implement them in a CI/CD workflow:
- Isolate Local AI Agents.
- Decision. Run AI coding agents inside a sandboxed microVM, such as Docker Sandboxes (
sbx run). - Delegation. Instruct engineers to use
sbx policy init deny-alland store tokens insbxsecret storage.
- Freeze Unvetted Dependencies.
- Decision. Apply a five-day cooldown period and disable lifecycle scripts.
- Delegation. Use
npm ci --ignore-scriptsand addmin-release-age=5to your npm configuration.
- Harden Container Builds.
- Decision. Use Docker Hardened Images such as
dhi.io/node:26, with minimal attack surface and a non-root user by default. - Delegation. Convert Dockerfiles to multi-stage builds and separate build and runtime stages.
- Generate SBOM and Provenance Attestations.
- Decision. Generate SBOM and max-level provenance attestations for every build.
- Delegation. Enable BuildKit attestations in CI/CD and include every Dockerfile build stage in the SBOM.
- Scan the Attested SBOM for Vulnerabilities.
- Decision. Use Trivy to scan the SBOM attached to the built image and fail on fixable Critical CVEs.
- Delegation. Make the vulnerability scan a required CI gate before promoting the image to a release tag.
You can find a working implementation of this framework in the SIP sample repository, which implements the five controls in a GitHub Actions workflow and with a sample Node.js application.
Agent Prompt
Install the SIP skill in your agentic client, then give the agent this prompt inside the repository you want to secure:
$sip SIP it up! Implement controls ii through v. Ask questions when in doubt.
The skill is portable across clients that support Agent Skills. The exact installation and invocation mechanism differs between clients, but the security plan does not.
Implementing SIP
If you, like me, prefer to do things yourself, you can follow the instructions here to get an idea of how to implement SIP in your own machine and CI/CD workflow.
Isolate Local AI Agents
This is the only control that is not implemented in CI/CD and protects your own machine. As we are using AI coding agents increasingly, they are being targeted by attackers more and more. If you have a coding agent installed, you should treat it like a hacker's agent. Agents can be easily tricked into exfiltrating secrets or executing malicious code. The safest way to run an agent is inside a sandboxed microVM, such as Docker Sandboxes (sbx).
Let's set up the sandbox with a deny-by-default network policy:
$ sbx policy init deny-all
Then allow only the network destinations the agent actually needs:
$ sbx policy allow network "api.openai.com,github.com,*.npmjs.org"
Store credentials using sbx rather than exposing the raw token inside the sandbox:
$ sbx secret set openai
$ sbx secret set github
For Codex, OAuth can also remain entirely host-side:
$ sbx secret set openai --oauth
$ sbx run codex .
Other coding agents and their clients are similar. Docker Sandboxes runs the agent inside an isolated environment, supports deny-by-default network policies, and stores secrets in the host's credential store. Credentials are automatically injected through the host-side proxy without making their raw values readable to the agent.
Freeze Unvetted Dependencies
Many supply chain attacks start with a dependency that is either malicious or compromised. In most cases, a compromised dependency is detected within the first few days after publication. A five-day cooldown period allows time for the community to discover and report malicious packages.
Also, many supply chain attacks rely on lifecycle scripts that execute automatically during installation. Disabling lifecycle scripts prevents automatic execution of scripts that could compromise the build or leak secrets.
For npm projects, add the following policies directly to the repository in .npmrc:
min-release-age=5
ignore-scripts=true
- The policy
min-release-age=5tells npm not to resolve package versions published within the previous five days. - The policy
ignore-scripts=trueprevents dependency lifecycle scripts from automatically executing during installation.
In addition, you should also commit your lockfile (package-lock.json) to the repository. This ensures that the exact versions of dependencies are installed in CI, rather than allowing npm to resolve new versions:
$ npm ci --ignore-scripts
One subtlety matters: npm ci trusts versions already recorded in the lockfile,
so resolution-time cooldown settings do not re-check them. CI must therefore
validate the publication age of locked packages before installing them and fail
closed when registry metadata cannot be verified.
In GitHub Actions:
- name: Set up Node
uses: actions/setup-node@<PINNED-SHA>
with:
node-version: 26
- name: Validate locked dependency age
run: node scripts/validate-lockfile-age.mjs
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Test
run: npm test
The validate-lockfile-age.mjs script checks the publication date of each dependency in the lockfile and fails if any package is younger than five days. This ensures that only vetted dependencies are installed in CI/CD. The script is already available in the SIP skill repository.
If a package genuinely requires an installation script, do not globally re-enable scripts. Review the package and create an explicit exception.
Again, the example uses npm, but the same principle applies to other package managers. The immediate rule is:
No fresh dependencies. No automatic lifecycle scripts. No unlocked installations in CI.
Some package managers might not have a built-in cooldown mechanism. In that case, you can implement a custom script to check the publication date of each dependency before installation. You could perhaps ask your AI coding agent to write one for you, but make sure to run it inside a sandboxed microVM!
Harden Container Builds
The application is now built from a constrained dependency graph. The next step is to control what ends up on the container image and goes into production. Believe it or not, many CVEs, if not most, come from your base images, not your application code. So, let's tighten things up.
Use a multi-stage Dockerfile and Docker Hardened Images.
A multi-stage build separates the build environment from the runtime environment. The build stage can include compilers, package managers, and other tools needed to build the application, while the runtime stage contains only the minimal set of files needed to run the application.
For example:
# syntax=docker/dockerfile:1
ARG BUILDKIT_SBOM_SCAN_STAGE=true
# Build stage
FROM dhi.io/node:<version>-<distro>-dev AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --ignore-scripts
COPY . .
RUN npm run build
# Runtime stage
FROM dhi.io/node:<version>-<distro>
COPY --from=build --chown=node:node /app /app
WORKDIR /app
CMD ["index.js"]
The Docker Hardened Images have near-zero number of exploitable CVEs. Also, they have two sets of images:
- Development images, in this case
dhi.io/node:<version>-<distro>-dev, which include compilers, package managers, and other build tools. - Runtime images, in this case
dhi.io/node:<version>-<distro>, have no package managers, no shell, and a non-root user by default. This reduces the attack surface of the final image.
Before building in GitHub Actions, authenticate to DHI:
- name: Login to DHI
uses: docker/login-action@v4
with:
registry: dhi.io
username: ${{ vars.DOCKER_USERNAME }}
password: ${{ secrets.DHI_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
Do not pass credentials using Docker build arguments. Use CI/CD secrets and the docker/login-action to authenticate to the registry. This prevents credentials from being exposed in the build context or image layers.
Yet, we have one thing we did not explain about the Dockerfile:
ARG BUILDKIT_SBOM_SCAN_STAGE=true
We will explain it in the next section.
Generate SBOM and Provenance Attestations
Now record what was built and how it was built.
BuildKit (being the thing that builds your Docker image) supports two particularly useful attestations:
- SBOM, the software components contained in or used to create the image.
- Provenance, information about how the image was produced.
Docker recommends max-level provenance when possible. SBOM generation must be explicitly enabled.
First authenticate to the target registry:
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
Then build a candidate image identified by the Git commit:
- name: Build and attest candidate
id: build
uses: docker/build-push-action@v7
with:
context: .
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
sbom: true
provenance: mode=max
outputs: type=image,push=true,oci-mediatypes=true,oci-artifact=true
BuildKit normally stores attestations alongside the image index. Setting oci-artifact=true formats the attestation manifests as OCI artifacts and adds a subject pointing back to the image manifest. This allows OCI-aware tooling and registries to discover the relationship between the image and its attestations.
The build action also returns the resulting image digest:
${{ steps.build.outputs.digest }}
Use that digest from this point onward. A tag can move; a digest identifies the exact artifact that was built.
The result is now:
- Source →
- Dependencies →
- Multi-stage Docker build →
- Image digest →
- SBOM
- Provenance
Because BUILDKIT_SBOM_SCAN_STAGE=true was declared in the Dockerfile, the SBOM also records packages from the relevant build stages instead of describing only the minimal final runtime image. This is important, because a vulnerability in a build-time dependency can still compromise the final image. And it shows why it's important to generate SBOMs during the build rather than after the fact.
Scan the Attested SBOM for Vulnerabilities
The final stage is to scan the SBOM for vulnerabilities, before you can take a SIP of your favorite beverage.
Do not merely ask a scanner to look for an attached SBOM. Discovery can be best-effort and fall back to inspecting image layers. The gate should explicitly extract the attached SBOM from the exact image digest, validate it, and scan that file.
Install a pinned version of Trivy in GitHub Actions:
- name: Install Trivy
uses: aquasecurity/setup-trivy@<PINNED-SHA>
with:
version: <PINNED-TRIVY-VERSION>
Then retrieve and scan the attestation from the exact image digest created by the build:
- name: Retrieve and scan attested SBOM
run: |
docker buildx imagetools inspect \
"ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}" \
--format '{{ json .SBOM.SPDX }}' > sbom.spdx.json
jq -e \
'.spdxVersion and (.packages | type == "array") and (.packages | length > 0)' \
sbom.spdx.json > /dev/null
trivy sbom \
--scanners vuln \
--severity CRITICAL \
--ignore-unfixed \
--exit-code 1 \
sbom.spdx.json
This makes the attestation mandatory: a missing or malformed SPDX document fails before Trivy runs.
The remaining policy is intentionally simple:
--severity CRITICAL
--ignore-unfixed
--exit-code 1
A fixable Critical vulnerability therefore fails the workflow.
Only after this gate succeeds should the candidate become a release image. For example:
- name: Promote image
run: |
docker buildx imagetools create \
--tag ghcr.io/${{ github.repository }}:latest \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
This promotes the already scanned digest rather than rebuilding the application after the security check.
Putting It Together
The resulting GitHub Actions flow looks like this:
name: SIP Supply Chain
on:
push:
branches: [main]
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@<PINNED-SHA>
# SIP ii — Freeze dependencies
- name: Set up Node
uses: actions/setup-node@<PINNED-SHA>
with:
node-version: 26
- name: Validate locked dependency age
run: node scripts/validate-lockfile-age.mjs
- name: Install locked dependencies
run: npm ci --ignore-scripts
- name: Test
run: npm test
# SIP iii — Harden the container build
- name: Login to DHI
uses: docker/login-action@<PINNED-SHA>
with:
registry: dhi.io
username: ${{ vars.DOCKER_USERNAME }}
password: ${{ secrets.DHI_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@<PINNED-SHA>
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Buildx
uses: docker/setup-buildx-action@<PINNED-SHA>
# SIP iv — Generate attestations
- name: Build and attest candidate
id: build
uses: docker/build-push-action@<PINNED-SHA>
with:
context: .
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
sbom: true
provenance: mode=max
outputs: type=image,push=true,oci-mediatypes=true,oci-artifact=true
# SIP v — Scan the attested SBOM
- name: Install Trivy
uses: aquasecurity/setup-trivy@<PINNED-SHA>
with:
version: <PINNED-TRIVY-VERSION>
- name: Retrieve and gate attached-SBOM Critical CVEs
run: |
docker buildx imagetools inspect \
"ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}" \
--format '{{ json .SBOM.SPDX }}' > sbom.spdx.json
jq -e \
'.spdxVersion and (.packages | type == "array") and (.packages | length > 0)' \
sbom.spdx.json > /dev/null
trivy sbom \
--scanners vuln \
--severity CRITICAL \
--ignore-unfixed \
--exit-code 1 \
sbom.spdx.json
# Promote exactly what was scanned
- name: Promote image
run: |
docker buildx imagetools create \
--tag ghcr.io/${{ github.repository }}:latest \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
This compact workflow shows the artifact chain. In production, trigger SIP on pull requests and give SIP ii, SIP iii–iv, and SIP v separate jobs so failures are visible. Keep credential-bearing PR jobs behind a protected environment; fork PRs must not receive registry credentials. Promotion remains release-only.
SIP therefore creates one continuous chain:
Sandbox the agent → delay and constrain dependencies. → build with a hardened multi-stage Dockerfile. → attest every relevant build stage → scan that attested inventory → release only the artifact that passed.
The controls are small individually. Their value comes from the fact that each one strengthens the input to the next.
Conclusion
SIP is a five-step emergency plan for reducing software supply chain risk across AI agents, dependencies, containers, attestations, and releases. It is designed to be implemented quickly and effectively, providing immediate security benefits.
The presented GitHub Actions workflow is a working implementation of the SIP framework. It's rather simplistic for educational purposes, if you want a more robust implementation, check out the SIP sample repository.
