Docker-Image mit Zimbra

Das Docker-Image mit der Zimbra Collaboration Suite ist nun fertig und sogar schon seit einigen Wochen im Praxis-Einsatz. Zimbra – derzeit in der Version 8.8.8 – bringt die folgenden Features mit in das Projekt:

  • E-Mail
  • Kontakt-Adressbücher (synchronisierbar mit dem Handy über CardDAV)
  • Kalender (synchronisierbar mit dem Handy über CalDAV)
  • Taskverwaltung
  • Online-Dateiablage
  • Anbindung an eine bestehende Owncloud/Nextcloud-Installation zur Realisierung einer noch leistungsfähigeren Dateiablage (für CloudyCube noch geplant)

Bei dem Docker-Image bin ich einen für Docker eher ungewöhnlichen, eigentlich verpönten Weg gegangen. Das Image selbst enthält weder Zimbra noch Dienste, die Zimbra vorraussetzt. Stattdessen sind in dem Image nur Skripte enthalten, die auf einem eingebundenen Docker-Volume ein minimalistisches Ubuntu 16.04 LTS mittels debootstrap installieren und anschließend Zimbra von der offiziellen Website herunterladen und installieren. Der ganze Vorgang ist über Dialoge geführt, so dass manuelle Eingriffe, um das System an die eigenen Anforderungen anzupassen, leicht zu bewerkstelligen sind. Aufgrund der hohen Komplexität der Installation wollte ich von einer automatisierten Konfiguration absehen. Am Ende der Installation befindet sich ein vollständiges Ubuntu 16.04 LTS mit Zimbra und allen dafür benötigten und konfigurierten Diensten auf dem eingebundenen Docker-Volume. Das eigentliche Docker-Image startet dann nur noch die „innere“ Ubuntu-Installation, in der Zimbra lebt. Dieser kleine Kniff erlaubt Zimbra persistente Veränderungen an Systemdiensten sowie die dauerhafte Installation zusätzlicher Softwarepakete oder Zimbra-Erweiterungen (Zimlets). Im Vergleich mit „üblichen“ Docker-Containern ergibt sich die Besonderheit, dass die installierte Software gewartet werden muss. Ein neues Docker-Image aktualisiert nicht automatisch Zimbra und auch nicht die verwendeten Betriebssystemkomponenten! Vom Verhalten her ähnelt dies eher einer virtuellen Maschine oder einem LXD Container, bietet aber dennoch alle Annehmlichkeiten des Docker-Stacks.

Um den Nutzer an diese Aufgabe zu erinnern, schaut apticron regelmäßig nach anstehenden Betriebssystemupdates und benachrichtigt den Administrator bei Bedarf mit einer E-Mail. Eine neue Zimbra-Version muss ebenfalls manuell eingespielt werden. Eine Automatik gibt es auch hier nicht, da nach einem Update auf eine neue Version Änderungen, die von Hand gemacht wurden, wieder nachgepflegt werden müssen.

Sowohl das Webinterface als auch die Mailing-Dienste (SMTP, IMAP, POP) unterstützen – und fordern an einigen Stellen sogar – verschlüsselte Verbindungen. Die dafür benötigten (kostenfreien) X.509 Zertifikate beantragt der ACME-Client certbot automatisch bei der LetsEncrypt CA. Die typischen Probleme mit selbst-signierten Zertifikaten entfallen. Damit dieser Mechanismus funktioniert, muss der Container mit einer IPv4- oder IPv6-Adresse direkt im Internet erreichbar sein und über einen entsprechenden A (IPv4) oder AAAA (IPv6) Eintrag im DNS verfügen. Aufgrund der Knappheit von IPv4-Adressen läuft auf der IPv4-Adresse sehr wahrscheinlich ein Reverse-Proxy, der HTTP-Requests an die eigentlich adressierten Docker-Container weiterleitet. Damit hat der Zimbra-Container keine IPv4-Adresse, unter der er im Internet direkt erreichbar wäre. Viele Provider bieten aber gegen Aufpreis eine weitere IPv4-Adresse an, falls man sich den Luxus leisten möchte. Etwas weniger problematisch geht es mit der IPv6-Adresse, da im Normalfall jeder Container seine eigene Adresse bekommt und damit im Internet direkt erreichbar ist.

Mit der Erreichbarkeit aus dem Internet ergeben sich auch Anforderungen an die Firewall. Der Zimbra-Container kümmert sich darum, indem er nicht öffentlich benötigte Dienste vor Zugriff schützt und häufige Angriffe wie TCP Flooding, Ping of Death und TCP Pakete mit unsinnigen Flags frühzeitig herausfiltert.

Weitere Details zum Image gibt es im Github Repository.

Docker konfigurieren

Nutzung des BTRFS Storage Drivers

Das BTRFS Dateisystem unterstützt blockweise Operationen und Copy-on-Write Snapshots und ist damit eine natürliche Wahl für Docker. Ich folge im Wesentlichen dem offiziellen Guide und beschreibe daher nur die relevanten Schritte.

