Going to AWS re:Invent 2024?

➡️ Book a meeting with Spacelift

Docker

How to Keep Docker Secrets Secure: Complete Guide

The Complete Guide to Docker Secrets

Secret values such as API keys, passwords, and certificates need to be safely handled throughout the software development process and your app’s runtime. Exposure of secrets can be catastrophic, as unauthorized actors could use the credentials to perform privileged interactions with your services.

Secrets are often encountered when you’re working with Docker containers. It can be challenging to correctly handle container secrets because Docker historically lacked a built-in secrets management system. In this article, you’ll learn how to securely use secrets when you’re building Docker images and starting containers.

We will cover:

  1. Why you need Docker secrets management
  2. Using secrets in Docker
  3. Using secrets with Docker Compose
  4. Using Docker Swarm secrets
  5. Dockerfile secrets when you’re building images
  6. Best practices for Docker secrets

Why You Need Docker Secrets Management

Safe secrets management is a crucial component of software supply chain security. Leaked API keys and authentication tokens, including ones discovered in Docker images, have contributed to major data breaches in recent years. If an attacker would find a value useful, it needs to be treated as a secret.

Unfortunately, support for secrets management is often missing from container workflows. Developers tend to configure containers using environment variables, but these are visible in plaintext from outside the container:

$ docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=foobar mysql:8.0
96c995563df92a2d1341564cf635d284bc70fe1aa9ba4bba8da371f765243a35

The value of MYSQL_ROOT_PASSWORD should be kept secret. If it’s exposed, the recipient could login to the MySQL database as root. Because an environment variable’s been used, the password can be easily retrieved using  docker inspect, however:

$ docker inspect mysql
[
  {
    "Config": {
      "Env": [
        "MYSQL_ROOT_PASSWORD=foobar"
      ]
    }
  }
]

Similarly, software within the container can see the environment variable and its value. This makes it easier for a malicious process to steal the value after the container is compromised:

$ docker exec -it mysql bash
bash-4.4# echo $MYSQL_ROOT_PASSWORD
foobar

Finally, secrets that are configured as environment variables also have a habit of being accidentally committed to your repositories. They tend to turn up in docker-compose.yml, for example, either as forgotten test values or because developers fail to recognize the risks of hardcoded secrets.

version: "3"
services:
  mysql:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=foobar

Now let’s look at how to avoid these risks and securely handle secrets with Docker.

You can also read more about Container Security Best Practices & Solutions.

Using Secrets In Docker

Docker includes a secrets management solution, but it doesn’t work with standalone containers. You can supply secrets to your containers when you’re using either Docker Compose or Docker Swarm. There’s no alternative for containers created manually with a plain docker run command.

Let’s explore how to use the two available methods to securely set secrets in containers.

Using Secrets With Docker Compose

Compose’s secrets system is the most accessible for everyday use. It’s also your only option if you’re not using Docker Swarm.

Secrets are defined in Compose files within the top-level secrets field. Each named secret references a file in your working directory. When you run docker compose up, Compose will automatically mount that file into the container.

Secrets are mounted to a predictable container path: /run/secrets/<secret_name>. You should configure your containerized application to read the secret’s value from that path.

The following example uses Compose to securely configure the root user’s password for a MySQL container:

version: "3"
services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
    secrets:
      - mysql_root_password
secrets:
  mysql_root_password:
    file: password.txt

Let’s analyze what’s happening in this file:

  • The secrets section defines a single secret called mysql_root_password.
  • The secret’s value is read from password.txt in your working directory.
  • The mysql service references the secret within its own secrets field.
  • When the container starts, the contents of password.txt will be read and mounted to /run/secrets/mysql_root_password (the name of the secret) inside the container.
  • The MYSQL_ROOT_PASSWORD_FILE environment variable instructs the official MySQL Docker image to read its password from the mounted file.

Testing Docker Compose Secrets

To test this example, first create the password.txt file in your working directory:

$ echo foobar > password.txt

You can now use Docker Compose to bring up your container:

$ docker compose up -d

Inspecting the /run/secrets directory inside the container will confirm the secret’s existence:

$ docker compose exec -it mysql bash

bash-4.4# ls /run/secrets
mysql_root_password

bash-4.4# cat /run/secrets/mysql_root_password
foobar

