Wednesday, November 13, 2019

The Secrets of Docker Secrets

Most web apps need login information of some kind, and it is a bad idea to put them in your source code where it gets saved to a git repository that everyone can see. Usually these are handled by environment variables, but Docker has come up with what they call Docker secrets. The idea is deceptively simple in retrospect. While you figure it out it is arcane and difficult to parse what is going on.

Essentially the secrets function create in memory files in the docker image that contain the secret data. The data can come from files, or a Docker swarm.

The first thing to know is that the application running in the docker image needs to be written to take advantage of the Docker secrets function. Instead of getting the password from an environment variable, it would get the password from the file system at /run/secrets/secretname. Not all images available use this functionality. If they don't describe how to use Docker secrets, the won't work. The files will be created in the image, but the application won't read them.

For a development setup having files outside of the git source tree works well. To create a file with a secret, I created a folder called serverdata, with a dev/ and prod/ folder within. In the dev/ folder, run this command with all the secret data you will need:

echo "shh, this is a secret" > mysecret.txt
The names simply need to tell you what they do. What the secret is called in the image is set in the docker configuration. This is what my dev/ folder looks like:
-rw-r--r-- 1 derek derek 66 Nov  5 14:49 mongodb_docker_path
-rw-r--r-- 1 derek derek  6 Oct 22 14:09 mongodb_rootusername
-rw-r--r-- 1 derek derek 13 Oct 22 14:08 mongodb_rootuserpwd
-rw-r--r-- 1 derek derek 18 Oct 22 14:10 mongodb_username
-rw-r--r-- 1 derek derek 14 Oct 22 14:10 mongodb_userpwd
-rw-r--r-- 1 derek derek 73 Oct 22 14:02 oauth2_clientid
-rw-r--r-- 1 derek derek 25 Oct 22 14:02 oauth2_clientsecret
-rw-r--r-- 1 derek derek 14 Oct 22 14:03 oauth2_cookiename
-rw-r--r-- 1 derek derek 25 Oct 22 14:04 oauth2_cookiesecret
-rw-r--r-- 1 derek derek 33 Oct 26 08:27 oauth2_redirecturl

Function and description. I have some configuration details as well.

Using Secrets with docker-compose

This is the docker-compose.yml that builds a mongodb image with all the configuration.
version: '3.6'services:
  mongo-replicator:
    build: ./mongo-replicator
    container_name: mongo-replicator
    secrets:
      - mongodb_rootusername
      - mongodb_rootuserpwd
      - mongodb_username
      - mongodb_userpwd
    environment:
      MONGO_INITDB_ROOT_USERNAME_FILE: /run/secrets/mongodb_rootusername
      MONGO_INITDB_ROOT_PASSWORD_FILE: /run/secrets/mongodb_rootuserpwd
      MONGO_INITDB_DATABASE: admin
    networks:
      - mongo-cluster
    depends_on:
      - mongo-primary
      - mongo-secondary
 
And the secrets are defined as follows:
secrets:
  mongodb_rootusername:
    file: ../../serverdata/dev/mongodb_rootusername
  mongodb_rootuserpwd:
    file: ../../serverdata/dev/mongodb_rootuserpwd
  mongodb_username:
    file: ../../serverdata/dev/mongodb_username
  mongodb_userpwd:
    file: ../../serverdata/dev/mongodb_userpwd
  mongodb_path:
    file: ../../serverdata/dev/mongodb_docker_path
The secrets: section reads the contents of the file into a namespace, which is the name of the /run/secrets/filename. Mongo docker image looks for an environment variable with the suffix _FILE, then reads the secret from that file in the image file system. Those are the only two variables supported by the Mongo image.

Of course it gets more complicated. I wanted to watch the changes in the database within my node application for various purposes. This function is only supported in a replicated set in Mongo. To fully automate the configuration and initialization of Mongo within Docker images using replication requires a second Docker image that waits for the Mongo images to initialize, then runs a script. So here is the complete docker-compose.yml for setting up the images:
version: '3.6'services:
  mongo-replicator:
    build: ./mongo-replicator
    container_name: mongo-replicator
    secrets:
      - mongodb_rootusername
      - mongodb_rootuserpwd
      - mongodb_username
      - mongodb_userpwd
    environment:
      MONGO_INITDB_ROOT_USERNAME_FILE: /run/secrets/mongodb_rootusername
      MONGO_INITDB_ROOT_PASSWORD_FILE: /run/secrets/mongodb_rootuserpwd
      MONGO_INITDB_DATABASE: admin
    networks:
      - mongo-cluster
    depends_on:
      - mongo-primary
      - mongo-secondary

  mongo-primary:
    container_name: mongo-primary
    image: mongo:latest
    command: --replSet rs0 --bind_ip_all
    environment:
      MONGO_INITDB_DATABASE: admin
    ports:
      - "27019:27017"    networks:
      - mongo-cluster
  mongo-secondary:
    container_name: mongo-secondary
    image: mongo:latest
    command: --replSet rs0 --bind_ip_all
    ports:
      - "27018:27017"    networks:
      - mongo-cluster
    depends_on:
      - mongo-primary
The Dockerfile for the mongo-replicator looks like this:
FROM mongo:latest
ADD ./replicate.js /replicate.js
ADD ./seed.js /seed.js
ADD ./setup.sh /setup.sh
CMD ["/setup.sh"]
Mongo with various scripts added to it. Here they are.

replicate.js
rs.initiate( {
  _id : "rs0",  members: [
    { _id: 0, host: "mongo-primary:27017" },    { _id: 1, host: "mongo-secondary:27017" },  ]
});
seed.js
db.users.updateOne(
    { email: "myemail@address.com"},    { $set: { email: "myemail@address.com", name: "My Name"} },    { upsert: true },);
and finally what does all the work, setup.sh
#!/usr/bin/env sh
if [ -f /replicated.txt ]; then  echo "Mongo is already set up"else  echo "Setting up mongo replication and seeding initial data..."  # Wait for few seconds until the mongo server is up  sleep 10  mongo mongo-primary:27017 replicate.js
  echo "Replication done..."  # Wait for few seconds until replication takes effect  sleep 40
  MONGO_USERNAME=`cat /run/secrets/mongodb_username|tr -d '\n'`  MONGO_USERPWD=`cat /run/secrets/mongodb_userpwd|tr -d '\n'`

mongo mongo-primary:27017/triggers <<EOFrs.slaveOk()use triggersdb.createUser({  user: "$MONGO_USERNAME" ,  pwd: "$MONGO_USERPWD",  roles: [  { role: "dbOwner", db: "admin" },            { role: "readAnyDatabase", db: "admin" },            { role: 'readWrite', db: 'admin'}]})


EOF

  mongo mongo-primary:27017/triggers seed.js
  echo "Seeding done..."  touch /replicated.txt
fi
 In the docker-compose.yml the depends_on: orders the creation of images, so this one waits until the others are done. It runs the replication.js which initializes the replication set, then waits for a while. The password and username are read from the /run/secrets/ file, the linefeed removed, then the user is created in the mongo database. Then seed.js is called to add more initial data

This sets up mongoDb with admin user and password, as well as a user that is used from the node.js apps for reading and writing data.

No passwords in my git repository, and an initialized database. This is working for my development setup, with a mongo database, replicated so that I can get change streams, and read and write function from the node.js application.

More to come.

  1. Using secrets in node.js applications and oauth2_proxy
  2. The oauth2_proxy configuration
  3. Nginx configuration to tie the whole mess together

Comments: Post a Comment

Subscribe to Post Comments [Atom]





<< Home

This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]