Da der Server BTRFS als Root-Dateisystem benutzt und ich für Docker auch keine eigene Partition anlegen möchte, kann das im Guide beschriebene Vorgeplänkel mit der Vorbereitung einer solchen Partition entfallen. Es geht direkt mit der Anpassung der Konfiguration des Docker Daemons los. Die Konfiguration befindet sich in der Datei /etc/docker/daemon.json. Zur Festlegung des Storage Drivers auf BTRFS muss die Datei um einen Eintrag für storage-driver erweitert werden:

{
    "storage-driver" : "btrfs"
}

Danach ist der Docker Daemon neu zu starten:

$ systemctl restart docker

Docker sollte BTRFS als Storage Driver melden, wenn man die allgemeinen Informationen abfragt:

$ docker info

...

Storage Driver: btrfs
Build Version: Btrfs v4.4
Library Version: 101

...

Aktivierung von IPv6

Standardmäßig läuft Docker nur mit IPv4, IPv6 muss nachträglich aktiviert werden. Dazu sucht man sich innerhalb des Prefixes, das dem Server zugeteilt ist, ein kleineres Prefix aus, aus dem Docker sich bedienen kann, wenn es IPv6 Adressen für die Container allokiert. Die Größe des Prefixes sollte mindestens /80 sein, um Docker die Generierung von IPv6 Adressen aus Prefix und der (virtuellen) MAC-Adresse des Containers zu ermöglichen. Das ist wichtig, um Problemen mit dem Neighbor Cache in Docker aus dem Wege zu gehen.

Nehmen wir an, der Server hat das Prefix 2001:xxxx:xxxx:xxxx::/64 zugeteilt bekommen, dann wäre 2001:xxxx:xxxx:xxxx:d000::/80 eine mögliche Wahl. In der Konfiguration des Docker Daemons (/etc/docker/daemon.json) erfolgt die Einstellung:

{
  "ipv6" : true,
  "fixed-cidr-v6" : "2001:xxxx:xxxx:xxxx:d000::/80",
  "storage-driver" : "btrfs"
}

Docker aktiviert automatisch Forwarding (sofern noch nicht aktiviert) und fügt eine Route für dieses Subnetz hinzu. Manche Hosts beziehen ihre IPv6-Netzwerkeinstellungen über Router Advertisements. Die Aktivierung von Forwarding führt in diesen Fällen zum Verwerfen der Router Advertisements und damit den Verlust der Konnektivität, wenn das Netzwerkinterface seine Konfiguration über Router Advertisements bezieht. Man kann den Kernel anweisen, auch bei aktiviertem Forwarding Router Advertisements zu akzeptieren, indem man in /etc/sysctl.conf folgende Einstellung setzt/hinzufügt:

net.ipv6.conf.ens3.accept_ra = 2

Bei Netcup ist das nicht nötig, da die IPv6 Konfiguration statisch erfolgt. Wie ich leider feststellen musste, gibt es eher Probleme mit dieser Einstellung, wenn ein Router Advertisement die Netzwerkkonfiguration verstellt. Unter anderem musste ich schmerzlich den Verlust der Default-Route mit entsprechenden Konsequenzen beklagen…

Hat man die Konfiguration geändert, muss man noch die geänderte /etc/sysctl.conf neu laden:

$ sysctl -p /etc/sysctl.conf

Da Container sich mit ihren virtuellen Netzwerkinterfaces am docker0 Bridgedevice befinden, können sie nicht auf Neighbor Solicitation Requests vom ISP reagieren und daher auch keine Neighbor Advertisements senden. Der ISP nimmt damit an, dass angefragte Adressen, die sich in besagtem Subnetz befinden, vom Server nicht genutzt werden. Es werden in Folge dessen auch keine Paket für entsprechende IPv6 Adressen zum Server geroutet. Abhilfe schafft der Neighbor Discovery Protocol Proxy Daemon (ndppd). Er beantwortet Neighbor Solicitations mit entsprechenden Neighbor Advertisements gemäß seiner Konfiguration. Der ndppd lässt sich wie folgt installieren:

$ apt-get install ndppd

Unter /etc/ndppd.conf erwartet er standardmäßig seine Konfiguration.  Die Konfiguration ist denkbar einfach, da ndppd mit sinnvollen Defaulteinstellungen arbeitet, und benötigt nur eine Regel, die ndppd anweist, für Dockers Default-Netzwerk Neighbor Solicitations zu beantworten:

proxy ens3 {
    rule 2001:xxxx:xxxx:xxxx:d000::/80 {
        auto
    }
}

Der Parameter auto in der Regel bewirkt, dass ndppd in die aktuelle Routingtabelle schaut, um herauszufinden, an welches Device es die Neighbor Solicitations weiterleiten muss, um zu prüfen, ob es ein Interface gibt, das auf die betreffende IPv6 Adresse hört. Dabei wird zwar immer das docker0 Device herauskommen, es ist aber flexibler so als wenn man das Device selbst in der Konfiguration festlegt. Später wird es weitere IPv6 Prefixes geben, die von ndppd bedient werden müssen und da ist der Name des Devices nicht mehr fix.

Um ndppd beim Systemstart mit hochzufahren muss es noch aktiviert werden:

$ systemctl enable ndppd

Um ndppd in Betrieb zu nehmen und zu prüfen, ob es erfolgreich gestartet wird noch folgendes:

$ systemctl start ndppd
$ systemctl status ndppd
● ndppd.service - LSB: NDP Proxy Daemon
Loaded: loaded (/etc/init.d/ndppd; bad; vendor preset: enabled)
Active: active (running) since Mon 2018-02-05 17:25:48 CET; 2s ago
Docs: man:systemd-sysv-generator(8)
Process: 2494 ExecStop=/etc/init.d/ndppd stop (code=exited, status=0/SUCCESS)
Process: 2505 ExecStart=/etc/init.d/ndppd start (code=exited, status=0/SUCCESS)
Tasks: 1
Memory: 452.0K
CPU: 20ms
CGroup: /system.slice/ndppd.service
└─2513 /usr/sbin/ndppd -d -p /var/run/ndppd.pid

Feb 05 17:25:48 v22018025410360775 systemd[1]: Starting LSB: NDP Proxy Daemon...
Feb 05 17:25:48 v22018025410360775 ndppd[2505]: (notice) ndppd (NDP Proxy Daemon) version 0.2.4
Feb 05 17:25:48 v22018025410360775 ndppd[2505]: (notice) Using configuration file '/etc/ndppd.conf'
Feb 05 17:25:48 v22018025410360775 systemd[1]: Started LSB: NDP Proxy Daemon.

Um die Funktionsfähigkeit zu prüfen kann man www.google.de anpingen:

$ docker run -it \
  ubuntu:16.04 \
  /bin/bash -c "apt-get -y update && apt-get -y install iputils-ping && ping6 www.google.de"

 

Installation von Docker

Die Installation von Docker CE (Community Edition) erfolgt über das von Docker bereitgestellte Repository. Die Installation folgt im wesentlichen dem offiziellen Guide.

Um Docker auf das System zu bekommen sind folgende Schritte notwendig:

Paketindex aktualisieren
$ apt-get update
Vorraussetzungen installieren

Die Pakete werden über HTTPS aus dem Repository heruntergeladen. Dazu sind einige weitere Pakete notwendig:

$ apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common
Dockers GPG Key installieren

Die Pakete im Docker Repository sind zur Sicherung der Integrität mit einer GPG Signatur versehen. Der GPG Key zur Prüfung der Signatur kann wie folgt installiert werden:

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
OK

Der Key sollte nun installiert sein. Der Key hat den Fingerprint 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88. Eine Suche nach dem gekürzten Fingerprint (0EBFCD88) kann man mit apt-key durchführen. Die Ausgabe sollte in etwa so aussehen:

$ apt-key fingerprint 0EBFCD88

pub 4096R/0EBFCD88 2017-02-22
    Key fingerprint = 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88
uid Docker Release (CE deb) <docker@docker.com>
sub 4096R/F273FCD8 2017-02-22

Jetzt muss man noch das Repository dem System hinzufügen:

$ add-apt-repository \
    "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
    $(lsb_release -cs) \
    stable"

Das System sollte an diesem Punkt Zugriff auf das Docker Repository haben. Eine Aktualisierung des Paket-Indexes gefolgt von der Installation des Docker-Pakets (inkl. docker-compose) bringt Docker auf den Server:

$ apt-get update
$ apt-get install docker-ce
$ curl -L https://github.com/docker/compose/releases/download/1.19.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
$ chmod +x /usr/local/bin/docker-compose

Wenn alles gut gegangen ist, ist Docker jetzt lauffähig. Mit dem hello-world Container kann man dies kurz testen:

$ docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 (amd64)
 3. The Docker daemon created a new container from that image which runs the
 executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
 to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://cloud.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/engine/userguide/

Firewall konfigurieren

Auf dem Image ist standardmäßig die für Ubuntu typische Uncomplicated Firewall (UFW) installiert. Im Zusammenspiel mit Docker ist sie allerdings alles andere als unkompliziert. Ich ersetze sie daher durch iptables. Die Handhabung ist deutlich geradliniger und komplexere Regeln lassen sich – meiner Meinung nach – damit auch leichter realisieren.

apt-get purge ufw
rm -R /etc/ufw
apt-get install iptables iptables-persistent

Das Paket iptables-persistent sorgt dafür, dass eingestellte Firewall-Regeln beim Systemstart wieder geladen werden. Die Regeln bezieht es aus den Dateien /etc/iptables/rules.v4 (für IPv4) und /etc/iptables/rules.v6 (für IPv6). Nach Änderungen an der Firewall zur Laufzeit kann man den aktuellen Zustand abspeichern, so dass er beim Systemstart wieder hergestellt wird:

iptables-save > /etc/iptables/rules.v4
ip6tables-save > /etc/iptables/rules.v6

 

Absicherung des SSH-Zugangs

User „root“ deaktivieren

Standardmäßig ist es dem privilegierten Benutzer root erlaubt, sich über SSH anzumelden. Da dies die Standardeinstellung ist, liegt es nahe, dass Bruteforce-Angriffe auf den SSH-Zugang auch gerne mit diesem Benutzernamen durchgeführt werden. Ein Blick in das Server-Log (bei Ubuntu ist dies /var/log/auth.log) zeigt, dass fast alle fehlgeschlagenen Logins auf root zielen. Es liegt also nahe, einen nicht privilegierten Benutzer einzurichten und dem Benutzer root die Möglichkeit zur Anmeldung zu entziehen.

