Using Docker Secrets with NodeJS
This post shows how to use (and read from) Docker Secrets within a NodeJS app.
Why use it?
The difference when using (Docker) containers is that things which used to be transient (thus dying with the process) do become persisted (e.g. in image history, build cache). For instance, even when using environment variables, a simple docker inspect <... >
or docker image history <...>
can reveal sensitive data which survived the process (container) even after its exit.
Prerequisites
- Docker Engine v1.13 or newer
- Docker Swarm (as of now because secrets are stored in the encrypted Swarm Raft log)
- If using Docker-Compose: Docker-Compose File v3.1 or newer
Use Cases
Secrets, once created via e.g. docker cli
, are replicated among Swarm hosts. This gives you a replicated (thus host-independent), read-only tmpfs volume where each secret/string can be up to 500 kb in size.
If granted access to the secret and no matter on which Swarm host the granted container is running, Docker Secrets are mounted into the container in a tmpfs filesystem at /run/secrets/
.
1. PKI Files/ Certificates
Create Secret
Creating a secret from a file via docker cli
is simple:
docker secret create \
--label environment=development \
--label type=cert \
app_cert_key app_key.pem
This creates a secret called app_cert_key
from the file app_key.pem
in the current directory. The specified labels are optional but may be used to later filter certain secrets.
Grant access
If using docker-compose
version 3.1 is necessary:
version: "3.1"
services:
<service_name>:
<...>
secrets:
- app_cert_key
<...>
secrets:
app_cert_key:
external: true
Read Secret docker cli
While the container is running, you can:
docker exec <container_id> cat /run/secrets/app_cert_key
Read Secret nodejs
From the NodeJS application you need to use the fs
module:
const fs = require('fs');
fs.readFileSync('/run/secrets/app_cert_key', 'utf8')
# or
fs.readFile('/run/secrets/app_cert_key', 'utf8')
# or
fs.readFile('/run/secrets/app_cert_key', 'utf8').trim()
While .trim()
is not mandatory for a certificate (it is read correctly even without trimming) it is important to use for other strings as a Line Feed
is at the end of the read string.
2. Credentials
Create Secret
Although not advisable (as preserved in your shell history) secrets can be piped with an echo
echo "This is a secret" | docker secret create my_secret_data -
However, the first pipe may also be something less verbose such as e.g. mac os's keychain:
security find-generic-password -a <account> -w /path/to/keychain | docker secret create my_secret_data -
Grant access
Via docker-compose
v3.1 you might consider using an environment variable for the name (not the content) of the secret:
export MY_SECRET_DATA_ENV_VAR="my_secret_data"
version: "3.1"
services:
<service_name>:
<...>
secrets:
- MY_SECRET_DATA
environment:
- MY_SECRET_DATA_ENV_VAR=${MY_SECRET_DATA_ENV_VAR}
<...>
secrets:
MY_SECRET_DATA:
external:
name: ${MY_SECRET_DATA_ENV_VAR}
Depending on your overall environment(s) this offers the opportunity to switch the name (and thus the content) of your secret in each environment.
Read Secret docker cli
Again, while the container is running, you can:
docker exec <container_id> cat /run/secrets/my_secret_data
You can as well do a docker secret inspect my_secret_data
to check the metadata (especially added labels
).
Read Secret nodejs
Again, in the NodeJS application you need to use the fs
module and can also access the MY_SECRET_DATA_ENV_VAR
environment variable:
const fs = require('fs');
fs.readFileSync('/run/secrets/' + process.env.MY_SECRET_DATA_ENV_VAR, 'utf8').trim()
Now .trim()
is mandatory to use as a line feed
is at the end of the string. You can check e.g. via:
console.log(fs.readFileSync('/run/secrets/' + process.env.MY_SECRET_DATA_ENV_VAR));
console.log(fs.readFileSync('/run/secrets/' + process.env.MY_SECRET_DATA_ENV_VAR, 'utf8'));
console.log(fs.readFileSync('/run/secrets/' + process.env.MY_SECRET_DATA_ENV_VAR, 'utf8') + "test");
console.log(fs.readFileSync('/run/secrets/' + process.env.MY_SECRET_DATA_ENV_VAR, 'utf8').trim() + "test2");
# Outputs
<Buffer 54 68 69 73 20 69 73 20 61 20 73 65 63 72 65 74 0a>
This is a secret
This is a secret
test
This is a secrettest2
The 0a
at the end of the Buffer
represents a line feed
. This is also shown by the next two console.log
outputs. The last output shows that by .trim()
the line feed
has been removed.
If worried about using .trim()
(as e.g. you might want to preserve something that .trim()
removes) you might want to consider .toString().slice(0, -1))
which only removes the last character.
3. Configuration files
As indicated by Docker secrets can also be used for
- ...
- other important data such as the name of a database or internal server
- Generic strings or binary content (up to 500 kb in size)
Remembering the fact that docker secrets
provide you with a fully replicated, read-only tmpfs filesystem, this opens up possibilities to share (small) state or configuration between containers across hosts.
Update: Meanwhile there are Docker Configs
available which work pretty much like Docker Secrets
.
Limitations
Although docker secrets
do remove the aspect of persisting sensitive data mentioned in the beginning, two things need to be kept in mind:
- Now (all) Docker hosts do persist the secrets. Well encrypted in the Swarm Raft log, but they are now there.
- During container runtime everybody with access to
docker exec <container_id>
can retrieve the data.
The second point even remains when using the long syntax for advanced file permissions and the nodejs container running as user node
(as configured in the official image)
version: "3.1"
services:
<service_name>:
<...>
secrets:
- source: app_cert_key
target: app_cert_key_name_in_container
uid: "1000"
gid: "1000"
mode: 0400
<...>
secrets:
app_cert_key:
external: true
The following commands still work:
docker exec <container_id> cat /run/secrets/app_cert_key_name_in_container`
docker exec -u root <container_id> cat /run/secrets/app_cert_key_name_in_container
The following command does not work (Permission denied
):
docker exec -u <other_unpriviledged_user> <container_id> cat /run/secrets/app_cert_key_name_in_container`
Conclusion
As these limitations do not differ very much from using Docker without secrets, docker secrets
are worth using as they at least remove sensitive data from your images. Especially in conjunction with environment variables they offer flexibility for passing varying (and not only sensitive) data to containers.
Further information
Docker Docs & Blog:
https://docs.docker.com/engine/swarm/secrets/#simple-example-get-started-with-secrets
https://docs.docker.com/compose/compose-file/#secrets
https://blog.docker.com/2017/02/docker-secrets-management/
Docker Configs:
https://docs.docker.com/engine/swarm/configs/
https://docs.docker.com/engine/reference/commandline/config/
Other tutorials:
http://blog.alexellis.io/swarm-secrets-in-action/
https://dzone.com/articles/docker-security-using-docker-secrets-with-swarm
Utilities used:
https://nodejs.org/api/fs.html
https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/String/Trim