Dealing with passwords, private keys, and API tokens in Docker containers can be tricky. Just a few wrong moves, and you'll accidentally expose private information in the Docker layers that make up a container. In this tutorial, I'll review the basics of Docker architecture so you can better understand how to mitigate risks. I'll also present some best practices for protecting your most sensitive data.

It has become fairly common practice to push Docker images to public repositories like hub.docker.com. This is a great convenience for distributing containerized apps and for building out application infrastructure. All you have to do is docker pull your image and run it. However, you need to be careful what you push to hub.docker.com or you can accidentally expose sensitive information.

If you are relatively new to using Docker, they have really great Getting Started Guides to get you comfortable with some of the topics we will discuss in this tutorial.

To better understand some of the risks associated with using private data in Docker, you first need to understand a few pieces of Docker's architecture. All Docker containers run from Docker images. So when you docker run -it ubuntu:vivid /bin/bash, you are running the image ubuntu:vivid. Almost all images, even Ubuntu, are composed of intermediate images or layers. When it comes time to run Ubuntu in Docker, the Union File System (UFS) takes care of combining all the layers into the running container. For example, we can step back through the layers that make up the Ubuntu image until we no longer find a parent image.

$ docker inspect --format='{{.Parent}}' ubuntu:vivid
2bd276ed39d5fcfd3d00ce0a190beeea508332f5aec3c6a125cc619a3fdbade6
$ docker inspect --format='{{.Parent}}' 2bd276ed39d5fcfd3d00ce0a190beeea508332f5aec3c6a125cc619a3fdbade6
13c0c663a321cd83a97f4ce1ecbaf17c2ba166527c3b06daaefe30695c5fcb8c
$ docker inspect --format='{{.Parent}}' 13c0c663a321cd83a97f4ce1ecbaf17c2ba166527c3b06daaefe30695c5fcb8c
6e6a100fa147e6db53b684c8516e3e2588b160fd4898b6265545d5d4edb6796d
$ docker inspect --format='{{.Parent}}' 6e6a100fa147e6db53b684c8516e3e2588b160fd4898b6265545d5d4edb6796d

$

The last docker inspect doesn't return a parent, so this is the base image for Ubuntu. We can look at how this image was created by using docker inspect again to view the command that created the layer. The command below is from the Dockerfile and it ADDs the root file system for Ubuntu.

