How to Handle Docker Secrets in Node.js

Don’t expose sensitive information in a production environment

How to Handle Docker Secrets in Node.js
Photo by Floh Maier on Unsplash

In my previous post, I told you not to put sensitive information in source control. I showed how you could use a .env file to store this information and exclude it from source control.

I also showed how to use dotenv to load these settings in the environment so your Node.js application could read them.

While using environment variables is the right way for local development, it is not recommended for a production environment.

A simple printenv in the command line will list all environment variables. A more secure way to manage your sensitive settings in production is to use Docker secrets.


Docker Secrets

Docker secrets help to manage sensitive data that a container needs at run-time. For example, usernames, passwords, and certificates. The largest size of a single secret is 500KB.

You can use Docker secrets when your container is running inside a cluster such as Docker Swarm. Docker supports Docker secrets on all types of containers from Docker version 17.06.

Creating Docker secrets

I prefer creating Docker secrets using the command line.

There are two options: use echo or use a file that contains your secret. The following command creates a Docker secret called DB_PASSWORD that holds the string “secretpassword” using echo.echo "secretpassword" | docker secret create DB_PASSWORD -

The other way is using a text file that contains the value of the secret.docker secret create DB_PASSWORD db_password.txt

Both of the commands have the same result. A Docker secret DB_PASSWORD is created that contains the secret, the password. As with any Docker resource, you can use the inspect command to get details of the secret.Docker secret inspect [secretid or name]

If you created the secret successfully, it returns a JSON object with its details. The details do not include value.

Reading and using Docker secrets in Node.js

Docker uses an in-memory filesystem for storing secrets. Docker secrets look like regular files inside your container.

Docker stores each secret as a single file in /run/secrets/. The filename is the name of the secret. When your Node.js app runs inside a container, it can read a secret like a regular file.

I developed a Node.js module that reads a file from/run/secrets and returns the contents of the file.

For local development, I want to use the .env file as described earlier.

But once the app is running in production, it should read settings from Docker secrets. By combining secrets.js with my standard configuration object, I get the best of both worlds.

On row 19, for example, I combine reading a secret and an environment setting secrets.read(‘STORAGE_HOST’) || process.env.STORAGE_HOST. The secret has priority over the environment.

Instead of developing your own, there are existing npm libraries that help with reading Docker secrets.

For example, docker-swarm-secrets, docker-secret, and @cloudreach/docker-secret. These modules read all the Docker secrets and expose them via a JavaScript object.

Assigning Docker secrets to services

We have to give a container explicit permission before it can access a secret. There are two options to give permissions, add it when creating a service or adding it to your docker-compose.yml file.docker service create --name myservice --secret STORAGE_HOST myimage

The service myservice has access to the secret STORAGE_HOST which is defined on the host using the command line.

A different and my preferred way is to use a compose file, see below.

The compose file names each secret in a separate block, see row 13. The secrets get the external: true to define that these secrets already exist and were created externally using the command line.


Docker Secrets in Official Docker Images

If you are wondering how official Docker images handle Docker secrets, there seems to be a typical pattern. Most of the official images that use an environment setting also contain the same environment setting with a _FILE postfix.

For example, the MongoDB image uses the MONGO_INITDB_ROOT_USERNAME and MONGO_INITDB_ROOT_PASSWORD environment variables.

It also accepts the MONGO_INITDB_ROOT_USERNAME_FILE and MONGO_INITDB_ROOT_PASSWORD_FILE environment variables. If you set the latter ones to /run/secrets/[secret name], the image will read and use that secret.

If we look at the documentation of the official Postgres image it mentions the following:

“As an alternative to passing sensitive information via environment variables, _FILE may be appended to some of the previously listed environment variables, causing the initialization script to load the values for those variables from files present in the container.
In particular, this can be used to load passwords from Docker secrets stored in /run/secrets/<secret_name> files.”

So, the official Postgres image also uses the same _FILE pattern.


Using the _FILE Pattern in Node.js

Although the earlier module to read secrets works fine, it would be better to use the same pattern as official images. We only have to make a small change to the secrets module and the config object.

Instead of accepting the name of the secret and prepending the path of the secret, we receive the complete path. The Config object first reads the value of the _FILE settings, and if not available, it uses the environment setting.

For example, domain: secrets.read(‘AUTH_DOMAIN_FILE’) || process.env.AUTH_DOMAIN on row 27 first tries to read the secret file, and if it does not succeed, it uses the AUTH_DOMAIN environment setting.


Start Using Docker Secrets

I hope that I have convinced you to start using Docker secrets for storing sensitive information in production. Thank you for reading and if you have any questions or remarks, feel free to leave a response.