The value can’t be directly accessed from outside the container. The output from docker inspect will show that password.txt is mounted to /run/secrets/mysql_root_password, but its content won’t be displayed:

[
  {
    "Mounts": [
      {
          "Type": "bind",
          "Source": "/home/james/@scratch/the-complete-guide-to-docker-secrets/password.txt",
          "Destination": "/run/secrets/mysql_root_password",
          "Mode": "",
          "RW": false,
          "Propagation": "rprivate"
      }
    ]
    }
  }
]

This output demonstrates how Compose implements its secrets management functionality. Secrets are injected into the container using a bind mount from the file in your working directory.

Using Docker Swarm Secrets

Compose secrets are an effective way to manage sensitive data in applications when you’re already using Compose or a dedicated secrets manager is unavailable. Docker also includes a more extensive secrets solution, accessed via the docker secret command group, but it only works with services deployed to a Swarm cluster.

Swarm secrets are managed by your cluster and will be securely replicated to each node within it. Secrets are guaranteed to be highly available, like the other cluster-level data created by the swarm.

Secrets are encrypted during transit and storage. They’re held in memory, so they’re not persisted on individual nodes or container filesystems. Additionally, nodes only receive the secrets required by the containers they’re running, further minimizing the risk of exposure.

Creating a Swarm

Because docker secrets only work in Swarm mode, you’ll need access to a swarm before you can try it. Run the following command to create a new single-node swarm on your current machine:

$ docker swarm init
Swarm initialized: current node (xtt0cz0s8qda3xek7ksie2r1h) is now a manager.

Creating Secrets

Once you’ve created a swarm, you can use docker secrets to start managing your secrets. First create a local file that holds your secret value:

$ echo foobar > password.txt

Next, create the Docker secret object. The command takes two arguments: the secret’s name, and the path to the file that contains its value:

$ docker secret create mysql_root_password password.txt
0almm6a62ec3z8jm4jjw6dny5

Similarly to other Docker commands, the ID of the created secret will be emitted to your terminal.

Creating Secrets Using Standard Input

You can populate secrets from your terminal’s input stream:

$ echo demo | docker secret create demo_secret -

This stops secrets from being saved to files on your machine.

Starting a Service With a Secret

You’re ready to mount your secret into a Swarm service! Set the --secret flag when you start your service to have Swarm automatically inject a named secret:

$ docker service create --name mysql --secret mysql_root_password -e MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_password mysql:8.0

As with Compose secrets, Swarm mounts the secret to /run/secrets/<secret_name> inside the container. This produces the path of /run/secrets/mysql_root_password in this example.

Altering Secret Attachments

In practice, this service doesn’t need long-lived access to the secret. The container only needs to know the password once, during MySQL’s first-run initialization routine. You can update the service to detach the secret, preventing a compromised container process from accessing it:

$ docker service update mysql --secret-rm mysql_root_password

You can also attach secrets to existing services:

$ docker service update mysql --secret-add mysql_root_password

Changing the Container Mount Point

Secrets are mounted to /run/secrets/<secret_name> by default, but you can customize this using a more verbose variant of the --secret and --secret-add flags:

$ docker service update mysql --secret-add source=password.txt,target=/etc/mysql/root_password

Running this command will mount your local password.txt file to /etc/mysql/root_password inside the container. This is useful when your application expects to read secret files from a specific path that can’t be changed.

Rotating Secrets

Swarm doesn’t have a built-in way to rotate a secret with a single command. It’s not possible to change the values of secrets after they’re created, so you have to create a new secret that’s mounted into the container at the same path.

Create your new secret first:

$ docker secret create mysql_root_password_v2 new-password.txt
2rpueuyn3p3zy6220bxemt4x5

Next, update your service to remove the old secret and mount the new one instead. Use the target option to mount the secret back to the original version’s path:

$ docker service update --secret-rm mysql_root_password --secret-add source=mysql_root_password_v2,target=mysql_root_password mysql

The container will now have access to the rotated version of the secret.

Managing Secrets

You can list all the secrets you’ve created with the docker secret ls command:

$ docker secret ls
ID                          NAME                  DRIVER    CREATED          UPDATED
0almm6a62ec3z8jm4jjw6dny5   mysql_root_password             15 minutes ago   15 minutes ago

