Vom blanken Server zum Docker Host

In diesem Artikel soll gezeigt werden, wie man ausgehend von einem Server mit frisch installiertem Debian Stretch einen Docker Host aufsetzt. Es besteht kein Anspruch auf Vollständigkeit in Bezug auf Sicherheit und Wartbarkeit. Ich lasse mich aber gerne eines Besseren belehren und werde den Text bei neuen Erkenntnissen anpassen.

Den Server aufsetzen

Der Ausgangspunkt

Den Anfang macht ein frisch installiertes Debian Stretch. Wahlweise kann auch Ubuntu Server zum Einsatz kommen. Die nötigen Schritte sollten weitestgehend gleich sein.

Es wurde ein Passwort für den root Benutzer vergeben und SSH ist über den Port 22 erreichbar.

Es besteht eine SSH Verbindung zum Server als root.

Locale

Auf einem frischen Debian Stretch ist die locale nicht gesetzt. Dies führt zu Warnungen beim Installieren von Paketen.

$ dpkg-reconfigure locales

de-DE.UTF-8 sollte im folgenden Dialog schon vorausgewählt sein. Nach der Bestätigung mit Enter ist de-DE.UTF-8 erneut als Standard zu wählen. Eine weitere Bestätigung mit Enter startet die Generierung der locale. Anschließend muss das System neu gestartet werden.

$ reboot

Editor

Zu Anfang ist auf dem System kein Editor installiert. Folgende Befehle installieren vim. Es kann aber auch ein beliebiger anderer Editor verwendet werden.

$ apt-get update
$ apt-get install vim

Informationen zur Benutzung von vim sind unter https://www.selflinux.org/selflinux/html/vim.html zu finden.

SSH Zugang

Über das Internet erreichbare SSH Dienste mit Standard Konfiguration werden massiv mit automatisierten Brute Force Angriffen bedacht. Dabei wird üblicherweise versucht, eine Anmeldung mit dem root Benutzer durchzuführen. Eine Konfiguration abseits des Standards ist eine einfache Methode dieses Problem zu lösen.

  • Die Anmeldung als root soll deaktiviert werden.
  • Der SSH Dienst soll nicht über Port 22 erreichbar sein.

Zunächst muss ein neuer Benutzer angelegt werden. Über diesen sollen zukünftige Zugriffe via SSH erfolgen.

$ adduser maxmustermann

maxmustermann ist dabei durch den Namen des neuen Benutzers zu ersetzen.

Es wird ein Dialog geführt, der alle benötigten Daten für den neuen Benutzer erfasst. Dabei ist ein Passwort zu vergeben. Dieses sollte dem üblichen Muster für sichere Passwörter entsprechen (lang und großer Zeichenraum). Ein Passwort-Manager (z.B. KeePass oder 1Password) hilft beim Erstellen und Verwalten.

Sollte das Passwort für root nicht dem beschriebenen Muster entsprechen, kann es folgendermaßen geändert werden:

$ passwd

Nachdem der Benutzer angelegt wurde, kann der SSH Dienst in der zugehörigen Konfigurationsdatei /etc/ssh/sshd_config konfiguriert werden.

$ vim /etc/ssh/sshd_config

Dies öffnet die Datei im vim Editor. Nun müssen zwei Zeilen angepasst werden.

Port 22

Der Port sollte auf einen Wert im Bereich oberhalb von 1024 gesetzt werden.

PermitRootLogin yes

Der Wert muss auf no gesetzt werden.

Abschließend startet man den SSH Dienst neu, um die geänderte Konfiguration zu laden.

$ /etc/init.d/ssh restart

Der SSH Dienst wurde gegen automatisierte Angriffe abgesichert. Neue Anmeldungen erfolgen auf dem neuen Port und dem neuen Benutzer. Nach erfolgter Anmeldung kann mittels su auf den root Benutzer gewechselt werden.

Firewall

Es soll ein einfaches Firewall Script mittels iptables erstellt werden. Dieses soll beim Systemstart geladen werden und erlaubt eingehende Verbindungen ausschließlich für den SSH Dienst.

