Revolutionizing Container Security: Implementing Chainguard Images for Java Applications


In the quest for truly secure containerized Java applications, the base image selection is foundational. Traditional Java images often contain unnecessary packages, shells, and package managers that expand the attack surface. Chainguard Images represent a paradigm shift—providing minimal, secure-by-design container images that are free of known vulnerabilities (CVE-free) and built with software supply chain security as a core principle.

What are Chainguard Images?

Chainguard Images are a collection of container images designed with security as the primary focus. Key characteristics:

  • Minimal: Contain only the application and its runtime dependencies
  • Distroless: No shell, package managers, or unnecessary binaries
  • CVE-Free: Regularly rebuilt and scanned to eliminate known vulnerabilities
  • SBOM & SLSA Compliant: Include Software Bill of Materials and meet Supply-chain Levels for Software Artifacts
  • Signed & Attested: Cryptographically signed for provenance verification

For Java applications, Chainguard provides specialized images that contain only the JVM and your application, drastically reducing the attack surface.

Why Chainguard Images for Java Applications?

Traditional Java container images suffer from several security issues:

  1. Large Attack Surface: Full Linux distributions with hundreds of packages
  2. Shell Access: /bin/bash or /bin/sh allows command execution if compromised
  3. Package Managers: apt, apk, or yum can be abused to install malware
  4. Known Vulnerabilities: Base images often contain known CVEs
  5. Unnecessary Components: Debugging tools, compilers, and utilities that aren't needed at runtime

Chainguard Images address all these issues for Java workloads.

Available Chainguard Images for Java

Chainguard offers several Java-focused images:

  • cgr.dev/chainguard/jre: Just the Java Runtime Environment
  • cgr.dev/chainguard/jdk: Java Development Kit (includes compiler)
  • cgr.dev/chainguard/jre-openjdk: OpenJDK-based JRE
  • cgr.dev/chainguard/jdk-openjdk: OpenJDK-based JDK
  • Wolfi-based images: Built on the Wolfi Linux distribution (CVE-free)

Migrating to Chainguard Images

1. From OpenJDK to Chainguard JRE

# ❌ Traditional Java image (vulnerable, large)
FROM openjdk:17-jre-slim
# 200+ packages, shell, package manager, known CVEs
# ✅ Chainguard Java image (minimal, secure)
FROM cgr.dev/chainguard/jre:latest
# ~15 packages, no shell, CVE-free
USER nonroot:nonroot
COPY --chown=nonroot:nonroot target/app.jar /app/app.jar
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

2. Complete Java Application Dockerfile

