-->

Thursday, September 12, 2024

Step-by-Step Guide for Hardening Docker Images and transform vulnerable Docker images to secure images with code

 In our journey to build secure and efficient containerized applications, we often encounter vulnerabilities that need to be addressed. In this article, we'll explore how updating our Dockerfile and implementing DevSecOps practices can significantly improve the security of our container images. We'll walk through the process of identifying vulnerabilities, fixing them, and creating a more secure runtime environment.


The Initial Vulnerability Scan

In our previous article on Advanced Maven Application Containerisation and Security Scanning with GitLab CI/CD, we set up a pipeline to build and scan our container image. The initial scan revealed several vulnerabilities in our image. You can view the full scan results here.


Analyzing the Vulnerabilities

The scan detected a total of 177 vulnerabilities in our initial image. These vulnerabilities ranged from low to critical severity and included issues in various packages. Some notable examples include:

  1. Critical vulnerability CVE-2022-37434 in zlib1g
  2. High severity vulnerability CVE-2022-42898 in libk5crypto3
  3. Medium severity vulnerability CVE-2022-3821 in libsystemd0

These vulnerabilities pose potential security risks and need to be addressed to ensure a secure runtime environment for our application.


Updating the Dockerfile

To address these vulnerabilities, we made several improvements to our Dockerfile. Let's compare the previous version with the updated one:

Previous Dockerfile

# Stage 1: Build the application
FROM maven:3.8.1-openjdk-11-slim AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package

# Stage 2: Create the runtime image
FROM openjdk:11-jre-slim
WORKDIR /app
# Copy the built artifact from the build stage
COPY --from=build /app/target/todo-app.war ./app.war
# Specify the command to run your application
CMD ["java", "-jar", "app.war"]

Updated Dockerfile


# Stage 1: Build the application FROM maven:3.8.1-openjdk-11-slim AS build WORKDIR /app COPY pom.xml . # Download dependencies RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package # Stage 2: Create the runtime image FROM adoptopenjdk/openjdk11:jre-11.0.11_9-alpine # Update packages and remove unnecessary ones RUN apk update && \ apk upgrade && \ apk add --no-cache dumb-init && \ rm -rf /var/cache/apk/* # Create a non-root user RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser WORKDIR /app # Copy the built artifact from the build stage COPY --from=build --chown=appuser:appgroup /app/target/todo-app.war ./app.war # Use dumb-init as the entrypoint ENTRYPOINT ["/usr/bin/dumb-init", "--"] # Specify the command to run your application CMD ["java", "-jar", "app.war"] # Add healthcheck HEALTHCHECK --interval=30s --timeout=3s \ CMD wget -q --spider http://localhost:8080/actuator/health || exit 1

Let's break down the improvements:

Previous DockerfileUpdated DockerfileImprovement
Used openjdk:11-jre-slim as the base imageSwitched to adoptopenjdk/openjdk11:jre-11.0.11_9-alpineAlpine-based image with fewer vulnerabilities
No explicit package updatesAdded apk update && apk upgradeEnsures all packages are up to date
No unnecessary package removalAdded rm -rf /var/cache/apk/*Removes unnecessary package cache
Root userCreated non-root user appuserImproves security by not running as root
No init systemAdded dumb-initProper init system for handling signals
No health checkAdded HEALTHCHECKEnables container health monitoring
No explicit dependency cachingAdded mvn dependency:go-offlineImproves build performance
No proper ownership of copied filesAdded --chown=appuser:appgroup to COPYEnsures correct file ownership

Additional Improvements

  1. Healthcheck Implementation: The addition of a HEALTHCHECK instruction is crucial for container orchestration systems. It allows the system to monitor the health of the application and take appropriate actions if the container becomes unhealthy. This can prevent downtime and improve overall system reliability.
  2. Multi-stage Build Optimization: While both Dockerfiles use multi-stage builds, the updated version optimizes this process by caching dependencies separately. This can significantly speed up subsequent builds.
  3. Security Scanning Integration: Although not visible in the Dockerfile itself, integrating security scanning (as demonstrated in our GitLab CI pipeline) is a crucial DevSecOps practice. It allows for continuous monitoring of vulnerabilities.
  4. Environment Variable Usage: Consider using environment variables for configurable values, such as the Java heap size or application-specific settings. This wasn't implemented in either version but could be a valuable addition.
  5. Layer Optimization: The updated Dockerfile combines RUN commands to reduce the number of layers, which can help reduce the overall image size.
  6. Explicit Port Exposure: Adding an EXPOSE instruction to document which ports the application uses can improve clarity, although it's not strictly necessary for functionality.
  7. Version Pinning: While we've pinned versions in the base images, consider pinning versions for installed packages (e.g., dumb-init) to ensure reproducible builds.

The Impact of Our Changes

After implementing these changes, we ran our pipeline again. The new scan results, which you can view here, showed a significant improvement. No vulnerabilities were detected in the updated image!

This dramatic reduction in vulnerabilities demonstrates the effectiveness of our approach in implementing DevSecOps practices and improving container security.


Key Improvements and Their Benefits

  1. Base Image Update: Switching to an Alpine-based image significantly reduced the attack surface and eliminated many vulnerabilities present in the previous image.
  2. Package Management: Regularly updating packages and removing unnecessary ones helps in keeping the image secure and reduces its size.
  3. Non-root User: Running the application as a non-root user is a crucial security practice that limits the potential damage if the container is compromised.
  4. Init System: Using dumb-init ensures proper signal handling and zombie process reaping, improving container stability.
  5. Health Checks: Implementing health checks allows for better monitoring and self-healing of the containerized application.
  6. Build Optimization: Caching dependencies improves build performance and consistency.

Approach for Overcoming Vulnerabilities

If you encounter vulnerabilities in your container images, consider the following approach:

  1. Analyze the Scan Results: Carefully review the vulnerability report to understand the nature and severity of each issue.
  2. Update Base Images: Always use the latest stable and security-focused base images.
  3. Keep Packages Updated: Regularly update all packages in your image.
  4. Minimize Image Content: Include only necessary components in your image to reduce the attack surface.
  5. Implement Security Best Practices: Use non-root users, proper init systems, and other security enhancements.
  6. Continuous Monitoring: Implement regular scans and updates as part of your CI/CD pipeline.
  7. Stay Informed: Keep up with security advisories and updates for the technologies you use.

Conclusion

By updating our Dockerfile and implementing DevSecOps best practices, we significantly improved the security of our container image. This approach not only fixed existing vulnerabilities but also set up a foundation for maintaining secure images in the future.

Remember, security is an ongoing process. Regularly updating and scanning your images, along with following best practices, is crucial in maintaining a robust and secure containerized environment.


This article was written by Ankit Mittal. For more DevOps insights and projects, check out:

Feel free to explore these repositories for more DevOps solutions and best practices!

0 comments:

Post a Comment