$ mkdir /opt/firewall
$ cd /opt/firewall
$ touch iptables.sh
$ chmod -x iptables.sh

Dies legt eine leere Datei unter /opt/firewall/iptables.sh an. Folgenden Inhalt soll die Datei bekommen:

#!/bin/bash

IPT="/sbin/iptables"
PUB_IF="eth0"
 
$IPT -F
$IPT -X
$IPT -t nat -F
$IPT -t nat -X
$IPT -t mangle -F
$IPT -t mangle -X
modprobe ip_conntrack
 
#unlimited
$IPT -A INPUT -i lo -j ACCEPT
$IPT -A OUTPUT -o lo -j ACCEPT
 
# DROP all incomming traffic
$IPT -P INPUT DROP
$IPT -P OUTPUT DROP
$IPT -P FORWARD DROP
 
# Block sync
$IPT -A INPUT -i ${PUB_IF} -p tcp ! --syn -m state --state NEW  -m limit --limit 5/m --limit-burst 7 -j LOG --log-level 4 --log-prefix "Drop Sync"
$IPT -A INPUT -i ${PUB_IF} -p tcp ! --syn -m state --state NEW -j DROP
 
# Block Fragments
$IPT -A INPUT -i ${PUB_IF} -f  -m limit --limit 5/m --limit-burst 7 -j LOG --log-level 4 --log-prefix "Fragments Packets"
$IPT -A INPUT -i ${PUB_IF} -f -j DROP
 
# Block bad stuff
$IPT  -A INPUT -i ${PUB_IF} -p tcp --tcp-flags ALL FIN,URG,PSH -j DROP
$IPT  -A INPUT -i ${PUB_IF} -p tcp --tcp-flags ALL ALL -j DROP
 
$IPT  -A INPUT -i ${PUB_IF} -p tcp --tcp-flags ALL NONE -m limit --limit 5/m --limit-burst 7 -j LOG --log-level 4 --log-prefix "NULL Packets"
$IPT  -A INPUT -i ${PUB_IF} -p tcp --tcp-flags ALL NONE -j DROP # NULL packets
 
$IPT  -A INPUT -i ${PUB_IF} -p tcp --tcp-flags SYN,RST SYN,RST -j DROP
 
$IPT  -A INPUT -i ${PUB_IF} -p tcp --tcp-flags SYN,FIN SYN,FIN -m limit --limit 5/m --limit-burst 7 -j LOG --log-level 4 --log-prefix "XMAS Packets"
$IPT  -A INPUT -i ${PUB_IF} -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP #XMAS
 
$IPT  -A INPUT -i ${PUB_IF} -p tcp --tcp-flags FIN,ACK FIN -m limit --limit 5/m --limit-burst 7 -j LOG --log-level 4 --log-prefix "Fin Packets Scan"
$IPT  -A INPUT -i ${PUB_IF} -p tcp --tcp-flags FIN,ACK FIN -j DROP # FIN packet scans
 
$IPT  -A INPUT -i ${PUB_IF} -p tcp --tcp-flags ALL SYN,RST,ACK,FIN,URG -j DROP
 
# Allow full outgoing connection but no incomming stuff
$IPT -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
$IPT -A OUTPUT -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT

# Allow ssh
$IPT -A INPUT -p tcp --destination-port 22 -j ACCEPT

# Allow ICMP
$IPT -A INPUT -p icmp --icmp-type 8 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
$IPT -A OUTPUT -p icmp --icmp-type 0 -m state --state ESTABLISHED,RELATED -j ACCEPT
 
# Do not log smb/windows sharing packets - too much logging
$IPT -A INPUT -p tcp -i ${PUB_IF} --dport 137:139 -j REJECT
$IPT -A INPUT -p udp -i ${PUB_IF} --dport 137:139 -j REJECT
 
# log everything else and drop
$IPT -A INPUT -j LOG
$IPT -A FORWARD -j LOG
$IPT -A INPUT -j DROP

exit 0

