Engineering

Building minimal and low CVE images for Java

Adrian Mouat, Staff DevRel Engineer
June 11, 2024
copied

At Chainguard, we're always looking for ways to help communities improve their security practices, especially in conjunction with Chainguard Images. This time, we take a look at Java. In this blog, we'll walk through how users can port an existing Java application to use Chainguard Images and demonstrate the resultant improvement in size and security.

All the Chainguard Images in this blog post are available in the free Developer tier of Chainguard Images. For more information on what Chainguard Images can offer you, including our paid products, check out our Images directory.

The application we're looking at was created by my colleague Mauren Berti for a recent Chainguard Learning Labs session. The code can be found in the learning-labs GitHub repository. It's a Spring Boot example application that includes a Dockerfile build which uses the Docker Hub official Maven image. 

The Dockerfile looks like:

-- CODE language-bash -- FROM maven WORKDIR /work COPY src/ src/ COPY pom.xml pom.xml RUN mvn clean package RUN REPOSITORY=$(mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout) && rm -rf ${REPOSITORY} WORKDIR /app RUN cp /work/target/java-demo-app-1.0.0.jar . ENTRYPOINT ["java", "-jar", "java-demo-app-1.0.0.jar"]

We can build the image with:

-- CODE language-bash -- docker build -t maven-app .

And run it with:

-- CODE language-bash -- docker run --rm -d -p 8080:8080 maven-app

The app should now be running and you can interact with it on port 8080:

-- CODE language-bash -- curl localhost:8080/hello Hello, World!

Now, let’s look at the size of the image:

-- CODE language-bash -- docker images maven-app REPOSITORY TAG IMAGE ID CREATED SIZE maven-app latest 8e2b870fe5dd 20 seconds ago 585MB

We can use Grype (or any other CVE scanner!) to investigate CVEs:

-- CODE language-bash -- grype docker:maven-app ✔ Vulnerability DB [no update available] ✔ Loaded image maven-app:latest ✔ Parsed image sha256:8e2b870fe5dd2f9cc8b36dc4427160fccf154570203b778b0870a2e3a1ef5ba4 ✔ Cataloged contents 0d82981cf1618d9ae87dc38bd5365c89d0017a7745bbfc30a9147e94967ba5e7 ├── ✔ Packages [254 packages] ├── ✔ File digests [6,313 files] ├── ✔ File metadata [6,313 locations] └── ✔ Executables [930 executables] ✔ Scanned for vulnerabilities [82 vulnerability matches] ├── by severity: 0 critical, 0 high, 25 medium, 41 low, 16 negligible └── by status: 8 fixed, 74 not-fixed, 0 ignored

The image is 525 MB and has 82 CVEs. This is a reasonable result for a Java image, but let's see what happens if we move to use Chainguard Images. The first thing we can do is simply replace FROM maven with the Chainguard equivalent, leaving everything else the same:

-- CODE language-bash -- FROM cgr.dev/chainguard/maven …

(Note that you can also type FROM chainguard/maven as Chainguard Images are now available on the Docker Hub!)

Let's rebuild the image and give it a new tag:

-- CODE language-bash -- docker build -t maven-app-cg .

Now, look at the size:

-- CODE language-bash -- REPOSITORY TAG IMAGE ID CREATED SIZE maven-app-cg latest bdbd76091dbf 4 minutes ago 342MB

And the CVE count:

-- CODE language-bash -- grype docker:maven-app-cg ✔ Vulnerability DB [no update available] ✔ Loaded image maven-app-cg:latest ✔ Parsed image sha256:bdbd76091dbf6b6ccfdfd187024abef00ea1792ac6642d50ac422864010116bb ✔ Cataloged contents 7c5d998d4197d49b72c63e9425455fae6a923ce14d74f586c8413bbcc4575b5f ├── ✔ Packages [143 packages] ├── ✔ File digests [739 files] ├── ✔ File metadata [739 locations] └── ✔ Executables [109 executables] ✔ Scanned for vulnerabilities [0 vulnerability matches] ├── by severity: 0 critical, 0 high, 0 medium, 0 low, 0 negligible └── by status: 0 fixed, 0 not-fixed, 0 ignored No vulnerabilities found

So by changing a single line, we've reduced the size by 243MB and went from 82 CVEs to zero. That's pretty good for such a simple change! However, we can still do more if we're willing to put in a little more effort. 

The current image still includes build tooling and source code that we don't need in the production image. If we change to use a multi-stage build, we can get rid of several dependencies like these. This reduces software bloat and the attack surface in the image (if an application doesn't exist in the image, it can't be exploited). Here's what such a Dockerfile looks like:

-- CODE language-bash -- FROM cgr.dev/chainguard/maven AS builder WORKDIR /work COPY src/ src/ COPY pom.xml pom.xml RUN mvn clean package RUN REPOSITORY=$(mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout) && rm -rf ${REPOSITORY} FROM cgr.dev/chainguard/jre AS runner WORKDIR /app COPY --from=builder /work/target/java-demo-app-1.0.0.jar . ENTRYPOINT ["java", "-jar", "java-demo-app-1.0.0.jar"]

Note that there are two FROM lines, the first one creates the "builder" image much as before and the second (named "runner") copies the built JAR from the builder step into the smaller cgr.dev/chainguard/jre image. The result is a container image that only contains the JRE and the built application we want to run.

Let's build this Dockerfile and see what we get:

-- CODE language-bash -- docker build -t maven-app-multi-cg .

Look at the size again:

-- CODE language-bash -- docker images maven-app-multi-cg REPOSITORY TAG IMAGE ID CREATED SIZE maven-app-multi-cg latest 0657f7e2914a 2 minutes ago 325MB

And the CVEs:

-- CODE language-bash -- grype docker:maven-app-multi-cg ✔ Vulnerability DB [no update available] ✔ Loaded image maven-app-multi-cg:latest ✔ Parsed image sha256:0657f7e2914abb384c60871ee2c5588eae2132ac0604b294b63c58c24d45a72e ✔ Cataloged contents 9429c21132b92721f52bce3b324978a082ebf33ce9a964846f6e183a7696069e ├── ✔ Packages [61 packages] ├── ✔ File digests [823 files] ├── ✔ File metadata [823 locations] └── ✔ Executables [86 executables] ✔ Scanned for vulnerabilities [0 vulnerability matches] ├── by severity: 0 critical, 0 high, 0 medium, 0 low, 0 negligible └── by status: 0 fixed, 0 not-fixed, 0 ignored No vulnerabilities found

The image still has zero CVEs but the size has dropped by a further 17 MB. Also note that the Grype result shows there are only 61 packages in the image, compared to the previous 143 (and 254 in the original). These packages all represent installed software that could potentially be used by an attacker against you in a “living off the land” method. They will also accumulate CVEs over time if not updated. By keeping the number of packages lower, we reduce complexity and improve security for container images.

All the instructions in this blog use the free, Developer tier of Chainguard Images that are available for you to use today. If you need a tagged or specific version of the JDK, JRE or Maven, please take a look at our Production Chainguard Images and contact us.

Related articles

Ready to lock down your supply chain?

Talk to our customer obsessed, community-driven team.