Um einem Benutzer die Möglichkeit zu geben, administrative Aufgaben, die sonst root vorbehalten sind, durchzuführen, fügt man ihn der Gruppe sudo hinzu. Das sudo Kommando ist bei Ubuntu 16.04 LTS schon so eingestellt, dass es allen Benutzern dieser Gruppe die Ausführung aller Befehle mit root Rechten erlaubt. Warum die Nutzung von sudo sinnvoll ist, behandeln andere Sites ausgiebig. Allerdings ist es dazu notwendig, die von den betreffenden Benutzern ausführbaren Befehle feingranularer zu definieren. Da hier allerdings der root Benutzer „ersetzt“ werden soll, ist ein weiteres Einschränken der Rechte nicht sinnvoll. Generell kann man sudo aber zur Erhöhung der Sicherheit einsetzen, wenn man Benutzern nur die Kommandos erlaubt, die sie zur Ausübung ihrer Tätigkeit brauchen (principle of least privilege).

Einen neuen Benutzer <user> samt Home-Verzeichnis, der bash als Shell legt man einfach wie folgt an und fügt ihn der sudo Gruppe hinzu:

$ useradd -m -s /bin/bash -p <password> <user>
$ usermod -aG sudo <user>

Die Einrichtung des Benutzers ist nun abgeschlossen und ein Probe-Login über SSH empfehlenswert. Kann man sich erfolgreich mit dem angelegten Benutzer einloggen, testet folgendes, ob sudo funktioniert:

$ sudo whoami
root

Jetzt kann das Login für den root Benutzer deaktiviert werden. Dazu ändert man in /etc/ssh/sshd_config folgende Zeile:

PermitRootLogin no

Nach dem Neustart von sshd ist die Änderung aktiv:

$ systemctl restart sshd

Rate Limiting

Die Absicherung des SSH-Zugangs ist eine reine iptables Lösung. Pro IP ist nur eine SSH-Verbindung pro Minute erlaubt, in der bis zu drei Login-Versuche getätigt werden können. Um hartnäckiges Ausprobieren von Passwörtern im Minutentakt zu unterbinden, sind pro Tag nicht mehr als 30 SSH-Verbindungen erlaubt. Die Inspiration dazu lieferte Heise mit einem Artikel zum Thema.

Zuerst muss man sshd so einstellen, dass es nach drei Login-Versuchen die Verbindung trennt. Das geschieht in der Konfigurationdatei /etc/ssh/sshd_config durch Hinzufügen/Ändern des MaxAuthTries Parameters:

MaxAuthTries 3

Die restlichen Anforderungen lassen sich mit iptables wie folgt lösen. Das Netzwerkinterface auf dem Server ens3 ist, bitte bei Bedarf anpassen:

  # IPv4
$ iptables -A INPUT -p tcp --dport 22 -i ens3 -m state --state NEW         -m recent --name v4-ssh-min --set
$ iptables -A INPUT -p tcp --dport 22 -i ens3 -m state --state NEW         -m recent --name v4-ssh-day --set
$ iptables -A INPUT -p tcp --dport 22 -i ens3 -m state --state ESTABLISHED -m recent --name v4-ssh-min --update --seconds 60    --hitcount 2  -j REJECT --reject-with tcp-reset
$ iptables -A INPUT -p tcp --dport 22 -i ens3 -m state --state ESTABLISHED -m recent --name v4-ssh-day --update --seconds 86400 --hitcount 31 -j REJECT --reject-with tcp-reset

  # IPv6
$ ip6tables -A INPUT -p tcp --dport 22 -i ens3 -m state --state NEW         -m recent --name v6-ssh-min --set
$ ip6tables -A INPUT -p tcp --dport 22 -i ens3 -m state --state NEW         -m recent --name v6-ssh-day --set
$ ip6tables -A INPUT -p tcp --dport 22 -i ens3 -m state --state ESTABLISHED -m recent --name v6-ssh-min --update --seconds 60    --hitcount 2 -j REJECT --reject-with tcp-reset
$ ip6tables -A INPUT -p tcp --dport 22 -i ens3 -m state --state ESTABLISHED -m recent --name v6-ssh-day --update --seconds 86400 --hitcount 31 -j REJECT --reject-with tcp-reset

Die ersten beiden Regeln schreiben mit der Option –set die IP-Adresse einer neuen Verbindung auf Port 22 nebst Zeitstempel in die Proc-Dateien /proc/net/xt_recent/[v4|v6]-ssh-[min|day]. Die dritte und die vierte Regel fügt mit –update einen weiteren Zeitstempel ein und prüft, ob zwei Zeitstempel innerhalb der letzten 60 Sekunden liegen bzw. innerhalb der letzten 86400 Sekunden (= 1 Tag). Ist dies der Fall, wird die Verbindung zurückgesetzt. Erst nach Ablauf einer Minute ist wieder eine Verbindung zum SSH-Server und drei weitere Login-Versuche möglich. Die vierte Regel limitiert analog dazu die Anzahl der Verbindungen auf 30 pro Tag.

