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:
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.
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.
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 calledmysql_root_password
. - The secret’s value is read from
password.txt
in your working directory. - The
mysql
service references the secret within its ownsecrets
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.
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.
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.
Need more tips on handling your secrets? Here are three best practices to remember:
- .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.
- 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.
- 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.
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.