Lockdown Your Containers: 11 Essential Docker Security Tips

Created October 5, 2024

Introduction

Hey! I'm Bobby, a Docker Captain and the author of the free Introduction to Docker eBook. In this article, we'll cover 11 essential Docker security tips to help you protect your containerized applications.

Prerequisites

To follow this guide, you should have:

1. Keep Docker Updated

Regularly updating your Docker engine is crucial for maintaining the security of your containerized environment. Each new release of Docker often includes security patches and fixes for vulnerabilities discovered in previous versions.

To update Docker on a Ubuntu system, you can use the following commands:

sudo apt-get update
sudo apt-get upgrade docker-ce

After updating, it's a good practice to restart the Docker daemon:

sudo systemctl restart docker

Make sure to test your applications after updating Docker to ensure compatibility with the new version.

2. Use Official Images

Official images from Docker Hub are maintained by the Docker team and the original software maintainers. They follow best practices for Docker image creation and are regularly updated with security patches.

When using official images in your docker-compose.yml file, always specify a specific version tag rather than using latest:

version: '3.8'
services:
  web:
    image: nginx:1.21.3  # Specific version of the official Nginx image

Using specific version tags ensures reproducibility and prevents unexpected changes when rebuilding your containers.

3. Scan Images for Vulnerabilities

Regularly scanning your Docker images for vulnerabilities is essential for maintaining a secure environment. Docker Scout is a powerful tool integrated into Docker Desktop and the Docker CLI for this purpose.

To scan an image using Docker Scout:

docker scout cve <image_name>:<tag>

For example:

docker scout cve nginx:1.21.3

This command will provide a detailed report of any known vulnerabilities in the image, including their severity levels and available fixes.

You can also integrate Docker Scout into your CI/CD pipeline. Here's an example of how you might do this in a GitHub Actions workflow:

name: Docker Image CI

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Build the Docker image
      run: docker build . --file Dockerfile --tag myapp:${GITHUB_SHA}
    - name: Scan the Docker image
      run: docker scout cve myapp:${GITHUB_SHA}

This workflow builds your Docker image and then scans it for vulnerabilities on every push to the repository.

4. Limit Container Resources

Setting Docker resource limits on your containers is crucial for preventing Denial of Service (DoS) attacks and ensuring fair resource allocation in multi-container environments.

In your docker-compose.yml file, you can set these limits as follows:

version: '3.8'
services:
  web:
    image: nginx:1.21.3
    deploy:
      resources:
        limits:
          cpus: '0.50'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M

This configuration limits the web service to use a maximum of 0.5 CPU cores and 512MB of memory, while reserving a minimum of 0.25 CPU cores and 256MB of memory.

You can also set these limits when running a container using the Docker CLI:

docker run -d --name myapp --cpus=0.5 --memory=512m nginx:1.21.3

By setting these limits, you prevent a single container from consuming all available resources on the host, which could affect other containers or the host system itself.

5. Use Non-Root Users

Running containers as root is a significant security risk. If an attacker manages to break out of the container, they would have root access to the host system. To mitigate this risk, create a non-root user in your Dockerfile and switch to this user.

Here's an example Dockerfile that creates a non-root user:

FROM node:18
# Create app directory
WORKDIR /usr/src/app

# Install app dependencies
COPY package*.json ./
RUN npm ci --only=production

# Bundle app source
COPY . .

# Create a non-root user and switch to it
RUN groupadd -r myapp && useradd -r -g myapp myuser
USER myuser

EXPOSE 8080
CMD [ "node", "server.js" ]

In this Dockerfile, we create a new user myuser and a group myapp, then switch to this user before running the application. This ensures that the application runs with limited permissions.

When running the container, you can also use the --user flag to specify a non-root user:

docker run -d --name myapp --user 1000:1000 myimage

This runs the container as the user with UID 1000 and GID 1000.

6. Implement Secret Management

Storing sensitive information like passwords, API keys, and other secrets directly in your Dockerfile or Docker Compose file is a security risk. Instead, use Docker secrets for managing this sensitive data.

First, create a secret:

echo "mysecretpassword" | docker secret create db_password -

Then, in your docker-compose.yml file, you can use this secret:

version: '3.8'
services:
  db:
    image: mysql:8.0
    secrets:
      - db_password
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_password
secrets:
  db_password:
    external: true

In this example, MySQL will read the root password from the file /run/secrets/db_password inside the container, which contains the secret we created.

For non-swarm mode, you can use environment variables and a .env file:

version: '3.8'
services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}

And in your .env file (which should be added to .gitignore):

DB_PASSWORD=mysecretpassword

This approach keeps secrets out of your version-controlled files while still allowing easy configuration.

7. Enable Content Trust

Docker Content Trust (DCT) allows you to verify the integrity and the publisher of all the data received from a registry over any channel. When DCT is enabled, you can be sure that the image you're pulling is the one that was pushed, without any tampering.

To enable DCT, set the DOCKER_CONTENT_TRUST environment variable:

export DOCKER_CONTENT_TRUST=1

With DCT enabled, when you push an image, Docker signs it:

docker push myrepo/myimage:latest

When pulling images with DCT enabled, Docker will only pull signed images:

docker pull myrepo/myimage:latest

If the image isn't signed or the signature doesn't match, the pull will fail.

