Structuring textual enterprise
data to solve real-life problems
with uncompromised quality.

Case studies

Contacts

Sienkiewicza 40a 15-004 Białystok Poland

sales@bluerider.software

+48-791-963-658

Security

Best practices for hardening Docker Container Images

2024-09-16, Sebastian Burzyński

In today’s fast-paced development world, Docker containers have become the go-to solution for packaging and deploying applications. They offer a lot of convenience, but with that comes the risk of vulnerabilities slipping through the cracks. Securing container images is as vital as writing secure code, yet many overlook this crucial step. A single vulnerable image can compromise an entire application, putting sensitive data at risk. By following some simple best practices, you can significantly reduce the chances of security issues in production. In this article, we’ll explore practical steps you can take to harden your Docker images and keep your applications safe.

Caching in Docker is a double-edged sword. On one hand, it speeds up builds by reusing layers from previous runs, saving a ton of time, especially in large projects. But if you’re not careful, relying too heavily on cached layers can cause you to miss important updates or include outdated and potentially insecure dependencies. Simply building without cache (docker build –no-cache) might seem like a good idea, but it can slow down your builds significantly and doesn’t guarantee you’re pulling in the latest dependencies if they’re pinned to specific versions.

Here are some tips to strike a balance between speed and security.

  • Always pin your dependencies to specific versions in your requirements.txt or package.json files. This ensures consistency and helps avoid unexpected issues.
# Example pyproject.toml file
[tool.poetry]
name = "BRS"
version = "0.1.0"
description = "Example pyproject.toml file"
authors = ["Sebastian Burzyński <sebastianburzynski@bluerider.software>"]

[tool.poetry.dependencies]
python = "^3.12"
requests = "^2.31.0"

[[tool.poetry.source]]
name = "pypi"
url = "https://pypi.org/simple"
default = true

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
 
  • When installing system packages, make sure to run apt-get update before apt-get install to fetch the latest package lists. Don’t forget to clean up afterward to keep your image slim.
