Securing Passwords with Docker Secrets

If you’d just like to dive into the code for adding Docker secrets to the our WordPress app you can skip ahead to Putting It All Together.

Introduction

This is Part 5 in a series describing a project to create a local WordPress development environment using Docker-Compose. The series is structured as follows:

So far in this series we’ve used environment variables in our Compose files to specify passwords and other sensitive information required to start our services. As mentioned previously, this is not a secure way to specify this sensitive information but luckily, Compose provides environment files and Docker secrets to help secure it. We will explore both in this part of the series.

Removing Sensitive Information from the Compose File with Environment Files

We’ve already seen one problem with specifying sensitive information in environment variables in our Compose file, the sensitive information is readily visible to anyone with access to the Compose file. Take our Compose file from Part 1 for example.

docker-compose.yml file for a simple WordPress app from Part 1

version: '3.7'

services:
  db:
    image: mariadb
    environment:
      MYSQL_ROOT_PASSWORD: simplewordpress
      MYSQL_DATABASE: wordpress

  wp:
    image: wordpress 
    ports:
      - "8000:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: root
      WORDPRESS_DB_PASSWORD: simplewordpress

Here the user name, password and database name for our WordPress database are visible in plain text in our Compose file and anyone with access to this file could potentially gain access to our WordPress database using this information. This might not worry you much if you’re just tinkering with WordPress on a local PC, but if you’d like to share your Compose file more broadly, it isn’t an ideal situation.

It is an easy matter to move sensitive information or any other environment variable data to one or more environment files. This keeps your sensitive data separate from your Compose file and lessens the chance of it being accidentally exposed. It has the added benefit of streamlining your Compose file.

Our simple Compose file above can be rewritten using environment files with the env_file key as follows:

docker-compose.yml file for a simple WordPress app from Part 1 using environment files

version: '3.7'

services:
  db:
    image: mariadb
    env_file:
      - ./db.env

  wp:
    image: wordpress 
    ports:
      - "8000:80"
    env_file:
      - ./wp.env

We’ve specified two environment files, db.env and wp.env, in our Compose file. Let’s add them to our part-1 project directory.

/part-1/db.env

MYSQL_ROOT_PASSWORD=simplewordpress
MYSQL_DATABASE=wordpress

/part-1/wp.env

WORDPRESS_DB_HOST=db
WORDPRESS_DB_NAME=wordpress
WORDPRESS_DB_USER=root
WORDPRESS_DB_PASSWORD=simplewordpress

You can see here that we’ve simply moved all of our environment variables from our original Compose file to an environment file in a variable=value format. Multiple environment files may be specified for a given service and environment files can be used in addition to variables defined under the environment key as well. Note that any variables specified in the environment key section override the same variable specified in an environment file, regardless of placement within the service definition. Also, if a variable is specified in more than one environment file the variable will have the value from the last file in which it is listed.

Using an environment file, however, doesn’t remove the data from your system. It is still sitting in plain text in a file pointed to by a line of code in your Compose file. In addition, the sensitive information is stored in plain text within the service’s container and is visible using the Docker inspect command.