Sollte das primäre Netzwerkinterface nicht eth0 sein, muss die Zeile PUB_IF="eth0" entsprechend angepasst werden.
Im Bereich #Allow ssh ist der Port 22 entsprechend des vorherigen Kapitels anzupassen.

Um das Script beim Systemstart automatisch auszuführen, muss die Datei /etc/rc.local angelegt und ausführbar gemacht werden.

$ touch /etc/rc.local
$ chmod +x /etc/rc.local

Der Inhalt soll folgendermaßen aussehen:

#!/bin/sh -e

sh /opt/firewall/iptables.sh

exit 0

Ab dem nächsten Neustart des Servers werden ausschließlich SSH Verbindungen zugelassen. Für den Betrieb von Docker sind keine weiteren Anpassungen notwendig. Der Docker Dienst erstellt automatisch iptables Regeln beim Start eines Containers. Wird der Container gelöscht, werden die zugehörigen iptables Regeln ebenfalls entfernt.

Automatische Updates

Um den Server gegen mögliche Angriffe abzusichern, ist es wichtig, die installierte Software auf aktuellem Stand zu halten. Es ist daher ratsam, in regelmäßigen Abständen Aktualisierungen durchzuführen.

Um die Administration des Servers zu vereinfachen, kann die Installation von Updates automatisiert werden. Folgender Befehl installiert die notwendigen Pakete:

$ apt-get install unattended-upgrades apt-listchanges

Anschließend sollte die Datei /etc/apt/apt.conf.d/50unattended-upgrades bearbeitet werden. Bei zwei Zeilen müssen die Kommentar-Zeichen // entfernt werden.

// Unattended-Upgrade::Mail "root";
// Unattended-Upgrade::Automatic-Reboot-Time "02:00";

Die erste bewirkt, dass Informationen zu erfolgten Installationen an das interne Postfach von root gesendet werden. Die zweite legt den Zeitpunkt für ggf. nötige Neustarts fest.

Es wird nun täglich geprüft, ob Sicherheitsupdates verfügbar sind und diese ggf. automatisch installiert. Weitere Details zur Konfiguration sind unter https://wiki.debian.org/UnattendedUpgrades zu finden.

Docker

Docker kann mittels des Debian Paket Managers apt installiert werden. Das dafür benötigte Repository ist von Haus aus nicht in apt registriert und muss daher zunächst bekanntgegeben werden.

$ apt-get update
$ apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common
$ curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -
$ add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"

Das Repository ist nun in apt registriert und Docker kann installiert werden.

$ apt-get update
$ apt-get install docker-ce

Um den Zugriff auf die Docker Shell Befehle auf root zu beschränken, müssen noch die Ausführungs-Rechte für sonstige Benutzer entfernt werden.

$ chmod o-x /usr/bin/docker*

Abschließend muss noch das automatische Starten von Docker beim Systemstart eingerichtet werden.

$ systemctl enable docker

Docker wurde installiert und startet automatisch beim Serverstart. Weitere Informationen zur Docker Installation sind unter https://docs.docker.com/install/linux/docker-ce/debian/ zu finden.

git (optional)

Eine Installation von git ist für den Betrieb des Docker Host nicht notwendig. Da aber mit steigender Anzahl genutzter Container auch die Menge von Scripts zur Wartung steigt, ist es durchaus sinnvoll diese in einem git Repository zu speichern. Neben der Versionierung bietet sich der Vorteil die Scripts auf einem Desktop System mit „schöneren“ Editoren bearbeiten zu können.

$ apt-get install git-core

Anschließend ist der git Benutzer zu konfigurieren. Name und Email Adresse müssen natürlich entsprechend angepasst werden.

$ git config --global user.name "Max Mustermann"
$ git config --global user.email max@mustermann.com

Zum Schluss kann das git Repository mit den Scripts geklont werden.

$ cd /opt
$ git clone https://url-to-repo/scripts-repo.git docker-scripts

Dies lädt das Repository in das Verzeichnis /opt/docker-scripts. Die URL ist dabei entsprechend anzupassen.

