SIP: Security Immediate Plan for Java
By Mohammad-Ali A'râbi and Catherine Edelveis
Secure your Java supply chain with five practical controls: sandbox AI agents, vet dependencies, harden images, generate SBOMs, and gate releases.
Forthcoming in
JAVAPRO
Building a secure software supply chain is often depicted as a monumental transformation of the existing practices. You need to redesign your CI/CD pipeline according to an established security framework such as SLSA, introduce provenance, implement zero trust principles, integrate security scanning tools, build a vulnerability management system, and do many more things. No wonder many teams postpone this task to better days with more time, larger security team, and a permission to rebuild the whole pipeline from scratch. Indeed, a mature supply chain security program may take months to build, but the first meaningful improvements can be made much sooner and without tearing everything down. So where should a team start? For Java teams, our answer is SIP: a Security Immediate Plan made of five controls that a technical leader can start implementing in hours rather than months. Each control strengthens the next step, from the code on a developer’s machine to the image released to production.

TL;DR — the SIP overview
- Isolate local AI coding agents.
- Freeze unvetted Java dependencies, build plugins, and annotation processors.
- Build the application in a hardened multi-stage container.
- Generate runtime and build-stage SBOMs and detailed provenance metadata.
- Scan every SBOM attached to the exact image digest and block findings under the runtime and build policies.
These five controls follow the Java software supply chain in order:
AI agent → Maven or Gradle dependencies → container build → attestations → vulnerability gate
The controls are intentionally small. Together, they give Java teams a practical starting point for securing both their application and the tools that build it.
Mohammad, one of the authors of this article, also created a portable SIP Agent Skill. You can tell a compatible AI coding agent to install it:
Install the SIP skill: https://github.com/ContainerSecurity-dev/sip-skill
The SIP Framework for Java
Here is the plan at a glance:
- Isolate local AI agents. Run coding agents in a sandboxed microVM with deny-by-default network access and host-managed secrets.
- Freeze unvetted build inputs. Pin Java dependencies and build plugins, reject snapshots and dynamic versions, and quarantine newly published artifacts for five days.
- Harden container builds. Compile in a JDK build stage and copy only the packaged application into a minimal, non-root JRE image.
- Generate SBOM and provenance attestations. Ask BuildKit to describe both the shipped runtime and the earlier Java build stages.
- Scan every attested inventory. Extract runtime and build-stage SBOMs from the immutable image digest, check their basic structure, and apply separate vulnerability policies.
The SIP Java companion repository contains a small Spring Boot application, the Dockerfiles below, and a script for scanning its SBOMs. We use Maven in the examples; Gradle teams can apply the same principles.
The workflow snippets illustrate the main steps. Full setup instructions, version pinning, and implementation details are in the companion repository.
1. Isolate Local AI Coding Agents
AI coding agents can read source code, execute build tools, install dependencies, edit CI workflows, and often even access credentials. Giving an agent unrestricted access to CI runner or a developer machine would mean that if an agent is compromised by a malicious prompt, the scope of its access will determine how much damage it can cause.
Therefore, a good security practice is to run an agent in an isolated execution environment with limited access to the rest of the system. Such an environment is called a sandbox. A sandbox does not prevent an agent from making a bad decision. Its goal is to reduce the blast radius if such a decision was made.
For Java projects, sandboxing may be especially relevant. Building a Java project could mean executing quite a lot of code like build scripts, plugins, code generators, etc. An AI Agent is an additional component that can decide when to execute that code. Plus, a Java project itself can integrate an AI agent using, for example, Spring AI or LangChain4j. If these in-built agents can execute commands, that will add to the issue we described.
Luckily, sandboxing is not a new technology, and there are a few ready solutions. For example, you can use Docker Sandboxes (sbx). Begin with a deny-by-default network policy:
sbx policy init deny-all
After that, allow only the destinations required for the task. For example, a Java project may need an AI provider, its source host, Maven Central, and the project’s approved artifact repository. This is how you can specify required destinations:
sbx policy allow network "api.openai.com,github.com,repo.maven.apache.org"
If your organization uses an internal Maven repository manager, allow that host instead of Maven Central. This ensures the build follows the same repository policy inside and outside the sandbox.
Store credentials through the sandbox rather than placing raw tokens in environment files that the agent can read:
sbx secret set github
sbx secret set artifact_repository
For Codex, OAuth can remain host-side:
sbx secret set openai --oauth
sbx run codex .
Note that hiding the credentials from an agent is only part of the deal. Even if an agent doesn’t see the credentials, it can still make authenticated requests using them. So apart from being hidden, credentials should also be short-lived and narrowly scoped.
2. Freeze Unvetted Java Build Inputs
Java builds execute more than the application’s dependencies. Maven plugins, Gradle plugins, annotation processors, compiler extensions, and test tooling can all run code during the build. A supply chain policy must therefore cover the entire build graph, not only the JAR files that ship with the application.
The sensible first step in implementing this policy is to gain visibility into the graph, pin the versions, and strengthen the trust paths. Apply these immediate rules to achieve that:
- Pin dependency and plugin versions to limit unexpected changes. Avoid version ranges and selectors such as
LATESTandRELEASE. - Reject
SNAPSHOTdependencies in release builds: their contents can change without a new version number. - Commit and verify the Maven or Gradle wrapper, including its distribution checksum, so developers and CI use the same build tool.
- Route downloads through an approved repository manager, including build plugins and annotation processors.
- Quarantine new releases for five days to give the ecosystem time to spot problems. Allow reviewed exceptions for urgent security fixes.
- Require checksums from a trusted source to detect altered downloads.
These controls reduce unexpected changes; they do not make every dependency safe or guarantee reproducible builds. Keep the sandbox from control one in place when running build code.
Maven has no built-in five-day release delay. Enforce that policy at your repository proxy, before artifacts reach the build. The same proxy should serve developer machines, CI, and container builds.
The artifact repositories must first be defined centrally by a responsible team. After that, they can be enforced in the build tool. For example, if you use Maven, configure a single approved mirror in the CI settings.xml:
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
<mirrors>
<mirror>
<id>approved-repository</id>
<name>Approved and quarantined Java artifacts</name>
<url>https://packages.example.com/maven</url>
<mirrorOf>*</mirrorOf>
</mirror>
</mirrors>
</settings>
The mirror sends dependency and plugin requests to the approved repository; the repository enforces the quarantine. Use the same settings inside and outside Docker. The wrapper also downloads Maven itself, so route that download through the proxy with MVNW_REPOURL.
With those settings supplied by CI, run the familiar build command from the Maven project directory:
./mvnw --settings "$MAVEN_SETTINGS_FILE" --strict-checksums \
--batch-mode --no-transfer-progress verify
The companion setup guide covers credentials, wrapper verification, and cache handling.
For Gradle, enable dependency locking, commit the lockfiles, enable dependency verification, and run with a trusted wrapper. The immediate rule remains the same:
Admit reviewed inputs before executing build-time code.
3. Harden the Java Container Build
The next step is to control what becomes part of the production image. Production images often include components not required by the running application: unnecessary OS packages, a full JDK with a compiler and development tools, build time dependencies. The bloat leads to increased attack surface and challenged vulnerability management. Besides, a package manager and other download utilities in a production image give an attacker a chance to install malicious packages.
Hardened images, a solution that has recently been gaining traction, aim to reduce these risks by providing a minimal base image with low-to-zero CVEs at the moment of release and provenance data. These images are continuously updated by the vendor to patch known CVEs. Migration to hardened images will help you drastically reduce the attack surface and get a more controlled runtime environment.
There are several offerings of hardened images. In the examples below, we will use Docker Hardened Images and BellSoft Hardened Images to illustrate the approach — both products are free to use with optional commercial support.
To minimize the Java production image and get the most out of hardened images it is recommended to use Docker multi-stage builds, where you compile the application with JDK at the first stage, and then transfer the JAR file to a JRE base image, leaving Maven, the compiler, and other build tools behind.
Docker Hardened Images (DHI) provide maintained, minimal images for application runtimes and build tools, together with SBOMs and build provenance. Separate development and runtime variants let you keep the tools needed for compilation out of production. In Docker’s Java pattern, the dhi.io/maven:...-dev image supplies Maven and a JDK, while the dhi.io/eclipse-temurin:... runtime contains only what is needed to run the application. That runtime has no package manager or shell and runs as a non-root user by default.
# syntax=docker/dockerfile:1
ARG BUILDKIT_SBOM_SCAN_STAGE=true
FROM dhi.io/maven:3-jdk25-alpine3.22-dev AS build
WORKDIR /build
COPY --chmod=755 mvnw ./mvnw
COPY .mvn/ .mvn/
COPY pom.xml ./
COPY src/ src/
RUN --mount=type=secret,id=maven_settings,required=true \
--mount=type=secret,id=maven_repository_url,env=MVNW_REPOURL,required=true \
./mvnw --settings /run/secrets/maven_settings --strict-checksums \
--batch-mode --no-transfer-progress -Dmaven.repo.local=/build/repository verify
FROM dhi.io/eclipse-temurin:25-alpine3.22 AS runtime
WORKDIR /app
COPY --from=build /build/target/application.jar /app/application.jar
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/application.jar"]
Adjust the JAR name to match the project. If the application is a Spring Boot executable JAR, consider extracting its layers so that application classes and third-party dependencies form separate container layers.
BellSoft Hardened Images (BHI) are built on Alpaquita Linux, BellSoft’s lightweight Linux distribution. Their Java images include Liberica JDK Lite, a compact OpenJDK distribution, with JDK and JRE options for building and running applications. BellSoft offers glibc and musl variants, plus non-root and distroless images; the distroless variants omit the shell and retain only essential runtime components. This gives Java teams a choice of small runtime images suited to their application’s requirements.
For Spring Boot, we can also separate the application’s classes from its dependencies. The following example combines layered extraction with BellSoft’s non-root JRE image.
# syntax=docker/dockerfile:1
ARG BUILDKIT_SBOM_SCAN_STAGE=true
FROM bellsoft/liberica-runtime-container:jdk-25-glibc AS build
WORKDIR /build
COPY --chmod=755 mvnw ./mvnw
COPY .mvn/ .mvn/
COPY pom.xml ./
COPY src/ src/
# Both settings and the wrapper's repository override are mandatory.
# Keep downloaded artifacts in the stage filesystem for SBOM discovery.
RUN --mount=type=secret,id=maven_settings,required=true \
--mount=type=secret,id=maven_repository_url,env=MVNW_REPOURL,required=true \
./mvnw --settings /run/secrets/maven_settings --strict-checksums \
--batch-mode --no-transfer-progress -Dmaven.repo.local=/build/repository verify
FROM bellsoft/hardened-liberica-runtime-container:jre-25-glibc AS optimizer
WORKDIR /app
COPY --from=build /build/target/application.jar application.jar
RUN ["java", "-Djarmode=tools", "-jar", "application.jar", "extract", "--layers", "--destination", "extracted"]
FROM bellsoft/hardened-liberica-runtime-container:jre-25-nonroot-glibc AS runtime
WORKDIR /app
COPY --from=optimizer /app/extracted/dependencies/ ./
COPY --from=optimizer /app/extracted/spring-boot-loader/ ./
COPY --from=optimizer /app/extracted/snapshot-dependencies/ ./
COPY --from=optimizer /app/extracted/application/ ./
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/application.jar"]
The first stage compiles the project. The second extracts the JAR into layers. The final stage copies those layers into a non-root runtime and starts the generated application.jar. When only application code changes, Docker can reuse the dependency layers, reducing the amount of data transferred during an update.
Both Dockerfiles use the wrapper and repository settings from control two. Supply those settings as build secrets, and authenticate to the image registries as needed; the companion repository shows the commands. The BUILDKIT_SBOM_SCAN_STAGE argument connects the build to our next control.
4. Generate SBOM and Provenance Attestations
A Software Bill of Materials, or SBOM, is an inventory of the components in an artifact. It helps answer a practical question: when a vulnerability is announced, where are we using the affected component?
Provenance records how the image was built and which inputs were used. Here we generate that record; signing and verifying it is a further step for establishing trust in the builder.
An SBOM created only by inspecting the final Java image can miss the JDK, Maven plugins, and other tooling used in an earlier build stage. BuildKit can generate the SBOM during the build, while those stages still exist.
After authenticating to the target registry, build and push a candidate identified by the Git commit:
- name: Build and attest Java candidate
id: build
uses: docker/build-push-action@<PINNED-SHA>
with:
context: examples/java
file: examples/java/Dockerfile.bellsoft
platforms: linux/amd64
no-cache: true
secrets: |
"maven_settings=${{ secrets.MAVEN_SETTINGS }}"
"maven_repository_url=${{ vars.APPROVED_MAVEN_REPOSITORY }}"
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
sbom: true
provenance: mode=max
outputs: type=image,push=true,oci-mediatypes=true,oci-artifact=true
BuildKit stores the attestations alongside the image. sbom: true requests the inventory, and provenance: mode=max requests detailed build metadata. Because that metadata can include build-argument values, pass credentials through secrets, as shown above.
The action also returns an immutable digest:
${{ steps.build.outputs.digest }}
Use that digest from this point onward. A tag can point to a different image later; the digest identifies the candidate you actually built.
Because the Dockerfile declares BUILDKIT_SBOM_SCAN_STAGE=true, BuildKit also scans the earlier Java build stage. This matters because a vulnerable or compromised Maven plugin, compiler, or JDK can influence the application even when it is absent from the runtime image.
There is one useful trap to understand: a BuildKit cache mount is outside the stage filesystem being scanned. If Maven stores its downloads in a cache mount, build-only dependencies can disappear from the inventory even though they influenced the build.
Our examples keep Maven downloads in the build-stage filesystem so the scanner can inspect them. The final image stays small because it receives only the packaged application. If you need cache mounts for performance, add a complementary dependency inventory; the companion guide explains the tradeoff.
5. Scan the Attested SBOM
Now turn the inventory into a release decision. Retrieve the SBOMs attached to the candidate’s digest and scan both the runtime and the earlier build stages. Scanning only the final image would miss the Maven plugins and other tools we set out to protect.
The companion gate script collects those inventories, checks that the required documents are present, and runs Trivy on each one. After installing a pinned Trivy version, call it from CI for the BellSoft example:
- name: Scan runtime and build inventories
run: |
python3 scripts/scan-attested-sboms.py \
"ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}" \
--platform linux/amd64 --min-build-sboms 2 \
--require-build-name sbom-build --require-build-name sbom-optimizer \
--runtime-severity CRITICAL --build-severity HIGH,CRITICAL
This example blocks fixable Critical vulnerabilities in the runtime and fixable High or Critical vulnerabilities in the build environment. A vulnerable build tool matters even when it is not shipped to users. Missing inventories or scanner errors also stop the release.
This is a starting policy: other findings still need review, and scanner coverage must match your packages and operating system. The companion repository documents the scanner limitations found in testing and how to adapt the gate to other Dockerfiles and platforms.
Before promotion, also start the candidate and check that the application responds. A clean scan is not a substitute for a working application.
Only after the gate and application checks pass should the candidate receive a release tag:
- name: Promote Java image
run: |
docker buildx imagetools create \
--tag ghcr.io/${{ github.repository }}:latest \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
This promotes the existing candidate with its attestations. It does not rebuild the application after the security check, so the release is the artifact you tested.
Putting It Together
The resulting release chain is:
Sandbox the coding agent → quarantine and verify Java build inputs → compile in a hardened JDK stage → run from a minimal non-root JRE image → attest the build and runtime inventory → scan runtime and build SBOMs attached to the immutable digest → promote only the artifact that passed.
Make each step visible in CI so a failure stops the release at the right point. The companion repository provides the implementation details, including pinning tools and protecting credentials.
Agent Prompt
If your coding client supports Agent Skills, install the SIP skill and give the agent this prompt inside the Java repository you want to secure:
$sip SIP it up! Adapt and implement controls ii through v for this Java project. Ask questions when in doubt.
Run the agent inside the sandbox from control one. Review every proposed dependency, Maven or Gradle plugin, Dockerfile change, and CI permission before merging it.
Conclusion
SIP is not a complete software supply chain security program. It is an immediate plan for reducing the most obvious risks while creating a trustworthy path from Java source code to a released container image.
For Java teams, that means looking beyond application JARs. The JDK, Maven or Gradle plugins, annotation processors, base images, build stages, attestations, and release tags are all part of the same chain.
Start with this connected path, test its failure cases, and extend it with verified provenance and ongoing vulnerability management.
References and Implementation Resources
- SIP Java companions: application, Dockerfiles, and SBOM gate
- SIP framework and reference implementation
- Portable SIP Agent Skill
- Maven Wrapper checksum verification
- Gradle dependency locking
- Gradle dependency verification
- Docker Hardened Images for Java
- Docker build attestations
- Original SIP article
- How to Containerize a Java Application Securely — JAVAPRO
- Modernizing Production Containers to Resist the Constant CVE Flow — JAVAPRO
- The Poisoned Macchiato: Signing Java SBOMs with Cosign — JAVAPRO
