Mit Docker Version 1.13 hielt eine eigene Verwaltung für Zugangsdaten Einzug, die sogenannten Docker Secrets. Damit ist es möglich Services mit Zugangsdaten, wie Passwörter und Zertifikate zu verknüpfen. Docker speichert diese verschlüsselt innerhalb seines Swarm Mode Clusters.

Es war einmal ...

Doch bevor ich genauer darauf eingehe, möchte ich beleuchten, wie ohne Docker Secrets damit umgegangen wird.

Da mir der praktische Nutzen meiner Artikel sehr wichtig ist, zeige ich im nachfolgenden Beispiel eine docker-compose.yml, die demzufolge Docker Compose nutzt. Die Datei enthält nur einen MySQL-Dienst. Das Beispiel ließe sich auch mit dem Docker CLI und einem docker container run umsetzen. Ich gehe aber davon aus, dass es sich in der Realität um einen komplexeren Stack, wie LAMP oder ELK handelt, bei dem die Befehle mit docker container run schnell unübersichtlich werden können und wir den Stack später einfach mit docker stack deploy auf das Produktiv-System bringen können.

version: "2"

services:
  mysql:
    image: mariadb:10.4
    environment:
      MYSQL_ROOT_PASSWORD: myrootpassword
      MYSQL_DATABASE: mydatabase
      MYSQL_USER: myuser
      MYSQL_PASSWORD: mypassword

Anmerkung der Redaktion: Die obige docker-compose.yml nutzt Version 2 des Standards.

Mit einer solchen Datei im aktuellen Verzeichnis und docker-compose up -dstarten wir unseren Container. Dabei werden die Werte aus dem Node environment als Environment Variables bekannt gemacht. docker-compose exec mysql env zeigt uns alle Umgebungsvariablen des laufenden Containers.

[...]
MYSQL_ROOT_PASSWORD=myrootpassword
MYSQL_PASSWORD=mypassword
MYSQL_USER=myuser
MYSQL_DATABASE=mydatabase
[...]

Laut Heroku's 12 Factor App sind Environment Variables eine ideale Möglichkeit die verschiedenen Konfigurationen in den unterschiedlichen Umgebungen bereitzustellen. Allerdings bringen Env Vars einige Probleme mit sich, die Aaron Franks in seinem lesenswerten Blog-Beitrag zusammenfasst. Bedenken sollten man bei der Entscheidung von Heroku's Autoren, dass sie einen generischen Weg gesucht haben, der mit möglichst vielen Technologien funktioniert und die Umgebungsvariablen sind in den meisten Programmiersprachen ansprechbar. Dazu passt der folgende Auszug aus dem Manifest:

The twelve-factor app stores config in environment variables [...] and unlike custom config files, or other config mechanisms such as Java System Properties, they are a language- and OS-agnostic standard.

Heroku, The Twelve-Factor App, Chapter 3: Config

Als Alternative zur Definition einer Liste innerhalb des Knotens environmentgibt es noch die Variante env_file: my-environment.txt zu nutzen, um einen Pfad zu einer Datei nach der env-Syntax zu nutzen. Das macht die docker-compose.yml lesbarer bei der Verwendung vieler Variablen.

$ cat my-environment.txt
MYSQL_ROOT_PASSWORD=myrootpassword
MYSQL_PASSWORD=mypassword
MYSQL_USER=myuser
MYSQL_DATABASE=mydatabase

Der neue Standard: Docker Secrets

Die nachfolgend beschriebenen Docker Secrets lassen sich nur unter Verwendung von Services und dem Swarm Mode nutzen. Den Wunsch nach größtmöglicher Parität aller eingesetzten Umgebungen, brechen wir damit ein wenig, aber wir werden später noch einen Weg kennenlernen, wie wir diesen Unterschied minimieren.

Die Docker Secrets lassen sich selbstverständlich auch mit dem Docker CLI und dem Basis-Befehl docker secret nutzen. Ich möchte die Docker Secrets, allerdings wieder am Beispiel einer docker-compose.yml erläutern.

version: "3.7"

services:
  mysql:
    image: mariadb:10.4
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
      MYSQL_DATABASE: mydatabase
      MYSQL_USER: myuser
      MYSQL_PASSWORD_FILE: /run/secrets/mysql_password
    secrets:
      - mysql_root_password
      - mysql_password

secrets:
  mysql_root_password:
    file: secrets/mysql_root_password.txt
  mysql_password:
    file: secrets/mysql_password.txt

Anmerkung der Redaktion: Die obige docker-compose.yml nutzt Version 3.7 des Standards.

