31
.
1
.
2017

Docker sicher und schlank einsetzen - PART I

Teil eins mit Docker als Entwicklungswerkzeug

Einleitung

In meinem vorherigen Blogpost habe ich über automatisierte dockerized Builds mit Jenkins Pipeline geschrieben. Dabei kam Docker bereits zur Anwendung, jedoch war der Fokus mehr auf die Benutzung bestehender Docker Images gerichtet.

In diesem Blogpost will ich auf das Erstellen von schlanken und sicheren Docker Images eingehen. Dabei zeige ich am Beispiel einer PHP Applikation, wie schnell man mit eigenen Docker Images eine lokale Buildumgebung aufbauen kann, die bereits stark der Production Umgebung entspricht.

Manch einer möchte mich an dieser Stelle evtl. auf Vagrant hinweisen, welches es einem auch ermöglicht virtualisierte Entwicklungsumgebungen aufzusetzen und mit Kollegen zu teilen. Aus verschiedenen Gründen finde ich, dass es nicht dem einfach und schlank Gedanken entspricht. Jeder soll selbst entscheiden auf welches Pferd er setzen will. Ich für meinen Teil sehe Docker als Universalwerkzeug, mit dem ich verschiedenste Probleme schnell, schlank, deterministisch und gut dokumentiert lösen kann.

 

Kurz nachdem ich den BlogPost fertig gestellt hatte, wurde Docker Version 1.13 released, daher an dieser Stelle ein kleiner Einschub. Die coolsten Neuerungen in v1.13 sind für mich ‘docker build –squash …’, ‘docker system prune’ und ‘docker system df’. Natürlich sind in diesem Release etliche neue Features und Bugfixes enthalten besonders für Swarm-Mode und Deployment mittels docker-compose, aber mich freuen die kleinen Alltagshelfer wie ‘prune’ und ‘squash’ besonders.

docker build –squash …

  • Erlaubt es jetzt unabhängig von der Anzahl der RUN Kommandos ein Image mit nur einem Layer zu erstellen.

docker system df

  • Zeigt einem die Resourcennutzung von Docker Containern an, also wieviel Platz diese auf dem System belegen.

docker system prune

  • Bereinigt ungenutzte Resourcen wie dangling-networks, dangling-container usw.

 

Warum jetzt wieder Docker? Ist das nicht umständlich?

Das war es evtl. bisher. Unter Windows 10 Pro und auch in der neusten Docker Version unter macOS kann auf Boot2Docker und den ganzen zusätzlichen VirtualBox Overhead verzichtet werden.

Die neuste Docker Version läuft unter Windows 10, macOS und Linux ohne Wrapper oder andere Hilfsmittel.

Möchte man bspw. unter Windows 10 schnell mal was in Alpine Linux ausprobieren, so reicht das folgende Kommando, um eine Interaktive Shell in einem auf Alpine Linux basierenden Docker Container zu starten:

shell:docker run -i -t alpine:3.5 sh

 

Alpine Linux dockerized ausführen, fortune installieren und aufrufen
Alpine Linux dockerized ausführen, fortune installieren und aufrufen

 

 

Eigene Docker Images? Es gibt doch Docker Hub!

Es mag für den ein oder anderen komisch erscheinen, warum man eigene Docker Images erstellen sollte, und auch diejenigen die bereits eigene Docker Images bauen sind sich der möglichen Sicherheitsrisiken und Abhängigkeiten evtl. nicht bewusst. Viele benutzen einfach vorgefertigte Images aus dem Docker Hub ohne zu überprüfen, wie das Docker Image eigentlich aufgebaut ist. Gefährlich kann dabei auch das erben von einem Docker Image mittels FROM Anweisung sein.

Man sollte immer wissen worauf man sich da einlässt. Jeder kann auf Docker Hub Images publishen und niemand prüft inhaltlich ob diese sicher sind (Docker Content Trust löst dieses Problem auch nicht). Daher stellt sich die Frage: Wem kann ich trauen? Wenn man mich fragt:

 

Docker Security Scanning Ergebnis für Alpine Linux
Docker Security Scanning Ergebnis für Alpine Linux

 

 

Best-Practices des Linux-Administrators

Was heißt das in der Praxis? Es bedeutet, dass man sich seine Docker Images sorgfältig selbst erstellen und dabei auf die Grundregeln des Linux-Administrators achten sollte.

Compu'er says no ...
Compu'er says no ...
  • Prozesse sollten nicht als root laufen sondern als nicht priviligierter Benutzer.
  • Erteile nur dort schreibende Dateisystemberechtigungen wo sie zwingend nötig sind und bediene dich dabei Benutzer und Gruppen Berechtigungen.
  • Vermeide ‘other’ Dateisystemberechtigungen ala ‘chmod -R 777 /myapp/’
  • Öffne nur die Netzwerk Ports, die nötig sind.
  • Verbiete generell alles und erlaube nur das Nötigste.
  • Benutze Kernel-Hardening- und Zugriffskontroll-Mechanismen wie GrsecuritySELinux oder AppArmor

Speziell auf Docker bezogen ist zu beachten

  • Setze auf dem Docker-Host System eine Firewall ein und verbiete dem Docker-Daemon eigene iptables Regeln anzulegen.
  • Versuche die Docker-Image Layers und damit die Docker-Image Größe so klein wie möglich zu halten.

 

 

Puh! Das klingt nach viel Arbeit … wie soll das gehen?

Ja es bedarf etwas Arbeit, aber wann war Sicherheit jemals einfach. Und ich kenne selbst die Krux des DevOps Mitarbeiters, der mit den manchmal sturen Entwicklern eine Lösung finden muss, wie die Anwendung entsprechend anzupassen ist. Daher meine Tipps:

  • Frühzeitig im agilen Prozess ein Cross-Funktionales Team etablieren (DevOps + Entwickler).
  • Das Docker-Image gemeinsam aufsetzen und als Development-Umgebung für alle Entwickler etablieren.
  • Das Docker-Image als Teil der Code-Base sehen und kontinuierlich pflegen.
  • Bei Produktivnahme kann das Docker-Image 1:1 oder leicht abgewandelt genutzt werden.
GAMBATE!!!
GAMBATE!!!

 

 

Praxisbeispiel: Universelles Docker-Image für PHP Silex Anwendung

Gut genug theoretisiert, wir wollen Action und was sehen. Daher werden wir anhand eines Praxisbeispiels sukzessive das Docker-Image erstellen.

Rahmebedingungen des Praxisbeispiels

  • Bestehender Ubuntu Server 16.04 64-bit dient als Docker Host.
  • Bestehende PHP Anwendung basierend auf dem Silex Framework.

Ziele des Praxisbeispiels

  • (Z1) Entwickler sollen Docker-Image während Entwicklung einsetzen können.
  • (Z2) Docker-Image soll 1:1 auf Staging-Umgebung eingesetzt werden können (Environment Variable).
  • (Z3) Docker-Image soll schlank sein.
  • (Z4) Docker-Image soll Composer bereitstellen, um lokal PHP Bibliotheken installieren zu können.
  • (Z5) Docker-Image soll einen Apache2 Webserver mit PHP 7 bereistellen, der ‘mod_rewrite’ aktiviert hat und den Einsatz von ‘.htaccess’ Dateien mit enthaltenen RewriteRules erlaubt.
  • (Z6) Docker-Image soll ‘gehärtet’ sein. (GrsecuritySELinux oder AppArmor)
  • (Z7) Prozesse sollen nicht als root laufen.
  • (Z8) Docker-Host System soll mit Firewall geschützt sein.

Besonderheit und Abgrenzung

  • Oft wird ja ein und dasselbe Docker Image immer wieder gebaut und darin mittels COPY oder ADD Anweisung die Anwendung hineinkopiert.
  • Wir wollen aber ein universelles Docker-Image bauen, welches einmal gebaut wird und dann über VOLUMES die Anwendung ‘untergeschoben’ bekommt. Ähnlich wie das Docker-Image von plex.tv.
  • Damit das Praxisbeispiel nicht zu komplex wird verzichten wir auf den Einsatz einer Docker-Registry und genügen uns mit lokalem bauen des Docker-Images. Im echten Leben würde ein BuildSystem das Docker-Image bauen und über eine Docker-Registry dem Produktiv-Server zur Verfügung stellen.

 

Demo-Projekt auf GitHub zum Praxisbeispiel
Demo-Projekt auf GitHub zum Praxisbeispiel

 

Schritt 1: Das Docker-Image erzeugen (Dockerfile)

Wir gehen Schritt für Schritt vor. Zunächst erstellen wir das Dockerfile und beginnen mit den grundlegendsten Anweisungen. Es ist sinnvoll die Best-Practices zu befolgen.

github:2198d79f5282defd803126422c2814db

FROM alpine:3.5

RUN apk add –no-cache php7 …

  • Wir installieren zuerst über den offiziellen Paketmanager verschiedene Software Pakete. Darunter (Z5) PHP7, Apache2 Webserver und curl.
  • Da wir nur ein RUN Kommando absetzen und nicht mehrere, wird auch nur ein Image-Layer erzeugt, was zu einer sehr kleinen Gesamt-Image-Größe führt. Somit ist auch (Z3) zum Teil erfüllt.

 

 

Hinweis: Ich trenne die folgenden RUN Kommandos jetzt der Übersichtlichkeit halber auf, aber in der Realität würde man das alles in einem RUN Kommando abhandeln.

github:5876e8d13f9a6bed23c569b19b8c5a7e

RUN ln -sf /dev/stderr /var/log/apache2/error.log

  • Wir linken das Apache2 Webserver Error-Log auf die Standard Fehler Ausgabe, sodass das Logfile angezeigt wird, wenn wir das Image interaktiv starten. Das erfüllt (Z1) und hilft besonders während der lokalen Entwicklung.

RUN ln -s /usr/bin/php7 /usr/bin/php

  • Wir erzeugen für Composer einen Symlink, sodass er die PHP-Executable unter /usr/bin/php findet.

RUN curl -o /usr/bin/composer -J -L https://…/composer.phar && chmod +x /usr/bin/composer

  • Wir laden das (Z4) composer.phar direkt nach /usr/bin herunter und machen es für jeden ausführbar.

 

 

github:baef60db790a72f19fcd0a446498bddd

RUN addgroup -g 10777 phpworker

  • Wir legen die Gruppe ‘phpworker’ an und geben fest die GID 10777 vor. Das dient der Vorbereitung von (Z7) und die feste GID ermöglicht es einem später auf dem Docker-Host System auch eine entsprechende Gruppe mit der GID anzulegen, da die in gemappten Volumes geschriebenen Dateien dann mit dieser GID versehen werden.

RUN adduser -h /phpapp/ -H -D -G phpworker -u 10777 phpworker

  • Wir legen den Benutzer ‘phpworker’ an und geben fest die UID 10777 vor. Gleichzeitig ordnen wir den Benutzer der Gruppe ‘phpworker’ zu und setzen sein Homeverzeichnis auf ‘/phpapp/’. Das dient der Vorbereitung von (Z7) und die feste UID ermöglicht es einem später auf dem Docker-Host System auch einen entsprechenden Benutzer mit der UID anzulegen, da die in gemappten Volumes geschriebenen Dateien dann mit dieser UID versehen werden.

 

 

github:0cc6315d30ff56a59f015b8aef2a6c2b

Jetzt müssen wir etwas springen und Dinge im Zusammenhang erklären. Wir wollen ja später Verzeichnisse auf unserem Docker-Host System mit dem Docker-Container ‘sharen’, sodass der Docker-Container Dateien einlesen kann, die wir ihm vom Docker-Host aus ‘unterschieben’. Genauso soll der Docker-Container auch schreiben können, sodass wir die Dateien auf dem Docker-Host System sehen können. Zu diesem Zweck gibt es die VOLUME Anweisung.

Kurzfassung: Die folgenden Kommandos erstellen innerhalb des Containers die entsprechenden Verzechnisse und versehen sie mit den entsprechenden Dateisystemberechtigungen, sodass später der Apache2 Webserver oder Composer Prozess laufend als non-root Benutzer ‘phpworker’ (Z7) in diesen Verzeichnissen schreiben kann.

RUN mkdir -p /phpapp/www

  • Erstelle die als Volume bereitgestellten Verzeichnisse (gleichartige Befehle gekürzt)

RUN touch /var/www/logs/error.log

  • Erstelle leere Log-Dateien, sodass zur Erstellungszeit des Images bereits die Dateisystemberechtigungen richtig gesetzt werden. (gleichartige Befehle gekürzt)

RUN chown -R phpworker:phpworker /phpapp/

  • Setze die Dateiberechtigungen auf ‘phpworker’ für alle als Volume bereitgestellten Verzeichnisse (gleichartige Befehle gekürzt)

VOLUME [“/phpapp/www”]

  • Nun geben wir an welche Verzeichnisse dafür vorgesehen sind später von außen (Docker-Host) ‘gemounted’ zu werden.
  • Das ist nötig, um im non-root (Z7) Betrieb zur Laufzeit Konflikte mit Dateiberechtigungen zu vermeiden.

 

 

github:3f5bdccfd0e0a8ca4391f5ecbd316c26

RUN sed -i -e ‘s/foo/bar/g’ /etc/php7/php.ini

  • Wir benutzen das Tool ‘sed’ um innerhalb der Konfigurationsdatei von PHP7 ‘php.ini’ verschiedene Einstellungen anzupassen.
  • Angepasst wird:
  • (1) upload_max_filesize wird auf 32MB gesetzt
  • (2) post_max_size wird auf 32MB gesetzt

RUN sed -i -e ‘s/foo/bar/g’ /etc/apache2/httpd.conf

  • Wir benutzen das Tool ‘sed’ um innerhalb der Konfigurationsdatei des Apache Webservers ‘httpd.conf’ verschiedene Einstellungen anzupassen.
  • Angepasst wird:
  • (1) ‘/var/www/localhost/htdocs/’ wird auf ‘ /phpapp/www’ geändert. Das ändert den sogenannten DocumentRoot des Webservers auf ‘/phpapp/www/’ was unser vorbereitetes Volume Verzeichnis ist, in das wir die PHP Anwendung hinein-mounten werden.
  • (2) ‘Listen 80’ wird auf ‘Listen 9999’ geändert, sodass der Webserver auf Port 9999 lauscht. Wegen (Z7) läuft der HTTPD Prozess unter ‘phpworker’ und dieser darf Systemports im niedrigen Bereich nicht ‘binden’.
  • (3) ‘AllowOverride None’ wird auf ‘AllowOverride All’ gesetzt was uns erlaubt (Z5) innerhalb von ‘.htaccess’ Dateien bspw. RewriteRules zu definieren.
  • (4) ‘#LoadModule rewrite_module’ wird auf ‘LoadModule rewrite_module’ gesetzt was das Module ‘rewrite’ (Z5) aktiviert und uns die Verwendung von RewriteRules ermöglicht.

 

 

github:204f4f0126ac8abdf2e8c9a86370350f

WORKDIR /phpapp/www

  • Damit geben wir an, dass Befehle die ausgeführt werden wie bspw. Composer in diesem Verzichnis arbeiten sollen.

EXPOSE 9999

  • Der Apache Webserver läuft auf Port 9999 und diesen Port definieren wir als ‘verwendbar von außen’.

USER phpworker

  • Alle bisherigen Befehle oberhalb dieses Kommandos liefen als root Benutzer ab.
  • Alle folgenden Befehle unterhalb des Kommandos laufen als Benutzer ‘phpworker’ ab, was (Z7) erfüllt.

ENV ENVIRONMENT local

  • Wir setzen die default Umgebung auf ‘local’. Dieser Wert lässt sich später zur Laufzeit innherhalb der PHP Anwendung auslesen. Darauf wird im Abschnitt ‘Schritt 4: Staging-Umgebung’ genauer eingegangen.

CMD [“httpd”, “-DFOREGROUND”]

  • Das letzte Kommando unseres Dockerfiles startet den httpd Prozess im Vordergrund. Das ist nötig, da sich der Docker Container sonst beenden würde.
  • Das Kommando lässt sich auch überschreiben, darauf wird im Abschnitt ‘Schritt 2: Benutzung’ genauer eingegangen.

 

 

Jetzt bleibt uns nichts weiter zu tun als mittels folgendem Kommando mit den Anweisungen aus dem Dockerfile ein Docker-Image mit dem Namen ‘almightyphp’ zu erzeugen. Das fertig gebaute Docker-Image werden wir im Abschnitt ‘Schritt 2: Benutzung’ verwenden.

shell:docker build -t almightyphp .
Bauen des Docker-Images
Bauen des Docker-Images

 

Schritt 2: Benutzung des Docker-Images während der Software-Entwicklung

Als Entwickler möchte ich ein Docker-Image einfach benutzen können. Es soll mir bei meinen alltäglichen Aufgaben helfen und mich nicht darin behindern. Wir haben einleitend bereits definiert, was das Docker-Image können soll, haben es im Abschnitt ‘Schritt 1: Docker-Image’ gebaut und werden es nun verwenden.

Als Basis dient in diesem Abschnitt eine auf dem Silex PHP Framework basierende Anwendung. Zuerst clonen wir uns die Demo-Applikation mittels git.

github:8c6d4f5dab666ba45bef43905f96367b

Nun liegen uns die Dateien index.php.htaccess und composer.json im Verzeichnis ‘php-demo-app’ vor.

Dateien der PHP Demo Applikation
Dateien der PHP Demo Applikation

 

 