$ docker inspect --format='{{.ContainerConfig.Cmd}}' 6e6a100fa147e6db53b684c8516e3e2588b160fd4898b6265545d5d4edb6796d
{[/bin/sh -c #(nop) ADD file:49710b44e2ae0edef44de4be4deb8970c9c48ee4cde29391ebcc2a032624f846 in /]}

This brings us to an important point in how Docker works. You see, each intermediate image has an associated command, and those commands come from Dockerfiles. Even containers like Ubuntu start off with a Dockerfile. It's a simple matter to reconstruct the original Dockerfile. You could use docker inspect to walk up through the images to the root, collecting commands at each step. However, there are tools available that make this a trivial task.

One very compact tool I like is dockerfile-from-image created by CenturyLink Labs. If you don't want to install any software, you can use ImageLayers to explore images.

Here's the output of dockerfile-from-image for Ubuntu.

$ dfimage ubuntu:vivid
ADD file:49710b44e2ae0edef44de4be4deb8970c9c48ee4cde29391ebcc2a032624f846 in /
RUN <-- edited to keep this short
CMD ["/bin/bash"]

As you can see, it's very easy to reconstruct the Dockerfile from an image. Let's take a look at another Docker image I created. This container has nginx running with an SSL certificate.

$ dfimage insecure-nginx-copy
FROM ubuntu:vivid
MAINTAINER Matthew Close "https://github.com/mclose"
RUN apt-get update && apt-get -y upgrade
RUN apt-get -y install nginx && apt-get clean
RUN echo "daemon off;" >> /etc/nginx/nginx.conf
COPY file:0ed4cccc887ba42ffa23befc786998695dea53002f93bb0a1ecf554b81a88c18 in /etc/nginx/conf.d/example.conf
COPY file:15688098b8085accfb96ea9516b96590f9884c2e0de42a097c654aaf388321a5 in /etc/nginx/ssl/nginx.key
COPY file:bacae426a125600d9e23e80ad12e17b5d91558c9d05d9b2766998f2245a204cd in /etc/nginx/ssl/nginx.crt
CMD ["nginx"]

See the problem? If I were to push this image to hub.docker.com, anyone would be able to obtain the private key for my nginx server.

Note: You should never use COPY or ADD with sensitive information in a Dockerfile if you plan to share the image publicly.

Perhaps you don't need to copy files into a container, but you do need an API token to run your application. You might think that using ENV in a Dockerfile would be a good idea; unfortunately, even that will lead to publicly disclosing the token if you push it to a repository. Here's an example using a variation on the nginx example from above:

$ dfimage insecure-nginx-env
FROM ubuntu:vivid
MAINTAINER Matthew Close "https://github.com/mclose"
RUN apt-get update && apt-get -y upgrade
RUN apt-get -y install nginx && apt-get clean
RUN echo "daemon off;" >> /etc/nginx/nginx.conf
ENV MYSECRET=secret
COPY file:0ed4cccc887ba42ffa23befc786998695dea53002f93bb0a1ecf554b81a88c18 in /etc/nginx/conf.d/example.conf
COPY file:15688098b8085accfb96ea9516b96590f9884c2e0de42a097c654aaf388321a5 in /etc/nginx/ssl/nginx.key
COPY file:bacae426a125600d9e23e80ad12e17b5d91558c9d05d9b2766998f2245a204cd in /etc/nginx/ssl/nginx.crt
CMD ["nginx"]

Spot the problem this time? In the above output, ENV commands from the Dockerfile are exposed. Therefore, using ENV for sensitive information in the Dockerfile isn't a good practice either.

So what are some possible solutions? The easiest one is to separate the process of building your containers from the process of customizing them. Keep your Dockerfiles to a minimum and add the sensitive tokens and password files at run time. Taking the nginx example above again, here's how I might solve my problem with SSL keys and environment variables.

First, let's take a look at the Dockerfile for this container.

$ dfimage nginx-volumes
FROM ubuntu:vivid
MAINTAINER Matthew Close "https://github.com/mclose"
RUN apt-get update && apt-get -y upgrade
RUN apt-get -y install nginx && apt-get clean
RUN echo "daemon off;" >> /etc/nginx/nginx.conf
COPY file:0ed4cccc887ba42ffa23befc786998695dea53002f93bb0a1ecf554b81a88c18 in /etc/nginx/conf.d/example.conf
CMD ["nginx"]

There isn't much that's exposed in this file. Certainly, I'm not sharing private keys or API tokens. Here's how you'd then get the sensitive information back into the container at run time.

$ docker run -d -p 443:443 -v $(pwd)/nginx.key:/etc/nginx/ssl/nginx.key -v $(pwd)/nginx.crt:/etc/nginx/ssl/nginx.crt -e "MYSECRET=secret" nginx-volumes

By using volumes at run time, I'm able to share my private key as well as set an environment variable I want to keep secret. In this example, it would be perfectly fine to push the image built from my Dockerfile to a public repository because it no longer contains sensitive information. However, since I'm setting environment variables from the command line when I run my container, my shell history now includes information that it probably shouldn't. Even worse, the command to run my Docker containers becomes much more complex. I need to remember every file and environment variable that's needed to make my container run.

Fortunately, there is a better tool called docker-compose that will allow us to easily add run time customization and make running our containers a simple command. If you need an introduction to docker-compose, you should take a look at the Overview of Docker Compose.

Here's my basic configuration file, docker-compose.yml, to deploy my nginx container.

nginx:
    build: .
    ports:
        - "443:443"
    volumes:
        - ./nginx.key:/etc/nginx/ssl/nginx.key
        - ./nginx.crt:/etc/nginx/ssl/nginx.crt
        - ./nginx.conf:/etc/nginx/conf.d/example.conf
    env_file:
        - ./secrets.env

After running docker-compose build and docker-compose up, my container is up and running with the sensitive information. If I use dfimage on the image that was built, it only contains the basics to make nginx ready to run.

$ dfimage nginxcompose_nginx
FROM ubuntu:vivid
MAINTAINER Matthew Close "https://github.com/mclose"
RUN apt-get update && apt-get -y upgrade
RUN apt-get -y install nginx && apt-get clean
RUN echo "daemon off;" >> /etc/nginx/nginx.conf
CMD ["nginx"]

By using docker-compose, I've managed to separate the build process from how I customize and run a container. I can now also confidently push my Dockerfile images to public repos and not worry that sensitive information is being posted for the world to uncover.

Summary

Passwords, private keys, and API tokens in Docker containers can be tricky. After a basic understanding of Docker architecture, with the implementation of some best practices for protecting your most sensitive data, you can mitigate risks.

Don't have an account on CenturyLink Cloud? No problem. Get started for free and receive a substantial credit toward any of our products or services.

Sign up for our Developer-focused newsletter CODE. Designed hands-on by developers, for developers. Keep up to date on topics of interest: tutorials, tips and tricks, and community building events.