You can also enable DCT in your Docker daemon configuration file (/etc/docker/daemon.json):

{
  "content-trust": {
    "mode": "enforced"
  }
}

This enforces DCT for all operations on the Docker daemon.

8. Use Read-Only Containers

Running containers in read-only mode prevents attackers from making changes to the container's filesystem, even if they manage to execute arbitrary code within the container.

In your docker-compose.yml file, you can set a container to read-only mode like this:

version: '3.8'
services:
  web:
    image: nginx:1.21.3
    read_only: true
    tmpfs:
      - /tmp
      - /var/cache/nginx

The tmpfs mounts are necessary for Nginx to function correctly, as it needs to write to these directories. These are temporary filesystems that exist only in memory.

When using the Docker CLI, you can use the --read-only flag:

docker run -d --name myapp --read-only nginx:1.21.3

For applications that need to write data, you can use volumes or bind mounts for specific directories that need write access, while keeping the rest of the filesystem read-only.

9. Implement Network Segmentation

Network segmentation in Docker helps to isolate containers and reduce the attack surface. By default, all containers on a Docker network can communicate with each other. However, you can create separate networks for different groups of containers.

Here's an example docker-compose.yml file that implements network segmentation:

version: '3.8'
services:
  frontend:
    image: nginx:1.21.3
    networks:
      - frontend
  backend:
    image: app:latest
    networks:
      - backend
  db:
    image: mysql:8.0
    networks:
      - backend
networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

In this setup, the frontend service cannot communicate directly with the db service, as they're on different networks. The backend service acts as a bridge between the two.

You can also create and manage networks using the Docker CLI:

# Create networks
docker network create frontend
docker network create backend

# Run containers on specific networks
docker run -d --name nginx --network frontend nginx:1.21.3
docker run -d --name app --network frontend --network backend app:latest
docker run -d --name mysql --network backend mysql:8.0

This network segmentation adds an extra layer of security by limiting the potential spread of an attack if one container is compromised.

10. Regular Security Audits

Regular security audits are crucial for maintaining the security of your Docker environment. Docker Bench for Security is a script that checks for dozens of common best practices around deploying Docker containers in production.

To run Docker Bench for Security:

docker run -it --net host --pid host --userns host --cap-add audit_control \
    -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
    -v /var/lib:/var/lib \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v /usr/lib/systemd:/usr/lib/systemd \
    -v /etc:/etc --label docker_bench_security \
    docker/docker-bench-security

This command runs a series of checks and provides a report with recommendations for improving your Docker security posture.

It's a good practice to run this audit regularly, such as weekly or after any significant changes to your Docker environment. You can automate this process by integrating it into your CI/CD pipeline or setting up a cron job.

Here's an example of how you might integrate Docker Bench into a GitHub Actions workflow:

name: Docker Security Audit

on:
  schedule:
    - cron: '0 0 * * 0'  # Run weekly on Sunday at midnight

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
    - name: Check out code
      uses: actions/checkout@v2
    - name: Run Docker Bench for Security
      run: |
        docker run --net host --pid host --userns host --cap-add audit_control \
          -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
          -v /var/lib:/var/lib \
          -v /var/run/docker.sock:/var/run/docker.sock \
          -v /usr/lib/systemd:/usr/lib/systemd \
          -v /etc:/etc --label docker_bench_security \
          docker/docker-bench-security

This workflow runs Docker Bench for Security every week and provides a report in the GitHub Actions log.

11. Implement Logging and Monitoring

Proper logging and monitoring are essential for detecting and responding to security incidents in your Docker environment. Docker provides built-in logging mechanisms that you can configure for each container:

Docker Logging Drivers

In your docker-compose.yml file, you can set up logging like this:

version: '3.8'
services:
  web:
    image: nginx:1.21.3
    logging:
      driver: "json-file"
      options:
        max-size: "200m"
        max-file: "10"

This configuration uses the json-file logging driver, which is the default. It sets a maximum size of 200MB for each log file and keeps up to 10 log files.

For more advanced logging, you can use the syslog driver to send logs to a central syslog server:

logging:
  driver: syslog
  options:
    syslog-address: "udp://192.168.0.42:1234"

For monitoring, consider using tools like Prometheus and Grafana. Here's an example docker-compose.yml that sets up a basic monitoring stack:

version: '3.8'
services:
  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
  grafana:
    image: grafana/grafana
    depends_on:
      - prometheus
    ports:
      - "3000:3000"

You'll need to configure Prometheus to scrape metrics from your Docker daemon and containers. Here's a basic prometheus.yml configuration:

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'docker'
    static_configs:
      - targets: ['docker.for.mac.localhost:9323']

To enable Docker metrics, you need to configure your Docker daemon. Add the following to /etc/docker/daemon.json:

{
  "metrics-addr" : "0.0.0.0:9323",
  "experimental" : true
}

Remember to restart the Docker daemon after making these changes.

With this setup, you can create dashboards in Grafana to visualize your Docker metrics and set up alerts for any suspicious activity.

Conclusion

Security is an ongoing process that requires regular attention and updates!

These 11 security tips will significantly help you with your container security and Docker environment but you should always keep an eye on the latest security best practices and updates.

For more information on Docker, check out my free Introduction to Docker eBook.

If you're looking for a reliable platform to practice these security tips, consider using DigitalOcean. They offer a $200 free credit to get you started.