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:
- Large Attack Surface: Full Linux distributions with hundreds of packages
- Shell Access:
/bin/bashor/bin/shallows command execution if compromised - Package Managers:
apt,apk, oryumcan be abused to install malware - Known Vulnerabilities: Base images often contain known CVEs
- 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 Environmentcgr.dev/chainguard/jdk: Java Development Kit (includes compiler)cgr.dev/chainguard/jre-openjdk: OpenJDK-based JREcgr.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:
- Smaller Image Size:
# Traditional Java image openjdk:17-jre-slim ~200MB # Chainguard Java image cgr.dev/chainguard/jre ~80MB - Faster Startup: Fewer layers and smaller size reduce pull and startup times
- Reduced Memory: Minimal OS footprint leaves more memory for Java heap
- Lower Storage Costs: Smaller images reduce registry storage requirements
Migration Strategy
- Assessment: Identify which applications can run without shell
- Testing: Deploy Chainguard images in staging environments
- Monitoring: Ensure applications work correctly with minimal images
- Gradual Rollout: Migrate non-critical applications first
- 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.