Quick Links

Safe secret management is an important aspect of container security. If you're injecting passwords and API keys as environment variables, you risk unintentional information exposure. Shell variables are often logged, passed down to child processes, or leaked to error reporting services without your knowledge.

Injecting values as dedicated secrets mitigates these risks. Docker has built-in support for secure secret management which you can hook into with Docker Compose. Access to secrets is granted on a per-service basis.

How Do Secrets Work?

The Docker CLI has a batch of secret management commands but these only work with Swarm clusters. You can't add secrets to standalone containers using the Docker CLI alone.

Docker Compose added "fake" secrets to bring these capabilities to workloads without a cluster. Compose's implementation functions similarly to the Docker Swarm features and works with any Compose file.

Secrets are created as regular text files which are bind mounted into your containers. Your application accesses the secret's value by reading the file's contents. This model lets values stay inert until they're explicitly used within your container, unlike permanently visible environment variables.

Defining Secrets in Compose Files

Getting a secret into a container is a two-step process. First you need to define the secret, using the top-level secrets field in your Compose file. Then you update your service definitions to reference the secrets they require.

Here's an example that uses secrets to safely supply a password to a service:

version: "3"
    

services:

app:

image: example-app:latest

secrets:

- db_password

secrets:

db_password:

file: ./db_password.txt

The secret's value will be read from your working directory's db_password.txt file when you run docker-compose up. Compose will mount the file to /run/secrets/db_password within the container. Your app can access the database password by reading the contents of the secret file.

Using Existing Docker Secrets

Beyond file-based secrets, Compose also lets you reference existing Docker Swarm secrets. If you use this mechanism, you must create the secrets in Docker before you run docker-compose up. The docker secrets command space will only work when your active Docker endpoint is a Swarm manager node.

Create the secret using the Docker CLI:

# take value from standard input
    

echo P@55w0rd | docker secret create db_password -

OR

# take value from a file

docker secret create db_password ./db_password.txt

Now update your Docker Compose file to reference the secret:

version: "3"
    

services:

app:

image: example-app:latest

secrets:

- db_password

secrets:

db_password:

external: true

Setting the secret's external field instructs Compose to source its value from your existing Docker secrets. The stack will fail with an error if you try to start it before the secret exists.

Extended Secret Syntax

Compose supports a longer secrets syntax if you need more granular control over the injection process. Switching to this syntax lets you customize file permissions and change the secret's mounted name.

Five optional fields are available:

  • source - The name of the secret to reference - this must be one of the values defined in your Compose file's secrets section.
  • target - Filename to use when the secret is mounted into the container.
  • uid - UID to set on the mounted secret file. Defaults to 0.
  • gid - GID to set on the mounted secret file. Defaults to 0.
  • mode - Filesystem permissions to apply to the mounted secret file, expressed in octal notation. This defaults to 0444. Beware that secret files are never writable as they're always mounted into a container's temporary filesystem.

Here's a modified example which renames the mounted secret file and changes its permissions:

version: "3"
    

services:

app:

image: example-app:latest

secrets:

- source: db_password

target: database_password_secret

mode: 0440

secrets:

db_password:

external: true

The simple syntax is usually sufficient for most deployments. If you've got more specific requirements, the extended version should give you the control you need. Individual secret references can mix and match the two syntaxes within the same Compose file.

Secrets and Image Authorship

Many popular community Docker images now support secrets instead of environment variables. As an image author, offering secrets is a best practice approach to protecting your users' data.

You can support both mechanisms by allowing environment variables to be set to a file path. If your image needs a database connection, let users set the DB_PASSWORD environment variable to either P@55w0rd or /run/secrets/db_password. Your container should check whether the variable's value references a valid file; if it does, discard it and read the final value out of the file.

This model gives users the flexibility to choose the most appropriate mechanism for their deployment. Remember that not all users will be able to adopt secrets - if Swarm and Compose are both unavailable, they'll have no way of supplying their values.

Conclusion

Using secrets instead of regular environment variables reduces the risks of unintentional information disclosure. Imagine a worst case scenario where a container sent its environment variables to a compromised third-party logging service. Attackers now have your database password and API keys.

By restricting secret data to filesystem access, values can't be inadvertently read as they're not a perpetual feature of your environment. Remember that secret files carry their own risks though. You may be tempted to commit them into source control, which would mean anyone with access to your repository could read their values.

Secrets should be "secret" throughout your container's lifecycle. For production deployments, it's usually best to automate builds with a CI system. Set your secrets in your CI provider's pipeline settings, then use your build script to write them out to files which Compose can access. This ensures only you have access to the actual values, via your CI tool's interface.