Die Kombination von NEW und ESTABLISHED verringert die Gefahr von Spoofing-Angriffen, bei denen ein Spaßvogel mit gefälschten IP-Absender-Adresse die Recent-Liste füllt und somit unter Umständen legitime Adressen darin landen und die Firewall sie blockiert. Anzumerken ist allerdings auch, dass bestehende Verbindungen getrennt werden, wenn man den Schutz auslöst. Das lässt sich leider auch nicht so leicht lösen, da der ESTABLISHED Status einer Verbindung Teil des Schutzmechanismus‘ ist.

Um Herauszufinden, was das recent Modul „weiß“, gibt  es einige einfache Möglichkeiten zur Abfrage des Status. Welche Adresse die Firewall wie oft gesehen hat, lässt sich mit …

$ cat /proc/net/xt_recent/[v4|v6]-ssh-[min|day]

… abfragen. Die gesamte Liste lässt sich mit…

$ echo clear > proc/net/xt_recent/[v4|v6]-ssh-[min|day]

löschen. Einzelne Adressen kann man wie folgt löschen vor (bitte das Minus vor der IP-Adresse beachten):

$ echo -IPv4-Adresse > proc/net/xt_recent/v4-ssh-[min|day]
$ echo -IPv6-Adresse > proc/net/xt_recent/v6-ssh-[min|day]

 

IPv6 einrichten

Auf dem Server ist standardmäßig nur IPv4 aktiviert, IPv6 muss nachträglich konfiguriert werden. Dem Netcup SCP entnimmt man das IPv6 Prefix, dass dem Server zugewiesen ist. Das Prefix ist leider nur /64, wie das bei den meisten ISPs der Fall ist. Eine Vergabe von /64 Prefixes an über IPSec angebundene VPN-Clients fällt also aus, was die schönen IPv6 Features wie z. B. die zustandslose Autokonfiguration (SLAAC) und die Privacy Extensions zunichte macht und mehr Aufwand bedeutet, um die Neighbor Discovery zum Laufen zu bekommen. Das ist machbar, aber nicht das, was ich als optimale Lösung bezeichnen würde.

Zurück zur Konfiguration… innerhalb des zugeteilten /64 Prefixes sucht man sich eine IPv6 Adresse aus, die der Server bekommen soll. Ist das IPv6 Prefix zum Beispiel 2a03:xxxx:xxxx:xxxx::/64, so wäre für den Server der Einfachkeit halber die erste Adresse, also 2a03:xxxx:xxxx:xxxx::1, eine gute Wahl.

Auf dem Server weist man die gewählte Adresse nun dem Netzwerkinterface – in meinem Fall ens3 – zu. Um Pakete mit unbekanntem Ziel in das Internet zu routen, fügt man noch eine Default-Route hinzu. Es kann sein, dass das Hinzufügen der Default-Route fehlschlägt, wenn der ISP schon im Vorgriff IPv6 aktiviert, aber dem Server noch keine konkrete Adresse zugewiesen wurde. Dann ist u. U. die Default-Route bereits gesetzt. Das ist bei Netcup der Fall, wie ich feststellen musste…

$ ip -6 addr add 2a03:xxxx:xxxx:xxxx::1/64 dev ens3
$ ip -6 route add default via fe80::1 dev ens3

Ein Ping über IPv6 zu Google zeigt, ob die Konfiguration erfolgreich war:

$ ping6 www.google.de

PING www.google.de(ams15s21-in-x03.1e100.net) 56 data bytes
64 bytes from ams15s21-in-x03.1e100.net: icmp_seq=1 ttl=57 time=10.1 ms
64 bytes from ams15s21-in-x03.1e100.net: icmp_seq=2 ttl=57 time=10.1 ms
64 bytes from ams15s21-in-x03.1e100.net: icmp_seq=3 ttl=57 time=10.1 ms

Funktioniert der Ping, kann die IPv6 Konfiguration persistent gemacht werden. Dazu editiert man /etc/network/interfaces und fügt die nötigen Zeilen ein:


# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto ens3
iface ens3 inet dhcp
iface ens3 inet6 static
           address 2a03:xxxx:xxxx:xxxx::1
           netmask 64
           gateway fe80::1

System aktualisieren

Wie bei vorgefertigten Images üblich, sind die mit dem Image ausgelieferten Pakete nicht mehr auf dem neuesten Stand und müssen aktualisiert werden.

Zuerst den Paket-Index aktualisieren:

apt-get update

Danach die bereits installierten Pakete auf den neuesten Stand bringen.

apt-get dist-upgrade

Ubuntu 16.04 LTS wird von Hause aus mit Kernel 4.4 ausgeliefert. Folgendes bringt den neuesten offiziellen HWE-Stack (aktuell Kernel 4.13.0) auf das System:

apt-get install --install-recommends linux-generic-hwe-16.04

Root-Dateisystem auf BTRFS umstellen

Da für das Projekt alle Komponenten und Dienste in Docker-Containern laufen sollen und Docker seit einiger Zeit die Overlay-Fähigkeiten von BTRFS nutzen kann, ist es einen Versuch wert, auf dem Server (Docker Host) direkt BTRFS einzusetzen, um den Overhead für das Layering so gering wie möglich zu halten.

Nach der Installation des minimalen Ubuntu-Images gibt es auf der SSD vier Partitionen, wobei /dev/sda3 das Betriebssystem enthält und /dev/sda4 eine unformattierte Partition mit dem restlichen Speicherplatz darstellt. Das installierte Image bringt leider immer das Betriebssystem auf einer EXT4 Partition mit. Das gilt es nun auf BTRFS umzustellen. Die folgenden Schritte beschreiben das Vorgehen:

