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.
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.
[
{
"ID": "lefggw7gfeahqz34b70dws8jx",
"Version": {
"Index": 15020
},
"CreatedAt": "2020-02-02T11:24:00.151051151Z",
"UpdatedAt": "2020-02-02T11:24:00.151051151Z",
"Spec": {
"Name": "DB_PASSWORD",
"Labels": {}
}
}
]
The details of a Docker Secret when executing docker secret inspect
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.
// dependencies
const fs = require('fs');
const log = require('../log');
const dockerSecret = {};
dockerSecret.read = function read(secretName) {
try {
return fs.readFileSync(`/run/secrets/${secretName}`, 'utf8');
} catch(err) {
if (err.code !== 'ENOENT') {
log.error(`An error occurred while trying to read the secret: ${secretName}. Err: ${err}`);
} else {
log.debug(`Could not find the secret, probably not running in swarm mode: ${secretName}. Err: ${err}`);
}
return false;
}
};
module.exports = dockerSecret;
secrets.js, a Node.js module to read a Docker Secret
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.
/*
* Create and export configuration variables used by the API
*
*/
const constants = require('./constants');
const secrets = require('./secrets');
// Container for all environments
const environments = {};
environments.production = {
httpPort: process.env.HTTP_PORT,
httpAddress: process.env.HOST,
envName: 'production',
log: {
level: process.env.LOG_LEVEL,
},
database: {
url: secrets.read('STORAGE_HOST') || process.env.STORAGE_HOST,
name: 'workflow-db',
connectRetry: 5, // seconds
},
workflow: {
pollingInterval: 10, // Seconds
},
authprovider: {
domain: secrets.read('AUTH_DOMAIN') || process.env.AUTH_DOMAIN,
secret: secrets.read('AUTH_SECRET') || process.env.AUTH_SECRET
}
};
// Determine which environment was passed as a command-line argument
const currentEnvironment = typeof process.env.NODE_ENV === 'string' ? process.env.NODE_ENV.toLowerCase() : '';
// Check that the current environment is one of the environment defined above,
// if not default to production
const environmentToExport = typeof environments[currentEnvironment] === 'object' ? environments[currentEnvironment] : environments.production;
// export the module
module.exports = environmentToExport;
Config object that combines Docker Secrets and Environment variables
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.
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.
version: "3.5"
services:
workflow-engine:
image: pkalkman/mve-workflowengine:0.4.6
environment:
AUTH_DOMAIN: /run/secrets/AUTH_DOMAIN
AUTH_SECRET: /run/secrets/AUTH_SECRET
STORAGE_HOST: /run/secrets/STORAGE_HOST
HOST: 0.0.0.0
HTTP_PORT: 8181
LOG_LEVEL: 1
secrets:
AUTH_DOMAIN:
external: true
AUTH_SECRET:
external: true
STORAGE_HOST:
external: true
docker-compose.yml that defines and uses Docker Secrets
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.
So, the official Postgres image also uses the same _FILE
pattern.
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.
// dependencies
const fs = require('fs');
const log = require('../log');
const dockerSecret = {};
dockerSecret.read = function read(secretNameAndPath) {
try {
return fs.readFileSync(`${secretNameAndPath}`, 'utf8');
} catch(err) {
if (err.code !== 'ENOENT') {
log.error(`An error occurred while trying to read the secret: ${secretNameAndPath}. Err: ${err}`);
} else {
log.debug(`Could not find the secret, probably not running in swarm mode: ${secretNameAndPath}. Err: ${err}`);
}
return false;
}
};
module.exports = dockerSecret;
Reading Docker Secrets by specifying the complete path
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.
/*
* Create and export configuration variables used by the API
*
*/
const constants = require('./constants');
const secrets = require('./secrets');
// Container for all environments
const environments = {};
environments.production = {
httpPort: process.env.HTTP_PORT,
httpAddress: process.env.HOST,
envName: 'production',
log: {
level: process.env.LOG_LEVEL,
},
database: {
url: secrets.read('STORAGE_HOST_FILE') || process.env.STORAGE_HOST,
name: 'workflow-db',
connectRetry: 5, // seconds
},
workflow: {
pollingInterval: 10, // Seconds
},
authprovider: {
domain: secrets.read('AUTH_DOMAIN_FILE') || process.env.AUTH_DOMAIN,
secret: secrets.read('AUTH_SECRET_FILE') || process.env.AUTH_SECRET
}
};
// Determine which environment was passed as a command-line argument
const currentEnvironment = typeof process.env.NODE_ENV === 'string' ? process.env.NODE_ENV.toLowerCase() : '';
// Check that the current environment is one of the environment defined above,
// if not default to production
const environmentToExport = typeof environments[currentEnvironment] === 'object' ? environments[currentEnvironment] : environments.production;
// export the module
module.exports = environmentToExport;
Config object that uses the same pattern as the official Docker images
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.
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.
#node-js #docker #javascript #programming