$ cd /opt/docker-scripts
$ git pull

Hierdurch werden die Scripts auf den aktuellen Stand gebracht.

Betrieb und Wartung

Backups der Container Volumes

Angenommen es existiert ein Container mit dem Namen my-app und es ist für ihn ein Volume my-app-config angelegt worden. In dem Volume sind Daten abgelegt, die regelmäßig gesichert und ggf. zurückgesetzt werden sollen. Des weiteren existiert das Volume backup-data, das als Ziel für Backups dient.

Alternativ kann anstatt von backup-data ein Verzeichnis auf dem Host eingebunden werden. Der Parameter im Befehl würde dann dem Muster -v /data/backups:/target folgen.

Folgendes Script packt alle Daten im Volume my-app-config in ein .tar.gz Archiv und verschiebt dieses in das eingebundene /target Verzeichnis:

#!/usr/bin/env bash
docker stop my-app
docker run --rm --name my-app-backupdata -v my-app-config:/source -v backup-data:/target debian:latest /bin/sh -c "cd /source && tar -zcvf my-app-config.backup.tar.gz ./* && mv ./my-app-config.backup.tar.gz /target"
docker start my-app

Das Wiederherstellen der Daten mit vorherigem Löschen des Inhalts von my-app-config wird folgendermaßen durchgeführt:

#!/usr/bin/env bash
docker stop my-app
docker run --rm --name my-app-backupdata -v my-app-config:/target -v backup-data:/source debian:latest /bin/sh -c "rm -r -f /target/* && cp /source/my-app-config.backup.tar.gz /target && cd /target && tar -zxvf ./my-app-config.backup.tar.gz && rm ./my-app-config.backup.tar.gz"
docker start my-app

Das Backup Script kann mittels cron regelmäßig ausgeführt werden.

Container aktualisieren

Zusätzlich zu den Annahmen des vorherigen Abschnitts soll gelten, dass ein Docker Image my-app existiert und das Script createcontainer.sh im selben Verzeichnis liegt. Dieses Script beinhaltet die nötigen Befehle, um den Container aus dem Image mit dem Tag latest zu erstellen.

Das folgende Script bringt den Container auf den neusten Stand. Dabei wird das alte Image unter dem Tag rollback gesichert. Ebenfalls wird der Inhalt des Volume my-app-config gesichert.

#!/usr/bin/env bash
docker rmi my-app:rollback
docker tag my-app:latest my-app:rollback
docker pull my-app:latest
docker stop my-app
docker rm my-app
docker run --rm --name my-app-backupdata -v my-app-config:/source -v backup-data:/target debian:latest /bin/sh -c "cd /source && tar -zcvf my-app-config.rollback.tar.gz ./* && mv ./my-app-config.rollback.tar.gz /target"
sh ./createcontainer.sh
docker start my-app

Sollte die neue Version des Image Probleme bereiten, kann mit folgendem Script ein rollback zum vorherigen Zustand durchgeführt werden. Dabei wird die zuvor gesicherte Version des Image wiederhergestellt und der Inhalt von my-app-config zurückgespielt.

#!/usr/bin/env bash
docker stop my-app
docker rm my-app
docker rmi my-app:latest
docker tag my-app:rollback my-app:latest
docker rmi my-app:rollback
docker run --rm --name my-app-backupdata -v my-app-config:/target -v backup-data:/source debian:latest /bin/sh -c "rm -r -f /target/* && cp /source/my-app-config.rollback.tar.gz /target && cd /target && tar -zxvf ./my-app-config.rollback.tar.gz && rm ./my-app-config.rollback.tar.gz"
sh ./createcontainer.sh
docker start my-app

Zusammenfassung

Es wurde gezeigt, wie ausgehend von einem frisch installierten Debian Stretch ein Docker Host aufgesetzt werden kann. Dieser wurde gegen gängige automatisierte Angriffe abgesichert. Außerdem wurden Scripts zur Sicherung von Daten und zum Updaten von Containern erstellt.