To demonstrate this limitation, start your project with docker-compose up -d and inspect your MariaDB and WordPress containers using the docker inspect command. We can find our specified environment variables in the Configuration section of our db and wp containers. Inspecting the database container we find the following excerpt in the environment section.

            "Env": [
                "MYSQL_ROOT_PASSWORD=simplewordpress",
                "MYSQL_DATABASE=wordpress",
		...

For the WordPress container we have the following excerpt in the environment section.

            "Env": [
                "WORDPRESS_DB_HOST=db",
                "WORDPRESS_DB_NAME=wordpress",
                "WORDPRESS_DB_USER=root",
                "WORDPRESS_DB_PASSWORD=simplewordpress",
		...

Environment files have allowed us to segregate sensitive information outside our Compose file but, as shown above, the information is readily available simply by inspecting the environment files or service’s container. However, we can use Docker Secrets to help further obscure this information.

Docker Secrets

Docker allows one to specify a user name, password or other sensitive information as a secret, which is encrypted, managed centrally with other secrets, and transmitted only to containers allowed to access the secret. Unfortunately, the full functionality of Docker secrets requires that we run our containers in swarm mode, which is beyond the scope of this series. However, with Compose we have access to some of the Docker secret functionality that will allow us to remove sensitive data from our environment variables and further obscure it from view.

Docker secrets are specified in our Compose file using the secrets key, both at the top-level to declare and define a secret and at a service level to give individual services access to a secret. In Compose, our secrets can only be sourced from a file which means that while we’ve removed this data from our environment files, the data still exists in plain text on our system somewhere. We’ll look a bit later on for ways to address this limitation.

Docker secrets are specified individually, so we need to declare a top-level secret and specify a source file for each piece of information we want to make a secret. For our simple WordPress app let’s specify four secrets, a database password for the root user that we’ll call mysql_root_password, a name for the WordPress database that we’ll call mysql_database, and a user name and password for the WordPress database that we’ll call mysql_user and mysql_password.

Each of these secrets must be created in a separate file. Let’s put ours in a separate directory, named secrets, in our project directory. Each file simply contains the value of the secret in plain text.

So, continuing with the extension of our Part 1 project above, our top-level secrets section of our Compose file becomes:

secrets:
  mysql_root_password:
    file: ./secrets/mysql_root_password
  mysql_database:
    file: ./secrets/mysql_database
  mysql_user:
    file: ./secrets/mysql_user
  mysql_password:
    file: ./secrets/mysql_password

with four individual secret files of

./secrets/mysql_root_password

simplewordpress

./secrets/mysql_database

wordpress

./secrets/mysql_user

root

./secrets/mysql_password

simplewordpress

We then need to add a secret section to the database and WordPress services to provide access to these secrets. The syntax here is simple, we just need to specify the name of the secrets we’re providing access to for each service. Continuing on with our example from above we would add the following code to those services.

…

  db:
    …
    secrets:
      - mysql_root_password
      - mysql_database
    …

  wp:
    …
    secrets:
      - mysql_database
      - mysql_user
      - mysql_password
    …

Similar to a bind mount, Compose will mount the file defined for each secret within the specified container. By default, the mounting point is at /run/secrets/<secret_name> within the container and can be referenced there.

Our MariaDB and WordPress images provide environment variables that can take advantage of Docker Secrets. For our app we can simply append _File to the end of each of the environment variables we used above and then reference the respective secret file within the container at /run/secrets/<secret_name>. So, our Compose file snippet from above becomes the following for our MariaDB and WordPress services.

…

  db:
    …
    secrets:
      - mysql_root_password
      - mysql_database
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
      MYSQL_DATABASE_FILE: /run/secrets/mysql_database
    …

  wp:
    …
    secrets:
      - mysql_database
      - mysql_user
      - mysql_password
    environment:
      WORDPRESS_DB_NAME_FILE: /run/secrets/mysql_database
      WORDPRESS_DB_USER_FILE: /run/secrets/mysql_user
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/mysql_password
    …

With these additions/changes our example Compose file becomes the following.

Example docker-compose.yml file for a simple WordPress app from Part 1 using Docker Secrets

version: '3.7'

services:
  db:
    image: mariadb
    secrets:
      - mysql_root_password
      - mysql_database
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
      MYSQL_DATABASE_FILE: /run/secrets/mysql_database

  wp:
    image: wordpress 
    ports:
      - "8000:80"
    secrets:
      - mysql_database
      - mysql_user
      - mysql_password
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME_FILE: /run/secrets/mysql_database
      WORDPRESS_DB_USER_FILE: /run/secrets/mysql_user
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/mysql_password

secrets:
  mysql_root_password:
    file: ./secrets/mysql_root_password
  mysql_database:
    file: ./secrets/mysql_database
  mysql_user:
    file: ./secrets/mysql_user
  mysql_password:
    file: ./secrets/mysql_password

Alternatively, we could include the environment variables in an environment file.

Start your project with docker-compose up -d and use the docker inspect command to verify that our sensitive data is no longer exposed by a simple inspection of the container. For the database container you should see the following at the beginning of the environment section.

            "Env": [
                "MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_password",
                "MYSQL_DATABASE_FILE=/run/secrets/mysql_database",
		...

For the WordPress container you should see the following at the beginning of the environment section.

            "Env": [
                "WORDPRESS_DB_HOST=db",
                "WORDPRESS_DB_NAME_FILE=/run/secrets/mysql_database",
                "WORDPRESS_DB_USER_FILE=/run/secrets/mysql_user",
                "WORDPRESS_DB_PASSWORD_FILE=/run/secrets/mysql_password",
		...

You can see that our sensitive data is no longer visible when inspecting the container with the docker inspect command. However, the secrets are still stored in plain text within the container and can be accessed using the docker exec command (I’ll leave it as an exercise to verify this; see Part 4 for examples of using the docker exec command to examine a container’s content). However, as this is the case when using the fully functional Docker secrets in swarm mode, we won’t address it further here. Use the docker-compose down -v command to shut down your app in preparation for the next section.

Putting It All Together

In the discussion above we used our project from Part 1 to examined the use of environment files and Docker secrets to secure, or at least obscure sensitive information. Let’s take it a step further now by adding Docker secrets to our final app from Part 4d. I’ll be starting with a new folder I’ll call part-5. Create a Compose file like the following.

/part-5/docker-compose.yml file

version: '3.7'

services:
  db:
    image: mariadb
    secrets:
      - mysql_root_password
      - mysql_database
      - mysql_user
      - mysql_password
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
      MYSQL_DATABASE_FILE: /run/secrets/mysql_database
      MYSQL_USER_FILE: /run/secrets/mysql_user
      MYSQL_PASSWORD_FILE: /run/secrets/mysql_password
    volumes:
      - db:/var/lib/mysql

  wp:
    image: wordpress
    secrets:
      - mysql_database
      - mysql_user
      - mysql_password
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME_FILE: /run/secrets/mysql_database
      WORDPRESS_DB_USER_FILE: /run/secrets/mysql_user
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/mysql_password
    volumes:
      - wp:/var/www/html

  adminer:
    image: adminer

  nginx:
    image: nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx:/etc/nginx/conf.d

volumes:
  db:
  wp:      

secrets:
  mysql_root_password:
    file: ./secrets/mysql_root_password
  mysql_database:
    file: ./secrets/mysql_database
  mysql_user:
    file: ./secrets/mysql_user
  mysql_password:
    file: ./secrets/mysql_password

along with our four individual secret files in the /part-5/secrets folder.

/part-5/secrets/mysql_root_password

simplewordpress

/part-5/secrets/mysql_database

wordpress

/part-5/secrets/mysql_user

wordpressuser

/part-5/secrets/mysql_password

someotherwordpress

You can see I’ve specified a separate user name and password rather than continuing to rely on the root user and password which is not good practice (Note: although I couldn’t find any specific documentation mentioning it, it appears that MariaDB requires your passwords to be a minimum of eight characters. Anything shorter than this and your login attempts will fail.)

As with Part 4d, we also have our nginx configuration file, nginx.conf in the nginx folder.

/part-5/nginx/nginx.conf

server {
  proxy_set_header Host $host;

  location / {
    proxy_pass http://wp;
  }

  location /adminer {
    proxy_pass http://adminer:8080;
  }
}

As above, after starting this project with docker-compose up -d, you can see that our sensitive data is no longer visible when inspecting the container with the docker inspect command. But our sensitive data is still sitting in our project directory in plain text files. There are likely several ways to further protect these files. I addressed it for my project by moving the secrets directory to an encrypted USB drive and creating a link from our project directory to it.

There are many utilities that will encrypt a USB drive, I’m not going to summarize them here. You can easily find them on the web. For my purposes, encrypting the USB drive with the Ubuntu file manager was sufficient. To do this you need to format a USB drive, selecting the Internal disk for use with Linux systems only (Ext4) option and then specify a strong password. You’ll have the disadvantage of only being able to access your secrets on Linux systems, but I consider this an advantage in this case.

Once you have an encrypted USB drive, move, not copy, your secrets directory to it. You can then create a symbolic link to the secrets directory on your USB drive using the Linux ln command

ln -s <path to USB drive>/secrets

(You could also skip the symbolic link and just hard code your secret file location in your Compose file.)

Now Compose will access your secrets in your USB drive on project startup. After you start your project you can simply remove your USB drive to remove access to your secrets files. Note that this will break your symbolic link and you won’t be able to restart your project if needed without your USB drive mounted. You also won’t be able to have your app restart automatically, but that’s something we may cover in a future part of the series.

Wrapping Up

We’ve covered several ways to remove sensitive information from your Compose file using environment files and Docker secrets. We’ve also seen that you can secure these further using an encrypted USB drive. We’ve used a separate file as a source for each of our Docker secrets. This has the disadvantage of requiring a lot of files if you are managing a lot of secrets. This isn’t much of an issue at this point as our project only has four secrets, but as we move along, we might want to examine an alternative. Using the full capability of Docker secrets by running our project in swarm mode would allow us to let Docker maintain our secrets. But we’ll leave that for a later project.

Next up, we’ll take a first look at securing communications with our app with the https protocol in Part 6 – Securing network communications with self-signed certificates.