I'm sure we've all been there. That moment when you realize that important and sensitive access details have leaked online into a public space and potentially rendered your services to unrequited access. With the ever-growing amount of services we depend on for our development stack, the number of sensitive details to remember and track has also increased. To cope with this problem, tools have emerged in the field of "secrets management." In this post, I am going to look at Docker Secrets, the new secrets management feature available in Docker 1.13 and higher.
The feature doesn't require much work on your part from a Docker perspective, but you may need to refactor your applications to take advantage of it. I will present ideas on how to do this, but not exhaustively.
Docker Secrets only work across Docker swarms, mostly because that's the area where secrets management makes the most sense. After all, Swarm is aimed at production usage where multiple Docker instances need to share access details between themselves. If you want to use secrets management in a standalone container, you need to run a container with a
scale value set to '1'. Docker for Mac and Windows doesn't support multinode swarm mode, but you can use them to create a multinode swarm with Docker Machine.
As you create secrets from the command line, you have the whole gamut of tools available to you to for creating random passwords and piping output. For example, to create a random password for a database user:
openssl rand -base64 20 | docker secret create mariadb_password -
This will return an ID for the secret.
You need to issue this command a second time to generate a password for the MariaDB root user. You will need this to get started, but you won't need this for every service.
openssl rand -base64 20 | docker secret create mariadb_root_password -
If you're already forgetting the secrets you've created, then the time-honored
ls command also works here:
docker secret ls
To keep the secrets, well, secret, communication between the services happens in an overlay network that you define. They're only available in that overlay network by calling their ID.
docker network create -d overlay mariadb_private
This will also return an ID for that network. Again you can use
docker network ls to remind yourself of what's available.
This example will have a Docker node running MariaDB, and a node running Python. In a final application, the Python application would read and write to the database.
First, add a MariaDB service. This service uses the network you created to communicate, and the secrets created earlier are saved into two files: one for the root password and one for a default user password. Then pass all the variables you need to the service as environment variables.
docker service create \ --name mariadb \ --replicas 1 \ --network mariadb_private \ --mount type=volume,source=mydata,destination=/var/lib/mariadb \ --secret source=mariadb_root_password,target=mariadb_root_password \ --secret source=mariadb_password,target=mariadb_password \ -e MARIADB_ROOT_PASSWORD_FILE="/run/secrets/mariadb_root_password" \ -e MARIADB_PASSWORD_FILE="/run/secrets/mariadb_password" \ -e MARIADB_USER="python" \ -e MARIADB_DATABASE="python" \ mariadb:latest
The Python instance again uses the private network you created and copies the secrets accessible in the network. A better (production-ready) option would be to create the databases your application needs in an admin program and never give the application access to root passwords, but this is purely for an example.
docker service create \ --name cspython \ --replicas 1 \ --network mariadb_private \ --publish 50000:5000 \ --mount type=volume,source=pydata,destination=/var/www/html \ --secret source=mariadb_root_password,target=python_root_password,mode=0400 \ --secret source=mariadb_password,target=python_password,mode=0400 \ -e PYTHON_DB_USER="python" \ -e PYTHON_DB_ROOT_PASSWORD_FILE="/run/secrets/python_root_password" \ -e PYTHON_DB_PASSWORD_FILE="/run/secrets/python_password" \ -e PYTHON_DB_HOST="mariadb:3306" \ -e PYTHON_DB_NAME="python" \ chrischinchilla/cspython:latest
The example above uses a simple Docker image I created that sets up packages for creating a web application using Flask for serving web pages and PyMySQL for database access. The code doesn't do much but shows how you could access the environment variables from the Docker container.
For example, to connect to the database server with no database specified:
import os import MySQLdb db = MySQLdb.connect(host=os.environ['PYTHON_DB_HOST'], user=os.environ['PYTHON_DB_ROOT_USER'], passwd=os.environ['PYTHON_DB_PASSWORD_FILE']) cur = db.cursor() print(db) db.close()
It's good practice to frequently change sensitive information. However, as you likely know, updating those details in applications is a dull process most would rather avoid. Via Services, Docker Secrets management allows you to change the values without having to change your code.
Create a new secret:
openssl rand -base64 20 | docker secret create mariadb_password_march -
Remove the access to the current secret from the MariaDB service:
docker service update \ --secret-rm mariadb_password \ mariadb
And give it access to the new secret, pointing the target to the new value:
docker service update \ --secret-add source=mariadb_password_march,target=mysql_password \ mariadb
Update the Python service:
docker service update \ --secret-rm mariadb_password \ --secret-add source=mariadb_password_march,target=python_password,mode=0400 \ cspython
And remove the old secret:
docker secret rm mariadb_password
Spread the Word
Docker Secrets is a new feature, but Docker encourages image maintainers to add support for it as soon as possible for better security among Docker users. This entails allowing for a similar process to the example above, where a container can read every parameter it needs from a file(s) created by generating a secret instead of hard-coded into an application. This enforces the idea of containerized applications as containers can come and go, but always have access to the important information you need for your application to run.
For more details on the
secret command (there's not much!), read the reference guide here.