Delete a secret with docker secret rm:

$ docker secret rm mysql_root_password

It isn’t possible to delete a secret that’s actively used by a service. Detach the secret from your services before you try to remove it.

Referencing Swarm Secrets in Compose Files

You can reuse Swarm secrets in services managed by Docker Compose. Create the secret using docker secret create, then reference it within the services section of your docker-compose.yml file by setting the external field to true:

version: "3"
services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
    secrets:
      - mysql_root_password
secrets:
  mysql_root_password:
    external: true

Compose will take the secret’s value from the swarm, instead of reading it from a local file.

Dockerfile Secrets When You're Building Images

In addition to runtime container configuration, secret values can also be required by the Dockerfile instructions used to build your images. You might have to authenticate to a remote package registry ahead of an instruction that installs your project’s dependencies, for example.

Hardcoding these secrets into your Dockerfile is dangerous because they’ll be visible to anyone who can access your source control repository. It’s better practice to use Docker’s build args feature to declare variables that must be set when you run docker build.

The following Dockerfile installs npm packages from a custom registry that requires authentication. The ARG NPM_AUTH_TOKEN instruction defines a build arg that’s used to supply the authentication token:

FROM node:18 AS build
ARG NPM_AUTH_TOKEN

COPY package.json package.json
COPY package-lock.json package-lock.json
RUN npm config set @example:registry https://registry.example.com/ &&\
  npm config set -- '//registry.example.com/:_authToken' "${NPM_AUTH_TOKEN}" &&\
  npm install

Set the --build-arg flag to provide your auth token when you build the image:

$ docker build --build-arg NPM_AUTH_TOKEN=foobar -t example-image:latest .

This ensures sensitive values used by your build instructions aren’t hardcoded into your Dockerfile, or accidentally persisted to the container image’s filesystem.

Best Practices for Docker Secrets

Need more tips on handling your secrets? Here are three best practices to remember:

  1. .gitignore all files that contain secrets – Mounting secrets into containers from local files carries the risk of those files being accidentally committed to your repository. Add paths that will contain secrets to your .gitignore file to prevent git add . from inadvertently staging sensitive values.
  2. Design your Docker images around safe secrets management – Encourage the adoption of good secrets management practices by designing your images and applications around them. Make apps always read secrets from the filesystem, instead of environment variables, to prevent user mistakes and shortcuts.
  3. Ensure secrets are used for all sensitive values. – Developers sometimes lack the context to decide whether a particular value needs to be treated as a secret. Address this by clearly communicating secrets requirements, including how to identify candidate values. A secret is anything that could be valuable to an attacker, or which might expose other data – secrets shouldn’t be confined to passwords and certificates.

Keeping these points in mind will help you minimize the risk of secrets exposure caused by container misconfiguration.

Check out more Docker security best practices.

Key Points

Secrets are sensitive values such as API tokens and passwords that your application requires. Losing a secret usually empowers bad actors by providing privileged access to your services, making it essential to apply proper treatment to their storage and retrieval. Secrets mustn’t be hardcoded or stored in plaintext as this expands their audience and makes them difficult to rotate.

Docker containers often require secrets as part of their configuration. Supplying secret values as regular environment variables is dangerous because those variables can be easily retrieved, both outside and within the container.

Instead, you should use the secrets management system built into Docker Compose or Docker Swarm. This lets you securely mount secrets into your container’s filesystem. Compose is simple to get started with, but Swarm offers additional capabilities if you’re already running a cluster. Managing secrets independently of your containers also prepares you for other ecosystem tools where secrets are treated as first-class objects, such as Kubernetes secrets.

Looking for more Docker guides and tutorials? Check out our other articles on the Spacelift blog. We encourage you also to explore how Spacelift offers full flexibility when it comes to customizing your workflow. You have the possibility of bringing your own Docker image and using it as a runner to speed up the deployments that leverage third party tools. Spacelift’s official runner image can be found here.

The Most Flexible CI/CD Automation Tool

Spacelift is an alternative to using homegrown solutions on top of a generic CI. It helps overcome common state management issues and adds several must-have capabilities for infrastructure management.

Start free trial

Docker CLI Commands Cheat Sheet

Grab our ultimate cheat sheet PDF

for all the Docker commands you need.

Share your data and download the cheat sheet