Die GParted-DVD booten

Im Netcup SCP befindet sich im Fundus der DVD-Images eine GParted-DVD, einem komfortablen Tool zur Veränderung der Partitionierung von HDDs und SSDs. Dieses Image lädt man in das virtuelle DVD-Laufwerk und startet den Server neu. Es ist ein erzwungener Neustart erforderlich, damit die KVM die DVD auch wirklich bootet.

Die Arbeit mit GParted kommt ein wenig später. Die Vorarbeiten erfolgen im Konsolenfenster, das sich über das Kontextmenü auf dem Desktop öffnen lässt.

Das Root-Dateisystem auf BTRFS umstellen

Zuerst muss das Root-Dateisystem gemountet werden:

mount /dev/sda3 /mnt

Mit dem Ubuntu-Image wurde auch noch eine größere unformattierte Partition angelegt. Diese formattiert man mit ext4 und mountet sie. Diese Partition dient als Puffer für den Umzug des Root-Dateisystems:

mkfs.ext4 /dev/sda4
mkdir /mnt2
mount /dev/sda4 /mnt2

Jetzt das Root-Dateisystem dorthin sichern:

rsync -avxHAX --progress /mnt/. /mnt2/

Die Installation liegt nun trocken auf der großen Partition /dev/sda4. Wir können nun /dev/sda3 aushängen, mit BTRFS formattieren und wieder einhängen. Ich verzichte dabei bewusst auf die Duplizierung von Metadaten, da dies bei SSDs aufgrund der von SSDs selbst durchgeführten Deduplizierung von Daten nicht empfohlen wird und sogar zu negativen Effekten führen kann.

umount /mnt
mkfs.btrfs -f -m single /dev/sda3
mount /dev/sda3 /mnt

Danach das Root-Dateisystem wieder herstellen:

rsync -avxHAX --progress /mnt2/. /mnt/

An diesem Punkt befindet sich das Root-Dateisystem schonmal auf BTRFS, nur passt die Partitionsgröße noch nicht.

Umräumen mit GParted

Mit GParted lässt sich auf einfache Weise /dev/sda4 löschen und /dev/sda3 auf das maximal mögliche Maß vergrößern.

Bootloader reparieren

Mit der geänderten Partionstabelle kommt der Bootloader nicht mehr zurecht. Der Bootloader GRUB muss neu installiert werden. Dazu folgt man am besten den Anweisungen, die Ubuntu für diese Fälle anbietet. In Kürze sind das:

mount /dev/sda3 /mnt
mount -o bind /dev /mnt/dev
mount -o bind /sys /mnt/sys
mount -o bind /proc /mnt/proc
chroot /mnt /bin/bash
nano /etc/fstab          # siehe unten
grub-install /dev/sda
update-grub /dev/sda

Aufgrund der Formattierung von /dev/sda3 hat sich die UUID geändert. Die UUID der Partition verrät das Tool blkid. Diese UUID muss noch in /etc/fstab anstelle der alten UUID eingetragen werden. Ferner muss bei den Mountoptionen ssd hinzugefügt und errors=remount-ro entfernt werden.

Abschluss

Fast fertig:  Im Netcup SCP die Bootsequenz des Servers zurückstellen und den Server neu starten. Wenn alles gut gegangen ist, läuft Ubuntu danach vollständig auf BTRFS.

Umzug des Servers

Zeitpunkt des Umzugs

Bei den Langzeittests des VPN-Containers ist mir leider aufgefallen, dass es immer wieder Probleme mit dem Internetzugriff über den IPSec-Gateway gibt. Pakete, die zu TCP-Verbindungen gehören und vom Internet zurück zum VPN-Client fließen, werden häufig von conntrack als invalid klassifiziert und von meinen iptables Regeln verworfen. Beim Durchforsten von verschiedenen Blogs und Foren drängte sich mir bei den Symptomen der Verdacht auf, dass mein ISP bei seiner DDoS Protection möglicherweise Pakete modifiziert, so dass sie von conntrack nicht mehr zugeordnet werden können. Ich ziehe den Server also früher als eigentlich geplant zu meinem neuen Anbieter (Netcup) um und hoffe, dass sich die Probleme damit lösen lassen.

Neuer Anbieter, der Erstkontakt

Bei Netcup habe ich mich für den Root Server RS 4000 G7 SE entschieden. Er bietet acht dedizierte Xeon E5-2680V4 Kerne sowie 24GB DDR4 RAM und 120GB SSD. Damit ist der Server mehr als großzügig dimensioniert für das Projekt. Den größten Ressourcenhunger entwickelt Zimbra mit etwa 8GB RAM. Die restlichen 16GB sind für die zusätzlichen Dienste wie NextCloud für die Dateiablage unter Zimbra, einige WordPress-Blogs und den VPN-Server ausreichend.

Der Server wurde innerhalb von weniger als einer halben Stunde von Netcup bereitgestellt. Die Standardinstallation des Servers ist sicherlich für viele Kunden geeignet und bringt auch eine hübsche Oberfläche (Froxlor) zur Serveradministration mit. Mir ist es allerdings lieber, wenn ich mich selbst um die Konfiguration der Dienste kümmern kann. Ich habe mir also im Server Control Panel (SCP) ein Ubuntu 16.04LTS Image ausgesucht und auf dem Server installiert.

Nach dem ersten Boot wurde ich von einem deutschsprachigen Ubuntu begrüßt, was mich zugegebenermaßen irritierte, da ich bisher immer mit englischsprachigen Betriebssystemen und Tools gearbeitet habe. Die Umstellung der Sprache auf Englisch ließ sich aber erfreulich flink bewerkstelligen:

apt-get remove --purge language-pack-de language-pack-de-base -y
echo "en_US.UTF-8" > /etc/default/locale
dpkg-reconfigure locales

Leistungstest des neuen Servers

Nachdem ich mir schon bei anderen Anbietern die sprichwörtliche blutige Nase geholt habe, was die Leistungsfähigkeit der angebotenen Server angeht, habe ich Erfahrungsberichte anderer Netcup-Kunden gesucht und auch eigene Tests angestellt. Zusammengefasst kann man sagen: Top-Leistung zum erschwinglichen Preis.

CPU-Leistung

Auf die Messung der CPU-Leistung habe ich verzichtet, da mir die Vergleichsmessungen fehlen und zudem Netcup acht dedizierte Xeon E5-2680V4 CPU-Kerne zusichert.

RAM-Leistung

Die Leistung des RAMs habe ich mehrmals mittels sysbench gemessen. Sysbench schreibt 100GB Daten in das RAM und misst die Zeit, die es dafür braucht. Im Mittel kamen dabei ca. 8,5 GB/s an Durchsatz raus. Eine beachtliche Leistung…

sysbench --num-threads=1 --test=memory --memory-block-size=1M --memory-total-size=100G run

SSD-Leistung

Die Leistung der SSD habe ich ebenfalls mittels sysbench gemessen. Es arbeitet dabei mit einem Satz von 1024 Dateien mit je 10MB, d.h. insgesamt 10GB. Diese Dateien werden zufällig ausgelesen und überschrieben im Verhältnis 1,5 zu 1. Daraus wird dann eine mittlere Lese-/Schreibgeschwindigkeit berechnet.

sysbench --test=fileio --file-total-size=10G --file-num=1024 prepare
ulimit -n 200000
sysbench --num-threads=4 --test=fileio --file-total-size=10G --file-num=1024 --file-test-mode=rndrw --max-time=300 --max-requests=0 --file-extra-flags=direct --file-fsync-freq=1 run

Die gemessene mittlere Lese-/Schreibgeschwindigkeit beträgt 2,8 MB/s. Das sieht auf den ersten Blick nicht so toll aus, ist aber ok, da das 179 Requests / s bei 16 kB großen Blöcken sind, die beim Schreiben jedes Blocks bis auf die SSD durch müssen.

Nun wäre noch die Leistung beim sequentiellen Zugriff interessant. Dazu schreibt man eine 10 GB große Datei auf die SSD und liest diese anschließend wieder aus. Im Gegensatz zum vorherigen Test wird so Performance für Lese- / Schreibvorgänge von großen Dateien gemessen.

dd if=/dev/zero of=./test.file bs=1M count=10000 oflag=direct
dd if=./test.file of=/dev/null bs=1M count=10000

Im Mittel schafft es die SSD auf eine Geschwindigkeit von 1,35 GB/s beim sequentiellen Lesen und 381 MB/s beim sequentiellen Schreiben.

Fazit

Der Server ist für das geplante Projekt mehr als ausreichend und um ein Vielfaches leistungsfähiger als die Konkurrenz, bei der ich bisher war.

Docker-Image mit StrongSwan

Das Docker-Image mit StrongSwan ist nun funktionsfähig. Damit besteht mittels eines IKEv2-fähigen IPSec-Clients die Möglichkeit, einen VPN-Tunnel direkt in den Docker-Stack hinein zu öffnen und gesichert auf interne Administrationsdienste zuzugreifen. Diese Isolation reduziert stark die Angriffsvektoren, die Hacker nutzen können, um in das System einzudringen, da nur absolut für den öffentlichen Betrieb notwendige Dienste öffentlich zugänglich sind.

Obwohl mit StrongSwan viele IPSec-Spielarten implementierbar sind, habe ich mich doch auf IKEv2 beschränkt, da dies als State-of-the-Art gilt und viele Probleme adressiert, die mit anderen Verfahren schon Kopfschmerzen verursacht haben. Die Ausschlag gebenden Argumente für IKEv2 waren die verbesserte Fähigkeit, mit Clients hinter NAT-Firewalls (NAT-Traversal) und mit mobilen Clients umzugehen (MOBIKE). In der heutigen Welt der mobilen Geräte halte ich insbesondere den letzten Punkt für ein deutliches Plus.

Authentifizierung: Server => Client

Die Authentifizierung des VPN-Servers ggü. dem Client erfolgt mittels Zertifikat. In der aktuellen Ausbaustufe erzeugt der VPN-Server selbst intern eine Stammzertifizierungsstelle (Root Certificate Authority, Root CA) und lässt diese dann ein passendes Server-Zertifikat (RSA, 4096 bit) für StrongSwan erzeugen. Der Container prüft bei jedem Start, ob das Server-Zertifikat noch zu den Einstellungen bezüglich Hostnamen passt und generiert es ggf. neu.

Um Clients die Prüfung dieses Zertifikats zu ermöglichen, muss das Zertifikat der Stammzertifizierungsstelle in den Zertifikatsspeicher des Clients importiert werden. Danach kann der VPN-Client sicherstellen, dass er bei einem Verbindungsaufbau auch wirklich mit dem gewünschten VPN-Server verbunden ist. Die Verwendung von solchen selbst signierten Zertifikaten (Self-Signed Certificates) ist für den Testbetrieb sowie einfache Szenarien mit wenigen Clients im privaten Umfeld meistens ausreichend.

Man sollte sich allerdings im Klaren darüber sein, dass das Importieren eines Root-CA-Zertifikats in einem Client dazu führt, dass dieser dann alle von der Root-CA ausgestellten Zertifikate als gültig ansieht. Sollte einmal der VPN-Server kompromittiert und der private Schlüssel der Root-CA entwendet werden, so könnte ein Angreifer Zertifikate erzeugen, die von den Clients für gültig gehalten werden und damit Sicherheit (Authentizität und Integrität) vorgaukeln. Daher noch einmal der Hinweis: Die Verwendung der internen CA ist nur für Testzwecke und bestenfalls für den privaten Gebrauch zu empfehlen. Für einen gewissen Kompromiss zwischen Komfort und Sicherheit kann man den privaten Schlüssel der Root-CA nach deren Initialisierung aus dem Container entfernen. Solange weder Server- noch Client-Zertifikate (siehe unten) erzeugt werden müssen, wird dieser Schlüssel nicht benötigt.

Für Umgebungen, in denen man aus o.g. Gründen das Ausrollen von eigenen Root-CA-Zertifikaten vermeiden möchte, werde ich daher demnächst das Image so erweitern, dass man von außen auch eigene Zertifikate zuführen kann.

Authentifizierung: Client => Server

Bei der Authentifizierung von Clients ggü. dem Server habe ich mich ebenfalls für einen zertifikatsbasierten Ansatz entschieden, da Zertifikate im allgemeinen sicherer sind als selbstgewählte Passwörter. Außerdem kann man später noch SmartCards hinzunehmen, um den privaten Schlüssel sicher zu speichern. Der VPN-Server unterstützt die Authentifizierung von Clients mittels IKEv2 und EAP-TLS. Damit sollten alle IPSec-VPN-Clients, die Authentifizierung mittels Zertifikaten unterstützen, in der Lage sein, sich beim VPN-Server anzumelden.

Die Erzeugung der Client-Zertifikate übernimmt die interne CA des VPN-Servers. Das Image bietet hierzu einige Befehle zum Hinzufügen, Deaktivieren, Aktivieren und Löschen von Clients an (Details dazu gibt es hier)

Verschlüsselung

Die Auswahl der angebotenen Verschlüsselungs- und Hashalgorithmen steht noch aus. Derzeit werden StrongSwans Default-Einstellungen verwendet.

Zugriff auf andere Docker-Container

Der StrongSwan-Container enthält einen BIND9 DNS-Server, der wahlweise DNS-Requests an Dockers Embedded-DNS-Dienst oder frei wählbare DNS-Server weiterleitet. Dieser DNS-Server wird den VPN-Clients als zu verwendender DNS-Server im Rahmen des IKEv2 Handshakes mitgeteilt. Auf diese Weise sind alle Container erreichbar, die sich mit dem StrongSwan-Container in einem User Defined Network befinden. Der StrongSwan-Container kann sich zeitgleich in mehreren User Defined Networks befinden. Die Namensauflösung (und auch die Kommunikation mit anderen Containern) funktioniert nicht, wenn sich der StrongSwan-Container an Dockers Default-Brücke befindet.

Zugriff auf das Internet

VPN-Clients können auch auf das Internet zugreifen. Dabei unterstützt der VPN-Server für IPv4 Masquerading und für IPv6 Masquerading sowie die Vergabe von global eindeutigen Adressen (GUA, Global Unicast Address) an die VPN-Clients. Masquerading verschleiert die Identität des VPN-Clients, da dessen ursprüngliche IP-Adresse durch die IP-Adresse des VPN-Servers ersetzt wird. Bei der Verwendung von GUAs ist der Vorteil, dass auf dem Weg vom VPN-Client zu einem Host im Internet keine Adressumsetzung im Weg ist, die bei manchen Protokollen Probleme verursacht. Ein Zugriff aus dem Internet auf VPN-Clients ist bei Masquerading nicht möglich und wird bei GUAs – mit Ausnahme von einigen wichtigen ICMP-Paketen – unterbunden.

Kommunikation zwischen VPN-Clients

Die Kommunikation zwischen VPN-Clients kann optional freigeschaltet werden.

Multicast-Support

Der VPN-Server unterstützt kein Multicasting. Es gibt StrongSwan-Module, die Multicast-Support bieten – allerdings nur für IPv4 und nicht für IPv6. Um Irritationen zu vermeiden verzichte ich daher ganz auf Multicast-Support. Vielleicht gibt es irgendwann ja auch noch ein Multicast-Modul, das IPv6 beherrscht…