# Multi-stage build with Chainguard
# Stage 1: Build with JDK
FROM cgr.dev/chainguard/jdk:latest as builder
WORKDIR /build
# Copy source and build
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src
# Build application
RUN ./mvnw clean package -DskipTests
# Stage 2: Runtime with JRE
FROM cgr.dev/chainguard/jre:latest
# Install application
COPY --from=builder /build/target/*.jar /app/app.jar
# Set non-root user (pre-configured in Chainguard)
USER nonroot:nonroot
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["wget", "-q", "--spider", "http://localhost:8080/actuator/health"]
# Run application
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

3. Spring Boot Application with Chainguard

# Spring Boot with Chainguard
FROM cgr.dev/chainguard/jre:latest
# Add required certificates (if needed)
# RUN apk add --no-cache ca-certificates && update-ca-certificates
# Copy Spring Boot executable JAR
COPY target/spring-boot-app.jar /app/spring-boot-app.jar
# Use nonroot user (already exists in Chainguard images)
USER nonroot:nonroot
# Set Java options for containerized environment
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
# Expose port
EXPOSE 8080
# Entrypoint
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/spring-boot-app.jar"]

Note: Chainguard images don't include a shell by default. For Spring Boot's layered JAR support, we might need a minimal shell.

4. Alternative: Wolfi-based Spring Boot Image

# Use Wolfi base with shell for layered JARs
FROM cgr.dev/chainguard/wolfi-base:latest
# Install minimal JRE and shell
RUN apk add openjdk-17-jre bash
# Create non-root user
RUN adduser -D -u 1000 appuser
# Copy application
COPY --chown=appuser:appuser target/*.jar /app/app.jar
# Switch to non-root user
USER appuser
# Entrypoint with shell for layered JAR extraction
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

Kubernetes Deployment with Chainguard Images

1. Basic Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
name: java-chainguard-app
labels:
app: java-chainguard-app
spec:
replicas: 3
selector:
matchLabels:
app: java-chainguard-app
template:
metadata:
labels:
app: java-chainguard-app
spec:
# Security context - Chainguard runs as nonroot by default
securityContext:
runAsNonRoot: true
runAsUser: 65532  # nonroot user ID in Chainguard images
runAsGroup: 65532 # nonroot group ID
containers:
- name: java-app
image: cgr.dev/chainguard/jre:latest
# No shell escape possible - image doesn't contain /bin/sh
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
env:
- name: JAVA_TOOL_OPTIONS
value: >
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-Djava.security.egd=file:/dev/./urandom
ports:
- containerPort: 8080
name: http
volumeMounts:
- name: tmp
mountPath: /tmp
- name: logs
mountPath: /tmp/logs
livenessProbe:
exec:
command:
- java
- -cp
- /app/app.jar
- org.springframework.boot.loader.JarLauncher
- --spring.profiles.active=kubernetes
- --management.endpoint.health.probes.enabled=true
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
volumes:
- name: tmp
emptyDir:
sizeLimit: 100Mi
- name: logs
emptyDir:
sizeLimit: 1Gi

2. StatefulSet with Persistent Storage

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: java-stateful-chainguard
spec:
serviceName: "java-app"
replicas: 2
selector:
matchLabels:
app: java-stateful-chainguard
template:
metadata:
labels:
app: java-stateful-chainguard
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65532
fsGroup: 65532
containers:
- name: java-app
image: cgr.dev/chainguard/jre:latest
securityContext:
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: data
mountPath: /app/data
- name: config
mountPath: /app/config
readOnly: true
# Use startup probe for JVM initialization
startupProbe:
httpGet:
path: /actuator/health
port: 8080
failureThreshold: 30
periodSeconds: 10
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "fast-ssd"
resources:
requests:
storage: 10Gi

Advanced Configuration for Java Applications

1. Custom Chainguard Image with Dependencies

# Custom Chainguard image with additional libraries
FROM cgr.dev/chainguard/jre:latest
# Install additional dependencies (Wolfi packages)
RUN apk add --no-cache \
tzdata \
curl \
ca-certificates \
&& update-ca-certificates
# Set timezone
ENV TZ=UTC
# Copy application
COPY target/app.jar /app/app.jar
# Create directory for writable files
RUN mkdir -p /app/data /app/logs \
&& chown -R nonroot:nonroot /app
USER nonroot:nonroot
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

2. Multi-architecture Builds

# Build for multiple architectures
FROM --platform=$BUILDPLATFORM cgr.dev/chainguard/jdk:latest as builder
WORKDIR /build
COPY . .
RUN ./mvnw clean package -DskipTests
# Final image
FROM cgr.dev/chainguard/jre:latest
COPY --from=builder /build/target/*.jar /app/app.jar
USER nonroot:nonroot
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

Build with:

docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .

Security Features and Verification

1. Verify Image Signatures

# Verify Chainguard image signatures
cosign verify \
--certificate-identity-regexp ".*chainguard.dev" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
cgr.dev/chainguard/jre:latest
# Expected output includes signature verification

2. Generate and Verify SBOM

# Generate SBOM from Chainguard image
syft cgr.dev/chainguard/jre:latest -o cyclonedx-json > sbom.json
# Verify SBOM attestation
cosign verify-attestation \
--type cyclonedx \
cgr.dev/chainguard/jre:latest
# Scan for vulnerabilities in SBOM
trivy sbom sbom.json

3. Vulnerability Scanning

# Scan Chainguard image
trivy image cgr.dev/chainguard/jre:latest
# Compare with traditional image
trivy image openjdk:17-jre-slim
# Expected: Chainguard shows 0 or very few vulnerabilities

CI/CD Pipeline Integration

1. GitHub Actions Workflow

name: Build and Deploy with Chainguard
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Maven
run: mvn clean package -DskipTests
- name: Build Docker image with Chainguard
run: |
docker build -t myapp:latest .
- name: Scan for vulnerabilities
run: |
docker run --rm \
aquasec/trivy:latest \
image --severity HIGH,CRITICAL myapp:latest
- name: Verify image signature
run: |
docker run --rm \
gcr.io/projectsigstore/cosign:latest \
verify --key https://www.chainguard.dev/cosign.pub \
myapp:latest
- name: Push to registry
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login \
-u ${{ secrets.DOCKER_USERNAME }} \
--password-stdin
docker push myapp:latest

2. GitLab CI Pipeline

stages:
- build
- security
- deploy
build-chainguard:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
security-scan:
stage: security
image: aquasec/trivy:latest
script:
- trivy image --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- trivy sbom --format cyclonedx $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
deploy:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl set image deployment/java-app \
java-app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- main

Handling Application Requirements

1. Applications Needing Shell Access

For applications that truly need shell access (e.g., for startup scripts):

# Use Wolfi base with minimal shell
FROM cgr.dev/chainguard/wolfi-base:latest
# Install only what's needed
RUN apk add openjdk-17-jre bash
# Create app user
RUN adduser -D -u 10000 appuser
USER appuser
COPY app.jar /app/app.jar
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

2. Debugging and Troubleshooting

Since Chainguard images lack shells and debugging tools:

# Kubernetes ephemeral debug container
apiVersion: v1
kind: Pod
metadata:
name: java-app-debug
spec:
shareProcessNamespace: true  # Share PID namespace
containers:
- name: java-app
image: cgr.dev/chainguard/jre:latest
securityContext:
runAsUser: 65532
# Main application
- name: debug
image: busybox:latest
command: ["sleep", "3600"]
securityContext:
runAsUser: 0  # Root for debugging
# Debug container with shell access

Performance and Resource Benefits

Chainguard images offer significant advantages:

  1. Smaller Image Size: # Traditional Java image openjdk:17-jre-slim ~200MB # Chainguard Java image cgr.dev/chainguard/jre ~80MB
  2. Faster Startup: Fewer layers and smaller size reduce pull and startup times
  3. Reduced Memory: Minimal OS footprint leaves more memory for Java heap
  4. Lower Storage Costs: Smaller images reduce registry storage requirements

Migration Strategy

  1. Assessment: Identify which applications can run without shell
  2. Testing: Deploy Chainguard images in staging environments
  3. Monitoring: Ensure applications work correctly with minimal images
  4. Gradual Rollout: Migrate non-critical applications first
  5. Documentation: Update runbooks for troubleshooting without shell access

Conclusion

Chainguard Images represent the future of secure containerized Java applications. By providing minimal, CVE-free images that eliminate unnecessary components, they drastically reduce the attack surface and improve the security posture of Java workloads.

For organizations serious about supply chain security, Chainguard Images offer:

  • Provenance Verification: Cryptographically signed images with SBOMs
  • Minimal Attack Surface: No shells, package managers, or unnecessary binaries
  • Continuous Security: Regularly rebuilt to eliminate CVEs
  • Compliance Ready: SLSA compliance and audit trails

Adopting Chainguard Images requires a shift in mindset—from "debuggable" containers to "secure-by-default" containers. The trade-off of losing shell access is more than compensated by the dramatic improvement in security. For Java applications in production environments, Chainguard Images provide a foundation for building truly secure, resilient, and maintainable containerized applications.

Advanced Java Container Security, Sandboxing & Trusted Runtime Environments

https://macronepal.com/blog/sandboxing-java-applications-implementing-landlock-lsm-for-enhanced-container-security/
Explains using Linux Landlock LSM to sandbox Java applications by restricting file system and resource access without root privileges, improving application-level isolation and reducing attack surface.

https://macronepal.com/blog/gvisor-sandbox-integration-in-java-complete-guide/
Explains integrating gVisor with Java to provide a user-space kernel sandbox that intercepts system calls and isolates applications from the host operating system for stronger security.

https://macronepal.com/blog/selinux-for-java-mandatory-access-control-for-jvm-applications/
Explains how SELinux enforces Mandatory Access Control (MAC) policies on Java applications, strictly limiting what files, processes, and network resources the JVM can access.

https://macronepal.com/java/a-comprehensive-guide-to-intel-sgx-sdk-integration-in-java/
Explains Intel SGX integration in Java, allowing sensitive code and data to run inside secure hardware enclaves that remain protected even if the OS is compromised.

https://macronepal.com/blog/building-a-microvm-runtime-with-aws-firecracker-in-java-a-comprehensive-guide/
Explains using AWS Firecracker microVMs with Java to run workloads in lightweight virtual machines that provide strong isolation with near-container performance efficiency.

https://macronepal.com/blog/enforcing-mandatory-access-control-implementing-apparmor-for-java-applications/
Explains AppArmor security profiles for Java applications, enforcing rules that restrict file access, execution rights, and system-level permissions.

https://macronepal.com/blog/rootless-containers-in-java-secure-container-operations-without-root/
Explains running Java applications in rootless containers using Linux user namespaces so containers operate securely without requiring root privileges.

https://macronepal.com/blog/unlocking-container-security-harnessing-user-namespaces-in-java/
Explains Linux user namespaces, which isolate user and group IDs inside containers to improve privilege separation and enhance container security for Java workloads.

https://macronepal.com/blog/secure-bootstrapping-in-java-comprehensive-trust-establishment-framework/
Explains secure bootstrapping in Java, focusing on how systems establish trust during startup using secure key management, identity verification, and trusted configuration loading.

https://macronepal.com/blog/securing-java-applications-with-chainguard-wolfi-a-comprehensive-guide-2/
Explains using Chainguard/Wolfi minimal container images to secure Java applications by reducing unnecessary packages, minimizing vulnerabilities, and providing a hardened runtime environment.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper