EINLEITUNG
Im ersten Teil des Blogposts haben wir über den Einsatz von Docker-Containern in der Entwicklung geschrieben.
In diesem Blogpost wollen wir auf das Absichern von Containern mit Firewall und das Steuern von dockerized Diensten via SystemD eingehen.
SCHRITT 3: DOCKERIZED PHP BUILDS IM JENKINS CI SERVER
Wir haben im vorherigen Abschnitt gesehen, wie man als Entwickler das Docker-Image auf dem lokalen Computer einsetzen kann. Dasselbe Docker-Image lässt sich aber auch für Dockerized PHP Builds im Jenkins CI Serververwenden.
Damit wir mit dem non-root Docker-Container keine Dateisystemsberechtigungsprobleme bekommen, müssen wir einiges tun.
(1) Jenkins erlauben Docker-Container ohne ‘sudo’ zu starten. Das geht indem man eine Gruppe ‘docker’ anlegt und den Benutzer ‘jenkins’ dieser Grupppe zuordnet.
(2) Wir müssen nun Benutzer und Gruppe für ‘dockerworker’ mit fester UID 10777 und GID 10777 anlegen, was (Z7) erfüllt. Unter Ubuntu geht das wie folgt.
github:5ec0ae35f05309b4047fad31f4a670c3
(3) Den Jenkins-User der ‘dockerworker’-Gruppe zuordnen und Jenkins neustarten.
(4) Im Docker-Entrypoint oder dem Run-Skript folgende ‘umask’ einstellen, sodass für erzeugte Dateien auch die richtigen Gruppenberechtigungen gesetzt werden. Die umask Einstellung wird von Kindprozessen geerbt.
github:6d24f3cb4f075c2cce1b292a0c734a51
(5) Im Jenkinsfile zu Anfangs die entsprechenden Berechtigungen setzen.
github:291e97288ddf9938aed00813404516d1
Das einzige, worauf wir also achten müssen sind die Dateisystemberechtigungen und Gruppenzugehörigkeiten. Ansonsten ist der Umgang mit einem non-root Docker-Container nicht weiter kompliziert.
SCHRITT 4: DOCKERIZED STAGING-UMGEBUNG UND HOST-FIREWALL UNTER UBUNTU 16.04
Wir haben im vorherigen Abschnitt gesehen, wie wir das Docker-Image während des Builds einsetzen können. Es ist darüber hinaus möglich dasselbe Docker-Image auch als Staging-Laufzeitumgebung für unsere Applikation zu verwenden.
Einleitend hatten wir bereits mit (Z8) definiert, dass wir das System mit einer Firewall absichern wollen und die PHP Applikation über einen Docker-Container bereitgestellt werden soll. Wir haben mit (Z6) gefordert, dass wir gerne Kernel-Hardening mit Grsecurity und AppArmor einsetzen würden. Das Docker-Image basiert auf Alpine Linux welches bereits einen mit Grsecurity gehärteten Kernel mitbringt. Desweiteren startet der Docker-Daemon Container automatisch mit einem default App Armor Profil. Somit ist hier (Z6) für uns erfüllt, auch wenn man jetzt noch für jeden Docker-Container spezielle AppArmor Profile erstellen könnte, so reicht uns das Default-Profil aus.
Zunächst sollte man wissen, dass unter Linux auf Kernel-Ebene die Firewall-Funktion (Netfilter) implementiert ist und man als Old-School Linux-Nerd das Tool iptables nutzt, um Regeln für die Firewall zu definieren. Da aber jeder weiß, der schonmal mit iptables gearbeitet hat, dass man sich da lieber selbst mit dem Hammer ins Gesicht schlägt, verwenden wir stattdessen Uncomplicated Firewall kurz ufw, was es einem ermöglicht Regeln zu definieren, die man als normaler Mensch versteht, welche dann in iptable-Statements übersetzt werden.
Docker wiederum legt automatisch iptable Regeln an, außer man verbietet es dem Daemon explizt. Deshalb werden wir nun Docker verbieten iptable Regeln anzulegen und die ufw-Firewall einrichten.
Wie beziehen uns auf die Docker Firewall Dokumentation und konfigurieren wie folgt.
(1) Editiere ‘/etc/default/ufw’ und stelle die Forward-Policy auf Accept ein.
github:f2e0cb05336b063bc6b5352b336aa727
(2) Da wir Docker gleich verbieten selbst iptable-Regeln anzulegen, müssen wir eine kleine Konfiguration in ‘/etc/ufw/before.rules’ selbst vornehmen. Dies erlaubt NAT für das spezielle ‘docker0’ Interface.
github:84a12f7823c3faaf887177d09a837ca1
(3) Jetzt erlauben wir noch global das Forwarden von Paketen zwischen Interfaces in ‘/etc/ufw/sysctl.conf’.
github:2b6bc0a92c3e6ca35445483ab4cafff2
(4) Zuletzt verbieten wir dem Docker-Daemon das Anlegen von iptable-Statements und setzen den Google-DNS Server für die Container in ‘/etc/docker/daemon.json’.
github:48e07412388c0b72f6b741a49a850eef
Je nach System kann es auch sein, dass man die Docker Einstellungen in ‘/etc/default/docker’ vornehmen muss. Unter Ubuntu 16.04 zum Zeitpunkt des Verfassens, war aber ‘/etc/docker/daemon.json’ die einzige funktionsfähige Lösung.
Jetzt sind die Vorbereitungen getroffen und wir konfigurieren grundlegend die Firewall mit ufw.
(1) Wir erlauben erstmal nur SSH und aktivieren die Firewall mit ‘default deny’.
github:e1dc71ce085c6b11b5c08f5f10e271cb
(2) Da wir unsere PHP Applikation unter Port 8877 erreichbar machen wollen, öffnen wir den Port in der Firewall mit.
github:128f0a806bd7405ed51c8a79cd73dc8f
Das war es auch schon. Die Firewall ist für alle eingehenden Verbindung geschlossen und aktuell ist nur SSH (Port 22) und die PHP Applikation (Port 8877) erlaubt. Wir sind sehr zufrieden und machen weiter mit der Dockerized PHP Staging-Umgebung gesteuert über Systemd.
Auf Ubuntu 16.04 werden Dienste über SystemD gesteuert. Die Einrichtung erfolgt dabei über service-Dateien, welche das Starten, Stoppen und die Abhängigkeiten zu anderen Diensten definieren. Wir wollen nun einen Named-Docker-Container und eine zugehörige SystemD-Service-Datei erstellen. Ein Named-Docker-Container ermöglicht es einem den Container über den definierten Namen anstatt über die Docker-ID zu referenzieren. Auch überlebt der Container System-Neustarts und kann mittels ‘docker start myapp’ einfach wieder gestartet werden, wenn er gestoppt wurde.
(1) Wir bereiten das App- und Datenverzeichnis der PHP Applikation vor und kopieren die Anwendung hinein.
github:7d260c6d11a0c01975508f089daef9c7
(2) Nun erstellen wir einen Named-Docker-Container für unsere Anwendung und setzen dabei die Environment Variable auf ‘staging’, sodass die PHP Anwendung erkennen kann, in welcher Umgebung sie läuft (Z2).
github:d414cad59fc45bfa81845e6d15c7bc68
(3) Abschließend erstellen wir die SystemD-Service-Datei in ‘/etc/systemd/system/myapp.service’.
github:56cfb8895248a6ae55d64ae42b2c738f
(4) Zuletzt starten wir den Docker-Container via SystemD mittels ‘systemctl’ und enablen ihn, sodass er beim System-Neustart mit gestartet wird.
github:8e8943a5df94fc5561d716349bf145ad
Unser System ist somit mittels Firewall abgesichert. Kernel-Hardening ist durch Grsecurity und AppArmor gewährleistet. Der Container läuft als non-root und exposed nur einen Port. Die Docker-Container sind wie normale Dienste durch SystemD eingebunden und überleben System-Neustarts.
FAZIT UND AUSBLICK
Es wurde gezeigt, wie man ein Docker-Image von der Entwicklung über die Continuous-Integration Build-Chain bis hin zur Staging- oder Live-Umgebung verwenden kann. Durch die Nutzung von Environment-Variablen lässt sich das Paradigma ‘Build-Once-Deploy-Everywhere’ umsetzen. Sicherheit ist durch den Einsatz von Firewall, 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.
Weiterführende Tipps zum eignenen Image-Bau:
- Alpine-Linux kann auch an seine Grenzen stoßen bspw. wenn man das Oracle JDK nutzen will, muss man eine custom-glibc nutzen, da glibc nicht Teil von Alpine Linux ist. Es kann also durchaus sein, dass für gewisse Einsatzzwecke ein Ubuntu-Base-Image ratsamer ist.
- Kann man nicht schon im Dockerfile mittels ‘USER’ auf einen non-root Benutzer umschalten, so ist gosu super, um im Entrypoint auf einen anderen Benutzer zu wechseln und vorher noch Dinge als root auszuführen. Gosu ist sudo vorzuziehen.
Weiterführende Tipps zum Betrieb von Dockerized-Services:
- Der Docker-Daemon selbst kann sich aufhängen, daher ist ein Einsatz von Watchdogs wie Monit und Monitoring sinnvoll.
- Wenn einem die fixe UID und GID nicht gefällt kann man sich auch mit User Namespaces beschäftigen.
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.