# Example Dockerfile layer
RUN apt-get update && \
    apt-get install -y --no-install-recommends your-package && \
    rm -rf /var/lib/apt/lists/*
 
  • Arrange your Dockerfile commands so that the layers that change less frequently are built first. This way, Docker can cache those layers and skip rebuilding them unless necessary.
  • If you need to force Docker to rebuild certain layers (like when you want to make sure you’re getting the latest updates), you can use build arguments.
docker build --no-cache -t my-secure-image . 

For extra control, you can clear Docker’s build cache entirely if you suspect stale layers

But use this sparingly, as it removes all cached layers and can slow down your builds.

docker builder prune 

The base image you choose for your Docker container is crucial to both performance and security. Larger base images often come with unnecessary libraries, increasing the attack surface and the size of your image. Minimal base images, like alpine, contain only what’s essential, reducing potential vulnerabilities and keeping your image lean. However, minimal images may require you to manually install necessary dependencies, which can add complexity. The trade-off between size and convenience is worth considering, but for production, a smaller, minimal base image usually leads to better security and faster deployments.

Slim Python Image Dockerfile

FROM python:3.10-slim

# Install scikit-learn; dependencies like numpy and scipy are handled easily
RUN pip install scikit-learn

COPY ./app.py /app.py

CMD ["python", "app.py"] 

Alpine Python Image Dockerfile

FROM python:3.10-alpine

# Manually installing build dependencies necessary for numpy and scipy, required by scikit-learn
RUN apk add --no-cache gcc g++ musl-dev python3-dev libstdc++ lapack-dev gfortran \
    && pip install scikit-learn

COPY ./app.py /app.py

CMD ["python", "app.py"] 

Build and compare sizes

>> docker build -t python-slim -f Dockerfile.slim .
>> docker build -t python-alpine -f Dockerfile.alpine .

>> docker images

REPOSITORY                 TAG                    IMAGE ID       CREATED          SIZE
python-alpine              latest                 5c80f8159401   21 seconds ago   816MB
python-slim                latest                 be5a4d380175   39 seconds ago   468MB
 

Multi-stage builds are a great way to keep your Docker images lean and secure by separating the build environment from the runtime environment. Instead of carrying over all the tools and dependencies needed for building your application, you use one stage to build the app and a separate, more minimal stage to run it. This technique reduces the final image size, which not only boosts performance but also minimizes the attack surface by including only what’s necessary to run your application. It’s especially useful for projects with heavy dependencies like React apps, where only the build output is needed in production.

These Dockerfiles illustrate the basic concept of multi-stage builds. For production, ensure you’re following best practices

# Stage 1: Build the React app
FROM node:18 AS build

WORKDIR /app

COPY package*.json ./

RUN npm ci

COPY . .

RUN npm run build

# Stage 2: Serve the app with Nginx
FROM nginx:alpine

COPY --from=build /app/build /usr/share/nginx/html

CMD ["nginx", "-g", "daemon off;"] 

Even if you’re following best practices, your Docker images may still contain vulnerabilities, especially if you’re pulling in base images or dependencies from external sources. Regularly scanning your images for known security flaws is a crucial step in ensuring the safety of your containerized applications. Tools like Docker Scan, Trivy, or Clair can help you identify vulnerabilities early in your pipeline, giving you a chance to patch or update components before they reach production. Automated scanning as part of your CI/CD pipeline is a smart way to stay ahead of security issues without slowing down development.

>> trivy image python:3.12 --severity CRITICAL
2024-09-09T15:15:28+02:00	INFO	[vuln] Vulnerability scanning is enabled
2024-09-09T15:15:28+02:00	INFO	[secret] Secret scanning is enabled
2024-09-09T15:15:28+02:00	INFO	[secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning
2024-09-09T15:15:28+02:00	INFO	[secret] Please see also https://aquasecurity.github.io/trivy/v0.55/docs/scanner/secret#recommendation for faster secret detection
2024-09-09T15:15:31+02:00	INFO	[python] License acquired from METADATA classifiers may be subject to additional terms	name="pip" version="24.2"
2024-09-09T15:15:32+02:00	INFO	Detected OS	family="debian" version="12.7"
2024-09-09T15:15:32+02:00	INFO	[debian] Detecting vulnerabilities...	os_version="12" pkg_num=429
2024-09-09T15:15:32+02:00	INFO	Number of language-specific files	num=1
2024-09-09T15:15:32+02:00	INFO	[python-pkg] Detecting vulnerabilities...
2024-09-09T15:15:32+02:00	WARN	Using severities from other vendors for some vulnerabilities. Read https://aquasecurity.github.io/trivy/v0.55/docs/scanner/vulnerability#severity-selection for details.

python:3.12 (debian 12.7)

Total: 14 (CRITICAL: 14)

┌───────────────────┬────────────────┬──────────┬──────────────┬───────────────────┬───────────────┬─────────────────────────────────────────────────────────────┐
│      Library      │ Vulnerability  │ Severity │    Status    │ Installed Version │ Fixed Version │                            Title                            │
├───────────────────┼────────────────┼──────────┼──────────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤
│ git               │ CVE-2024-32002 │ CRITICAL │ affected     │ 1:2.39.2-1.1      │               │ git: Recursive clones RCE                                   │
│                   │                │          │              │                   │               │ https://avd.aquasec.com/nvd/cve-2024-32002                  │
├───────────────────┤                │          │              │                   ├───────────────┤                                                             │
│ git-man           │                │          │              │                   │               │                                                             │
│                   │                │          │              │                   │               │                                                             │
├───────────────────┼────────────────┤          │              ├───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤
│ libaom3           │ CVE-2023-6879  │          │              │ 3.6.0-1+deb12u1   │               │ aom: heap-buffer-overflow on frame size change              │
│                   │                │          │              │                   │               │ https://avd.aquasec.com/nvd/cve-2023-6879                   │
├───────────────────┼────────────────┤          │              ├───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤
│ libexpat1         │ CVE-2024-45490 │          │              │ 2.5.0-1           │               │ libexpat: Negative Length Parsing Vulnerability in libexpat │
│                   │                │          │              │                   │               │ https://avd.aquasec.com/nvd/cve-2024-45490                  │
│                   ├────────────────┤          │              │                   ├───────────────┼─────────────────────────────────────────────────────────────┤
│                   │ CVE-2024-45491 │          │              │                   │               │ libexpat: Integer Overflow or Wraparound                    │
│                   │                │          │              │                   │               │ https://avd.aquasec.com/nvd/cve-2024-45491                  │
│                   ├────────────────┤          │              │                   ├───────────────┼─────────────────────────────────────────────────────────────┤
│                   │ CVE-2024-45492 │          │              │                   │               │ libexpat: integer overflow                                  │
│                   │                │          │              │                   │               │ https://avd.aquasec.com/nvd/cve-2024-45492                  │
├───────────────────┼────────────────┤          │              │                   ├───────────────┼─────────────────────────────────────────────────────────────┤
│ libexpat1-dev     │ CVE-2024-45490 │          │              │                   │               │ libexpat: Negative Length Parsing Vulnerability in libexpat │
│                   │                │          │              │                   │               │ https://avd.aquasec.com/nvd/cve-2024-45490                  │
│                   ├────────────────┤          │              │                   ├───────────────┼─────────────────────────────────────────────────────────────┤
│                   │ CVE-2024-45491 │          │              │                   │               │ libexpat: Integer Overflow or Wraparound                    │
│                   │                │          │              │                   │               │ https://avd.aquasec.com/nvd/cve-2024-45491                  │
│                   ├────────────────┤          │              │                   ├───────────────┼─────────────────────────────────────────────────────────────┤
│                   │ CVE-2024-45492 │          │              │                   │               │ libexpat: integer overflow                                  │
│                   │                │          │              │                   │               │ https://avd.aquasec.com/nvd/cve-2024-45492                  │
├───────────────────┼────────────────┤          │              ├───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤
│ libopenexr-3-1-30 │ CVE-2023-5841  │          │              │ 3.1.5-5           │               │ OpenEXR: Heap Overflow in Scanline Deep Data Parsing        │
│                   │                │          │              │                   │               │ https://avd.aquasec.com/nvd/cve-2023-5841                   │
├───────────────────┤                │          │              │                   ├───────────────┤                                                             │
│ libopenexr-dev    │                │          │              │                   │               │                                                             │
│                   │                │          │              │                   │               │                                                             │
├───────────────────┼────────────────┤          │              ├───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤
│ wget              │ CVE-2024-38428 │          │              │ 1.21.3-1+b2       │               │ wget: Misinterpretation of input may lead to improper       │
│                   │                │          │              │                   │               │ behavior                                                    │
│                   │                │          │              │                   │               │ https://avd.aquasec.com/nvd/cve-2024-38428                  │
├───────────────────┼────────────────┤          ├──────────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤
│ zlib1g            │ CVE-2023-45853 │          │ will_not_fix │ 1:1.2.13.dfsg-1   │               │ zlib: integer overflow and resultant heap-based buffer      │
│                   │                │          │              │                   │               │ overflow in zipOpenNewFileInZip4_6                          │
│                   │                │          │              │                   │               │ https://avd.aquasec.com/nvd/cve-2023-45853                  │
├───────────────────┤                │          │              │                   ├───────────────┤                                                             │
│ zlib1g-dev        │                │          │              │                   │               │                                                             │
│                   │                │          │              │                   │               │                                                             │
│                   │                │          │              │                   │               │                                                             │
└───────────────────┴────────────────┴──────────┴──────────────┴───────────────────┴───────────────┴─────────────────────────────────────────────────────────────┘
 

In this example, scanning a python:3.12 image reveals multiple critical vulnerabilities, which raises a crucial question: should you stop using the image, wait for fixes, or create your own? When dealing with vulnerabilities in widely-used base images, there are a few factors to consider:

  • Severity and Exploitability: While the scan flags vulnerabilities as critical, it’s important to assess whether these vulnerabilities can be exploited in your specific context. Some might be in components (e.g., git, libexpat which is a library to parse XML files) that your container doesn’t even use. In such cases, mitigation may not be urgent.
  • Custom Base Images: If vulnerabilities are found in critical components or you can’t wait for upstream fixes, creating a custom base image could be an option. This involves starting with a minimal base image (e.g., python:3.12-slim or python:3.12-alpine), removing unused components, and applying patches manually. While more work, it provides greater control over your image’s security.
  • Upstream Fixes: It’s common for upstream image maintainers to patch vulnerabilities quickly. In the case of widely-used official images like python, it’s often a good idea to monitor for updates, as fixes can be released relatively fast. Keeping an eye on security bulletins and scanning your images frequently ensures you’re not caught off guard.
  • Mitigation in CI/CD: Even when you continue using an image with known vulnerabilities, incorporating compensating controls, such as application-level hardening or network isolation, can reduce risk. Automated vulnerability scanning integrated into your CI/CD pipeline will notify you as soon as updates become available.

In short, when critical vulnerabilities are identified, it’s essential to assess their impact on your use case, weigh the trade-offs between creating a custom image and waiting for upstream fixes, and ensure that your CI/CD pipeline provides early warnings of security risks.

So let’s see how do python:3.12-slim and python:3.12-alpine stands

>> trivy image python:3.12-slim --severity CRITICAL
2024-09-17T09:31:40+02:00	INFO	[vuln] Vulnerability scanning is enabled
2024-09-17T09:31:40+02:00	INFO	[secret] Secret scanning is enabled
2024-09-17T09:31:40+02:00	INFO	[secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning
2024-09-17T09:31:40+02:00	INFO	[secret] Please see also https://aquasecurity.github.io/trivy/v0.55/docs/scanner/secret#recommendation for faster secret detection
2024-09-17T09:31:42+02:00	INFO	Detected OS	family="debian" version="12.7"
2024-09-17T09:31:42+02:00	INFO	[debian] Detecting vulnerabilities...	os_version="12" pkg_num=106
2024-09-17T09:31:42+02:00	INFO	Number of language-specific files	num=1
2024-09-17T09:31:42+02:00	INFO	[python-pkg] Detecting vulnerabilities...
2024-09-17T09:31:42+02:00	WARN	Using severities from other vendors for some vulnerabilities. Read https://aquasecurity.github.io/trivy/v0.55/docs/scanner/vulnerability#severity-selection for details.

python:3.12-slim (debian 12.7)

Total: 4 (CRITICAL: 4)

┌───────────┬────────────────┬──────────┬──────────────┬───────────────────┬───────────────┬─────────────────────────────────────────────────────────────┐
│  Library  │ Vulnerability  │ Severity │    Status    │ Installed Version │ Fixed Version │                            Title                            │
├───────────┼────────────────┼──────────┼──────────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤
│ libexpat1 │ CVE-2024-45490 │ CRITICAL │ affected     │ 2.5.0-1           │               │ libexpat: Negative Length Parsing Vulnerability in libexpat │
│           │                │          │              │                   │               │ https://avd.aquasec.com/nvd/cve-2024-45490                  │
│           ├────────────────┤          │              │                   ├───────────────┼─────────────────────────────────────────────────────────────┤
│           │ CVE-2024-45491 │          │              │                   │               │ libexpat: Integer Overflow or Wraparound                    │
│           │                │          │              │                   │               │ https://avd.aquasec.com/nvd/cve-2024-45491                  │
│           ├────────────────┤          │              │                   ├───────────────┼─────────────────────────────────────────────────────────────┤
│           │ CVE-2024-45492 │          │              │                   │               │ libexpat: integer overflow                                  │
│           │                │          │              │                   │               │ https://avd.aquasec.com/nvd/cve-2024-45492                  │
├───────────┼────────────────┤          ├──────────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤
│ zlib1g    │ CVE-2023-45853 │          │ will_not_fix │ 1:1.2.13.dfsg-1   │               │ zlib: integer overflow and resultant heap-based buffer      │
│           │                │          │              │                   │               │ overflow in zipOpenNewFileInZip4_6                          │
│           │                │          │              │                   │               │ https://avd.aquasec.com/nvd/cve-2023-45853                  │
└───────────┴────────────────┴──────────┴──────────────┴───────────────────┴───────────────┴─────────────────────────────────────────────────────────────┘
 
>> trivy image python:3.12-alpine --severity CRITICAL
2024-09-17T09:32:12+02:00	INFO	[vuln] Vulnerability scanning is enabled
2024-09-17T09:32:12+02:00	INFO	[secret] Secret scanning is enabled
2024-09-17T09:32:12+02:00	INFO	[secret] If your scanning is slow, please try '--scanners vuln' to disable secret scanning
2024-09-17T09:32:12+02:00	INFO	[secret] Please see also https://aquasecurity.github.io/trivy/v0.55/docs/scanner/secret#recommendation for faster secret detection
2024-09-17T09:32:13+02:00	INFO	Detected OS	family="alpine" version="3.20.3"
2024-09-17T09:32:13+02:00	INFO	[alpine] Detecting vulnerabilities...	os_version="3.20" repository="3.20" pkg_num=37
2024-09-17T09:32:13+02:00	INFO	Number of language-specific files	num=1
2024-09-17T09:32:13+02:00	INFO	[python-pkg] Detecting vulnerabilities...

python:3.12-alpine (alpine 3.20.3)

Total: 0 (CRITICAL: 0)
 

By default, many Docker containers run as the root user, which poses a security risk. If an attacker manages to exploit a vulnerability within your container, they can gain root access and potentially compromise the entire host system. To mitigate this risk, you should always run your containers as a non-root user. This limits the damage an attacker can do, even if they gain access to the container. You can create a non-root user within the Dockerfile or use Docker’s user flag to ensure the application inside the container runs with limited privileges.

Hardcoding sensitive information like a GitLab token directly in your Docker image is a major security risk. Instead, Docker provides a way to securely pass secrets during the build process using the –mount=type=secret option. This ensures that sensitive data, such as your GitLab token, is available only at build time and isn’t stored in the final image. This keeps your secrets secure while allowing your build to access the required information.

Here’s an example of how to mount a secret during the build process for a GitLab token, ensuring it’s used securely within your Dockerfile

# First, you need to enable BuildKit, which is required for the --mount=type=secret feature.
# You can do this by setting the environment variable before building. 
>> export DOCKER_BUILDKIT=1 
FROM python:3.10-slim

# Define the secret mount
RUN --mount=type=secret,id=GITLAB_TOKEN,mode=0444 \
    POETRY_HTTP_BASIC_GITLAB_PASSWORD=$(cat /run/secrets/GITLAB_TOKEN) \
    poetry install --no-dev --no-interaction --no-ansi --no-root

# Rest of your Dockerfile 
>> docker build --secret id=GITLAB_TOKEN,src=/path/to/gitlab_token.txt -t my-secure-image . 

In this case, the GitLab token is stored in a file (gitlab_token.txt), which is mounted at /run/secrets/GITLAB_TOKEN during the build process. This ensures that the secret is not included in the image layers. The secret can be also incorporated into CI/CD pipeline as an environment variable.

Why This Approach is Secure:

  • The secret is mounted only for the duration of the build process and is never included in the final image.
  • Using mode=0444 ensures the secret is read-only within the container.
  • The token is accessed securely using $(cat /run/secrets/GITLAB_TOKEN) without exposing it in plain text.

Outdated images are one of the easiest entry points for attackers. Docker images, especially those based on popular OS distributions, regularly receive security patches and updates. Keeping your base images and any third-party libraries up-to-date is crucial for maintaining security. The more outdated an image is, the more likely it contains vulnerabilities that have already been discovered and could be exploited. Regularly pulling new images and rebuilding your containers ensures that you’re running on the most secure versions.

When building Docker images, the COPY or ADD commands can inadvertently include everything from your project directory into the image, even files that aren’t necessary or could pose security risks. Without a .dockerignore file, this means things like virtual environments, logs, local configuration files, or source control directories can end up in your Docker image. This leads to larger images, slower build times, and potential security risks if sensitive information (like credentials in .env files) gets baked into the image.

By using a .dockerignore file, you can explicitly exclude these unnecessary or sensitive files. This keeps your image smaller, reduces the attack surface, and ensures that only the essential files needed to run the application are included.

# Exclude Python bytecode files
__pycache__/
*.py[cod]

# Exclude virtual environments
venv/
env/
.venv/
.env/

# Ignore pip cache and other package managers
pip-log.txt
pip-delete-this-directory.txt
*.egg-info/
.eggs/

# Ignore local environment variables and credentials
.env
*.env

# Ignore build artifacts and temporary files
build/
dist/
*.log
*.tmp

# Exclude Git and other source control files
.git/
.gitignore
 

Running containers with a read-only filesystem is a strong security measure that helps limit the risk of tampering. When a container’s filesystem is set to read-only, it prevents unauthorized changes, such as creating, modifying, or deleting files. This makes it much harder for an attacker to cause lasting damage if they gain access, as they won’t be able to alter system files or modify critical application components.

In modern cloud-native environments, many applications are designed to be stateless, meaning they don’t rely on the local filesystem for dynamic data like logs, temporary files, or uploads. Instead, dynamic data is typically stored in external systems like AWS S3 or databases. Offloading file storage to services like S3 not only makes your containers more scalable but also enhances security. By using a read-only filesystem, you further reduce the attack surface, minimizing the risk of persistent threats inside your containers.

For container orchestration platforms such as AWS ECS, or when using tools like Docker Compose, you can also specify that the filesystem should be read-only. This ensures consistency across deployments and reinforces security at scale. Specific directories can still be made writable if necessary, but it’s generally best to treat containers as stateless and handle any dynamic data through external services.

# Docker example
# Run a container with a read-only filesystem, but allow /app/logs to be writable
>> docker run --read-only -v /app/logs:/app/logs myapp
 
# docker-compose example
services:
  myapp:
    image: myapp:latest
    read_only: true
    volumes:
      - /app/logs:/app/logs
 
# AWS ECS Task definition
{
  "containerDefinitions": [
    {
      "name": "myapp",
      "image": "myapp:latest",
      "readonlyRootFilesystem": true,
      "mountPoints": [
        {
          "sourceVolume": "logs",
          "containerPath": "/app/logs"
        }
      ]
    }
  ]
} 

One of the key aspects of securing your Docker images is ensuring that your Dockerfile is free of common issues that can lead to vulnerabilities or inefficiencies. Static code analysis of your Dockerfile helps catch issues early before they become costly mistakes. Enter Hadolint, a powerful tool designed to automatically analyze your Dockerfile for best practices, potential security issues, and inefficiencies. Running Hadolint as part of your CI/CD pipeline helps ensure that your Dockerfiles adhere to Docker’s best practices and security standards.

Hadolint provides guidance on everything from using outdated base images to not following the principle of least privilege. This tool can prevent simple mistakes like missing COPY directives or improper permission setups that could expose your containers to vulnerabilities.

You can learn more about Docker’s official best practices for writing secure and efficient Dockerfiles here, and explore Hadolint’s interactive set of features and rules here.

Here is an example of a very simple Dockerfile which is going to be used with Hadolint. 

P.S. Hadolint can be used with external services, and can generate output specifically related to tools like sonarqube

FROM ubuntu:18.04
RUN apt-get update && apt-get install -y curl
COPY . /app
WORKDIR /app
CMD ["python", "app.py"] 
>> hadolint Dockerfile
Dockerfile:2 DL3008 warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
Dockerfile:2 DL3009 info: Delete the apt-get lists after installing something
Dockerfile:2 DL3015 info: Avoid additional packages by specifying `--no-install-recommends`


>> hadolint Dockerfile -f sonarqube
{
  "issues": [
    {
      "engineId": "Hadolint",
      "primaryLocation": {
        "filePath": "Dockerfile",
        "message": "Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`",
        "textRange": {
          "endColumn": 1,
          "endLine": 2,
          "startColumn": 0,
          "startLine": 2
        }
      },
      "ruleId": "DL3008",
      "severity": "MAJOR",
      "type": "CODE_SMELL"
    },
    {
      "engineId": "Hadolint",
      "primaryLocation": {
        "filePath": "Dockerfile",
        "message": "Delete the apt-get lists after installing something",
        "textRange": {
          "endColumn": 1,
          "endLine": 2,
          "startColumn": 0,
          "startLine": 2
        }
      },
      "ruleId": "DL3009",
      "severity": "MINOR",
      "type": "CODE_SMELL"
    },
    {
      "engineId": "Hadolint",
      "primaryLocation": {
        "filePath": "Dockerfile",
        "message": "Avoid additional packages by specifying `--no-install-recommends`",
        "textRange": {
          "endColumn": 1,
          "endLine": 2,
          "startColumn": 0,
          "startLine": 2
        }
      },
      "ruleId": "DL3015",
      "severity": "MINOR",
      "type": "CODE_SMELL"
    }
  ]
} 

When building Docker images, it’s generally recommended to use COPY instead of ADD. Both commands copy files from your local system into the Docker image, but ADD has some additional functionalities, like automatically extracting compressed files and supporting remote URLs. While these features might seem convenient, they can introduce unexpected behaviors and potential security risks, such as unintentionally downloading remote files or extracting sensitive content.

The key difference is that COPY only copies files and directories, making it simpler and more predictable. Since it doesn’t do any additional processing, using COPY aligns with the principle of least privilege, which is essential in creating secure and efficient Docker images.

When to Use COPY: Use COPY when you just want to copy files or directories from the host filesystem into the Docker image. This is the most common use case, and it’s generally considered more secure.

When to Use ADD: ADD should be reserved for cases where you need to automatically extract a local tarball or download files from a remote URL. However, in most cases, it’s better to handle these tasks explicitly using COPY and separate commands (like RUN curl or RUN tar).

Hardening Docker images isn’t a “set it and forget it” kind of deal, it’s more like a crossfit membership for your containers. Just because you did it once doesn’t mean you’re done! Security threats evolve, and so should your defenses. By regularly revisiting your images and applying these best practices, you’re not only keeping attackers at bay but also ensuring your applications are running at peak performance. So, whether you’re tweaking permissions, slimming down base images, or scanning for vulnerabilities, always be on the lookout for ways to improve. In the world of Docker, a little paranoia is a good thing!

Sebastian Burzyński CTO BlueRider.Software