Unser Docker-Image stellt uns bereits das Tool Composer zur Verfügung. Mit Composser installieren wir nun die PHP Abhängigkeiten, die in der ‘composer.json’ definiert sind nach ‘/php-demo-app/vendor/’.

github:41ca22ebba0d5665e225eb50cdd27f42

Was hier genau passiert lässt sich am besten wie folgt erklären:

docker run … almightyphp composer update -vvv

  • Starte einen Docker-Container basierend auf dem Docker-Image mit dem Namen ‘almightyphp’ und führe darin das Kommando ‘composer update -vvv’ aus. Das Kommando installiert die PHP Abhängigkeiten, die in der ‘composer.json’ definiert sind.

docker run -i -t …

docker run … -v $(pwd)/php-demo-app/:/phpapp/www …

  • Mittels des ‘-v’ Volume-Kommandos geben wir an welches Verzeichnis auf dem Host wir auf welches Verzeichnis im Container mappen wollen.
  • Somit mappen wir drei Verzeichnisse. Unsere Anwendung nach /phpapp/www, das Composer-Home nach /phpapp/.composer und ein Datenverzeichnis nach /phpapp/data. Es ist immer eine Best-Practice ein Datenverzeichnis separat zur Anwendung zu mounten, und die PHP Anwendung dorthin schreiben zu lassen.
  • Leider ist die schlechte Performance von Docker-Mounts unter macOS mit osxfs ein bekanntes Problem. Abhilfe schaffen vorerst nur Workarounds wie docker-sync.
Abhängigkeiten der PHP Demo Applikation installieren
Abhängigkeiten der PHP Demo Applikation installieren

 

 

Die Abhängigkeiten sind installiert und wir können unsere Anwendung mit Apache2 und PHP7 starten.

github:45197dc68055f6a448a9f661adf395f3

Der Befehl ist sehr ähnlich zum Composer-Update Befehl. Es wird lediglich das Abweichende erklärt.

docker run … almightyphp

  • Da nun nach dem Docker-Image Namen kein Kommando angehängt wurde, wird das default CMD aus dem Dockerfile verwendet. Sprich ‘httpd -DFOREGROUND’ was den Apache2 Webserver startet.

docker run … -p 8899:9999 …

  • Wir mappen den Docker-Host Port 8889 auf den Container Port 9999 und stellen somit unsere PHP Anwendung unter http://localhost:8899/ zur Verfügung.
Abhängigkeiten der PHP Demo Applikation installieren
Abhängigkeiten der PHP Demo Applikation installieren

 

Wir sehen also (Z1) der Entwickler kann das Image während der Entwicklung einsetzen und muss sich weder PHP, Apache2 noch Composer lokal auf seinem System installieren. Die (Z4) Composer Kommandos können mit dem selben Image ausgeführt werden, welches auch die (Z5) Apache2+PHP7 Umgebung zur Verfügung stellt. Die PHP Fehler Ausgabe erfolgt im Falle von Exceptions auf STDOUT.

 

FAZIT UND AUSBLICK

Es wurde gezeigt, wie man ein Docker-Image in der Entwicklung verwenden kann. Sicherheit ist durch den Einsatz von Kernel-Hardening und non-root Containern sichergestellt. Docker zeigt sich als Universaltool, welches in vielen Bereichen zum Einsatz kommen kann.

Wir haben gezeigt wie man mit Alpine Linux als Basis schlanke und sichere non-root Docker-Images bauen kann. Docker lässt sich also insgesamt sicher auch in Produktion einsetzen. Wir betreiben bspw. einige unserer Dienste dockerized bei AWS.

Im zweiten Teil des BlogPosts, der nächste Woche veröffentlicht wird, wird Schritt 3: Dockerized PHP Builds im Jenkins CI Server und Schritt 4: Dockerized Staging-Umgebung und Host-Firewall unter Ubuntu 16.04 zu lesen sein.

 

Wenn auch Sie Ihre Dienste ‘dockerizen’ und beispielsweise in der AWS Cloud betreiben wollen, untersützen wir Sie gerne. Wir können in Workshops oder projektbegleitendem Coaching Ihre Mitarbeiter im Umgang mit Docker schulen oder auch gemeinsam mit Ihnen Ihre Infrastruktur migrieren. Mehr Details finden Sie unter Architektur & Entwicklung. Wir freuen uns über eine Kontaktaufnahme, gerne auch ganz formlos über das unten angezeigte Kontaktformular.

Marko
DevOps Engineer