Im Vergleich zur vorigen Datei fällt der neue Knoten secrets auf, der Verwendung auf zwei Ebenen findet. Zum einen auf der Hauptebene (unten), wo in diesem Beispiel auf externe Text-Dateien verwiesen wird. Möglich wäre es auch ein externes, bereits über das CLI angelegte, Secret zu verwenden. Zum anderen wird der neue Knoten innerhalb der Service-Definition benutzt, um klarzumachen, auf welche Secrets der Dienst Zugriff haben soll. Bereitgestellt werden die Geheimnisse innerhalb des Containers, im Verzeichnis /run/secrets/SECRET_NAME als Datei. Das MariaDB-Imageberücksichtigt die Zugangsdaten in diesem Verzeichnis standardmäßig nicht, aber die offiziellen Docker-Images akzeptieren bekannte Environment Variablen, wie MYSQL_ROOT_PASSWORD mit dem Suffix _FILE und dem gewünschten Pfad zum Secret. Im obigen Beispiel nutze ich für MYSQL_DATABASE und MYSQL_USER herkömmliche Env Vars, um den Einsatz beider Wege zu veranschaulichen. Für den Paranoia Modus lassen sich diese Werte auch über Secrets verwalten. Diese Datei ließe sich mit docker stack deploy -c docker-compose.yml mystack wunderbar in Produktion bringen. Zuvor müssen nur die zwei Dateien mit den Passwörtern angelegt werden.

Einsatz von eigenen Images

Nun haben wir ausführlich über die Verwendung des offiziellen MariaDB-Images gesprochen. Wie sieht es bei eigenen Images aus? Wie kann man die Zugangsdaten, die in die Container gereicht werden, an den Ort bringen, wo sie benötigt werden. Hierzu wechseln wir die Perspektive und schauen nicht mehr auf eine Stack-Defintion in Form einer docker-compose.yml, sondern schauen uns ein Dockerfile einer Go-Applikation an, die eine Config-Datei erwartet. Im besten Fall ist die Applikation so flexibel, dass sie die Übergabe eines eigenen Config-Pfads beim Aufruf ermöglicht, dann hängt man den Pfad einfach beim Aufruf an, wie im Folgenden zu sehen.

FROM golang:1.8-alpine

COPY src /usr/src/app
WORKDIR /usr/src/app

RUN go build && \
    chmod +x app

CMD ["./app", "-c", "/run/secrets/myconfig"]
EXPOSE 80

Bedenken sollte man dabei, dass die Secrets nur im Swarm Mode, bei der Verwendung von Services, bzw. Stacks, genutzt werden können, also aller Wahrscheinlichkeit nach vor allem auf dem Produktivsystem. Es ist aber kein Problem für die lokale Entwicklung mittels Docker Compose, eine Beispiel-Config an die gleiche Stelle, also unter /run/secrets/myconfig zu mounten oder das Kommando zum Ausführen der Applikation mittels docker-compose.yml zu überschreiben, wie unten stehend zu sehen.

version: "3"

services:
  app:
    build:
      context: .
    volumes:
      - "./my-example-config.txt:/run/secrets/my-example-config"
    command: "./app -c /run/secrets/my-example-config"

Wenn man nun allerdings Applikationen verpacken möchte, bei denen sich der Pfad zur Konfigurationsdatei gar nicht oder nur sehr aufwendig anpassen lässt, müssen wir die Config-Datei vor der Laufzeit der Applikation in Position bringen. Das erreichen wir mithilfe des folgenden Shell-Skripts, das wir im Dockerfile als ENTRYPOINT definieren.

#!/bin/sh
set -e

SOURCE="/run/secrets/my-example-config"
DESTINATION="/etc/app-config"

if [ -s "$SOURCE" ] && [ ! -f "$DESTINATION" ]; then
    ln -sf "$SOURCE" "$DESTINATION"
    echo "Config link created."
else
    echo "Config already exists."
fi

exec "$@"

Darin wird überprüft, ob das Secret an der erwarteten Stelle gefunden wurde und wenn noch kein Link am Ziel-Pfad existiert, wird ein Sym-Link an dieser Stelle erzeugt. Man könnte die Datei auch alternativ an den Bestimmungsort kopieren. Das würde allerdings den Docker Secrets die Möglichkeit nehmen, einem Service auch wieder Secrets zu entziehen, da die Kopie ja noch im Container verweilen würde. Anschließend folgt das passende Dockerfile dazu.

FROM golang:1.8-alpine

COPY src /usr/src/app
WORKDIR /usr/src/app

RUN go build && \
    chmod +x app

COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["./app", "-c", "/run/secrets/myconfig"]
EXPOSE 80

Fazit

Mit dem Secret Management hat Docker einen großen Schritt im Bereich der Produktivumgebung gemacht, wo die Secrets ihre Stärken ausspielen können. So werden die Inhalte bereits von Hause aus verschlüsselt. Im Swarm Mode lassen sich Services spielerisch Passwörter oder Ähnliches zuweisen und auch wieder entziehen. Als tmpfs-Mount werden die Infos nur flüchtig im Container gehalten, um dem hohen sicherheitstechnischen Anspruch gerecht zu werden.

Bild von Andrew Measham