Virtual Server als Mail und Webserver mit Docker
Hier eine Anleitung wie man mit Hilfe von Docker, bzw. Docker Compose einen virtuellen Server im Internet zu einem Mail und Web Server macht. Hierbei werde ich die folgenden Komponenten nutzen:
- mailcow: dockerized - Als Mailserver
- NGINX - als HTTP Proxy
- Wordpress - als Blogging CMS
- Mediawiki - als Dokumentationswerkzeug
- ein paar nützliche Tools
Zusätzlich kommen noch die Folgenden Features dazu:
- Automatisches Backup
- Automatisches oder Manuelles Updaten aller Komponenten
- System bereinigen
- Überwachung
Vorbereitung des Virtuellen Servers
Ich habe mich auf der virtuellen Maschine für ein Debian 10 (Buster) (64Bit) entschieden. Dieses wurde vom Betreiber "minimal" installiert. Also geht es nun für mich beim ersten Login direkt los. Wie man Debian minimal installiert habe ich bereits hier niedergeschrieben:
Ich habe nun als erstes meinen key eingespielt:
ssh-copy-id root@vserver.example.tld
Nun via ssh einloggen und erstmal alle Updates einspielen:
apt update && apt upgrade -y
Ich habe bei mir noch die Locales auf Europe/Berlin geändert.
dpkg-reconfigure locales
und in der Auswahl habe ich mich für "en_US.UTF-8 UTF-8" entschieden. Danach am besten mal einen reboot machen:
reboot
Nun installieren wir ein paar nützliche tools:
apt install curl locate expect wget dnsutils rsync logwatch bash-completion vnstat
Änderung der DNS Server
/etc/resolve.conf
nameserver 1.1.1.1 nameserver 8.8.8.8
swappinness Wert verkleinern, damit das System erst im äußersten Notfall den Swap nutzt, dafür die folgende Datei anlegen:
/etc/sysctl.d/swappiness.conf
vm.swappiness=0
richtige Timezone setzen
dpkg-reconfigure tzdata
In meinem Fall Europe/Berlin
Nun noch NTP einrichten. Da ich mich für den Hauseigenen NTP Dienst von systemd entschieden habe, sollte man ntp
falls installiert erst einmal deinstallieren:
<syntaxhighlight=bash>apt purge ntp</syntaxhighlight>
Dafür konfigurieren wir als erstes die Time Server in der Datei
/etc/systemd/timesyncd.conf
[Time] Servers=0.pool.ntp.org 1.pool.ntp.org 2.pool.ntp.org 3.pool.ntp.org
nun das ganze aktiviere: <syntaxhighlight=bash>timedatectl set-ntp true</syntaxhighlight> und prüfen ob alles OK ist: <syntaxhighlight=bash>timedatectl status</syntaxhighlight> man sollte folgende Ausgabe bekommen. Wichtig ist, das bei "NTP Service" Active steht.
Local time: Mon 2020-01-13 11:05:22 CET Universal time: Mon 2020-01-13 10:05:22 UTC RTC time: n/a Time zone: Europe/Berlin (CET, +0100) System clock synchronized: yes NTP service: active RTC in local TZ: no
nun noch aus Sicherheitsgründen den SSH Port auf einen beliebigen anderen Port umlegen
/etc/ssh/sshd_config
Port 22222 #Port 22
systemctl restart sshd
Nun sollte SSH über den Port 22222 erreichbar sein. Wir sichern diesen später noch mit fail2ban ab.
Installation Docker und Docker Compose
Nachdem das Grundsetup abgeschlossen ist, wird nun Docker und Docker Compose installiert.
Installation von mailcow: dockerized
Nun kommen wir zum Mailserver. Ich habe mich hier für mailcow entschieden, da dieses Projekt alles mitbringt was ich benötige. Mehr informationen bekommt ihr unter https://mailcow.email
Installation von Wordpress
Nun installieren wir die erste Webapplikation bestehend aus einem Wordpress Blog System:
Falls es zu einer Migration eines Wordpress Blogs von einem Anderen Server kommt, hier eine kleine Anleitung wie man am besten Wordpress umzieht.
Installation von Mediawiki
Nun kommen wir zur Mediawiki Webapplikation
Möchte man eine MediaWiki Installation zum testen neuer Features haben, kann man einfach das Compose Projekt dazu nutzen ein zweites MediaWiki hochzufahren, um hier ein wenig zu Spielen und neue Extensions auszuprobieren.
Installation von Portainer
Portainer ist eine schöne Webapplikation um Container grafisch zu verwalten. hier eine kleine Anleitung wie sich Portainer mittels Docker Compose deployt wird und dann vom NGINX Reverse Proxy ebenfalls nach draußen geroutet wird.
Installation NGINX SSL Reverse Proxy
Security und Firewall
Um ein wenig besseren Schutz von außen zu bekommen. Habe ich mich entschieden eine iptables Firewall (IPv4 und IPv6) und den Schutz für SSH mittels fail2ban einzurichten.
Da Docker für jeden Container eigene NAT Rules erstellt und ohne diese nicht richtig funktioniert, muss man ein wenig in die Trickkiste greifen. Folgendes passiert hier nun.
- Docker erstellt seine eigenen Regeln und ist komplett gelöst vom den fail2ban und iptables Regeln
- unser iptables Script greift für alles außerhalb von Docker, heißt auch, wenn ihr einen PORT in Docker mappt, ist dieser automatisch von außen erreichbar.
- fail2ban wird seine Regeln ebenfalls autark einrichten und in meinem Fall hier den SSH Port 22222 schützen, sollte doch ein Angreifer versuchen sich einzuloggen
fail2ban
Als erstes installieren wir fail2ban.
apt install fail2ban
dann richten wir den SSH Schutz ein. Hierfür einfach folgende Datei anlegen:
/etc/fail2ban/jail.d/sshd.conf
[sshd] # To use more aggressive sshd modes set filter parameter "mode" in jail.local: # normal (default), ddos, extra or aggressive (combines all). # See "tests/files/logs/sshd" or "filter.d/sshd.conf" for usage example and details. mode = aggressive port = ssh,22222 logpath = %(sshd_log)s backend = %(sshd_backend)s
hier konfiguriere ich nun, das ich ausführliche E-Mails bekommen möchte, welche standardmäßig an root@localhost geschickt werden. Hier kann man auch einfach eine andere Adresse angeben. Lässt man dies weg, wird der Angreifer nur ausgeschlossen, es kommt zu keiner Benachrichtigung.
/etc/fail2ban/jail.d/action.conf
[DEFAULT] # Destination email address used solely for the interpolations in # jail.{conf,local,d/*} configuration files. destemail = root@localhost # Choose default action. To change, just override value of 'action' with the # interpolation to the chosen action shortcut (e.g. action_mw, action_mwl, etc) in jail.local # globally (section [DEFAULT]) or per specific section action = %(action_mwl)s
und fail2ban Neustarten:
systemctl restart fail2ban
Mehr Informationen zu fail2ban habe ich hier Dokumentiert:
iptables Firewall
Wie hier schön beschrieben, gibt es ein Henne - Ei Problem was das Thema Docker angeht. Docker selbst erstellt alle möglichen iptables und ip6tables regeln, ohne die das gesamte Netzwerkkonstrukt von Docker nicht funktioniert. Es gibt hierfür 3 Lösungen:
- Externe Firewall vor den Docker Node stellen (sicher gut, wenn man eine hat)
- iptables in der Docker Engine abschalten und alle Regeln händisch einrichten (von Docker "not recommended"
- die hier Beschriebene Lösung nutzen, welche ein Zusammenspiel von Docker und Schutz des Host Systems beinhaltet
Als erstes legen wir uns zwei Konfigurationsdateien an, eine für IPv4 und eine für IPv6:
/etc/ip4tables.conf
*filter :INPUT ACCEPT [0:0] :FORWARD DROP [0:0] :OUTPUT ACCEPT [0:0] :FILTERS - [0:0] :DOCKER-USER - [0:0] -F INPUT -F DOCKER-USER -F FILTERS -A INPUT -i lo -j ACCEPT -A INPUT -p icmp --icmp-type any -j ACCEPT -A INPUT -j FILTERS -A DOCKER-USER -i eth0 -j FILTERS # Einzelport Freischaltungen -A FILTERS -m state --state ESTABLISHED,RELATED -j ACCEPT -A FILTERS ! -o eth0 -j ACCEPT -A FILTERS -m state --state NEW -m tcp -p tcp --dport 22222 -j ACCEPT -A FILTERS -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT -A FILTERS -m state --state NEW -m tcp -p tcp --dport 443 -j ACCEPT # Multiport Freischaltungen #-A FILTERS -m state --state NEW -m tcp -p tcp -m multiport --dports 25,465,587,143,993,110,995,4190 -j ACCEPT # Logging Rule -A FILTERS -j LOG --log-prefix "FILTERS -- DENY " --log-level 6 # System wird unsichbar für Angreifer -A FILTERS -j DROP # Macht das System sichtbar für Angreifer #-A FILTERS -j REJECT --reject-with icmp-host-prohibited COMMIT
/etc/ip6tables.conf
*filter :INPUT ACCEPT [0:0] :FORWARD DROP [0:0] :OUTPUT ACCEPT [0:0] :FILTERS - [0:0] :DOCKER-USER - [0:0] -F INPUT -F DOCKER-USER -F FILTERS -A INPUT -i lo -j ACCEPT -A INPUT -p ipv6-icmp --icmpv6-type 1 -j ACCEPT -A INPUT -p ipv6-icmp --icmpv6-type 2 -j ACCEPT -A INPUT -p ipv6-icmp --icmpv6-type 3 -j ACCEPT -A INPUT -p ipv6-icmp --icmpv6-type 4 -j ACCEPT -A INPUT -p ipv6-icmp --icmpv6-type 133 -j ACCEPT -A INPUT -p ipv6-icmp --icmpv6-type 134 -j ACCEPT -A INPUT -p ipv6-icmp --icmpv6-type 135 -j ACCEPT -A INPUT -p ipv6-icmp --icmpv6-type 136 -j ACCEPT -A INPUT -p ipv6-icmp --icmpv6-type 137 -j ACCEPT -A INPUT -p ipv6-icmp --icmpv6-type 141 -j ACCEPT -A INPUT -p ipv6-icmp --icmpv6-type 142 -j ACCEPT -A INPUT -s fe80::/10 -p ipv6-icmp --icmpv6-type 130 -j ACCEPT -A INPUT -s fe80::/10 -p ipv6-icmp --icmpv6-type 131 -j ACCEPT -A INPUT -s fe80::/10 -p ipv6-icmp --icmpv6-type 132 -j ACCEPT -A INPUT -s fe80::/10 -p ipv6-icmp --icmpv6-type 143 -j ACCEPT -A INPUT -p ipv6-icmp --icmpv6-type 148 -j ACCEPT -A INPUT -p ipv6-icmp --icmpv6-type 149 -j ACCEPT -A INPUT -s fe80::/10 -p ipv6-icmp --icmpv6-type 151 -j ACCEPT -A INPUT -s fe80::/10 -p ipv6-icmp --icmpv6-type 152 -j ACCEPT -A INPUT -s fe80::/10 -p ipv6-icmp --icmpv6-type 153 -j ACCEPT -A INPUT -j FILTERS -A DOCKER-USER -i eth0 -j FILTERS # Einzelport Freischaltungen -A FILTERS -m state --state ESTABLISHED,RELATED -j ACCEPT -A FILTERS ! -o eth0 -j ACCEPT -A FILTERS -m state --state NEW -m tcp -p tcp --dport 22222 -j ACCEPT -A FILTERS -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT -A FILTERS -m state --state NEW -m tcp -p tcp --dport 443 -j ACCEPT # Multiport Freischaltungen #-A FILTERS -m state --state NEW -m tcp -p tcp -m multiport --dports 25,465,587,143,993,110,995,4190 -j ACCEPT # Logging Rule -A FILTERS -j LOG --log-prefix "FILTERS -- DENY " --log-level 6 # System wird unsichbar für Angreifer -A FILTERS -j DROP # Macht das System sichtbar für Angreifer #-A FILTERS -j REJECT --reject-with icmp-host-prohibited COMMIT
Was passiert hier:
- die Regeln schreiben sich in die von Docker angelegte Chain
DOCKER-USER
und sorgen damit für ein gleichzeitiges funktionieren der Docker Regeln. Auch nur diese CHAIN wird geflusht, sowie die StandardINPUT
und die eigeneFILTERS
Chain. Die Regeln derFILTERS
Chain besagen einfach:
- Öffnen der TCP Ports 22222, 80, 443 (also unser SSH / HTTP und HTTPS für den Nginx Proxy)
- und dann Loggen wir noch ein wenig
- und verwerfen den Rest (Drop vs Reject, habe mich für DROP entschieden)
So nun der nächste Trick, um das Ganze richtig zu starten muss es mittels der folgenden Befehle aktiviert werden. Bitte das -n
nicht vergessen, das sorgt dafür das die bestehenden Regeln nicht geflusht werden.
iptables-restore -n /etc/ip4tables.conf
ip6tables-restore -n /etc/ip6tables.conf
Nun noch das Ganze reboot fähig mit einem systemd
Script machen:
/etc/systemd/system/iptables.service
[Unit] Description=Restore iptables firewall rules Before=network-pre.target [Service] Type=oneshot ExecStart=/sbin/iptables-restore -n /etc/ip4tables.conf ExecStart=/sbin/ip6tables-restore -n /etc/ip6tables.conf [Install] WantedBy=multi-user.target
und aktivieren:
systemctl enable --now iptables
nun kann man wann immer man eine Änderung am Regelwerk macht mittels systemctl restart iptables.service
die Firewall neu laden.
Updates einspielen
System Updates
Um über neue Debian Pakete informiert zu werden, oder sie gar automatisch einspielen zu lassen, nutzen wir das Tool cron-apt
:
Docker Compose Update
Hier der Link zum Aktualisieren von Docker-Compose:
Docker Container Updates
Wie man manuell die Compose Projekte aktualisiert ist im HowTo jedes einzelnen Compose Projekt hinterlegt.
Will man das Ganze automatisiert machen, hilft einem das Tool watchtower
.
Backup und Restore
Hier führen viele Wege nach Rom, ich habe hier mal ein paar Ideen zusammengetragen. Wichtig ist, egal wie man ein Backup macht, es regelmäßig zu tun und zu prüfen ob alles OK ist. Bitte auch alle Sicherungen nicht Lokal auf dem Rechner lassen sonder auf ein anderes System, eine externe Festplatte oder in die "Cloud" kopieren.
- Docker Backup und Restore - eine kleine Anleitung
- Docker Volume Backup Script
- Docker MySQL and MariaDB Backup Script
- Docker Compose Project Backup Script
- Linux Local System Backup Script
Aufräumen von Docker "Leichen"
Leider ist Docker nicht dafür ausgelegt, hinter einem aufzuräumen, das heißt, es behält alles was man nicht selbst löscht, alte Images, ausgeschaltete Container, verwaiste Netzwerke, Volumes, die nicht mehr zugeordnet sind. Zum Glück gibt es dafür prune
:
## Remove exited containers
docker container prune
## Remove dangling images
docker image prune
## Remove all images
docker image prune -a
## Remove unused volumes
docker volume prune
## Removes all unused stuff not the Volumes (dangling Images, stopped Containers, Networks)
docker system prune
## Removes all unused stuff, Volumes included
docker system prune --volumes
Möchte man einen der Befehle ohne Nachfrage, zum Beispiel täglich via cron-job
ausführen, kann einfach der Schalter -f
für force
hinzufügen.
Eine super ausführliche Anleitung gibts hier:
Systemüberwachung
Für die Systemüberwachung setze ich als erstes auf das kleine altbekannte Tool logwatch
, welches ich bereits weiter oben installiert habe und dieses mir täglich Statusmails über das System liefert. Zusätzlich lasse ich mir von allen wichtigen cronjobs (Backup Jobs) die Ausgabemail von cron
schicken.
Docker selbst bietet einige Möglichkeiten, wie man schauen kann ob alles OK ist. Dies habe ich hier mal zusammengefasst:
Möchte man alles zusammen mit einer Monitoring Suite Monitoren und sich bei Bedarf alarmieren lassen, so macht man dies am besten mit Prometheus inklusive diverser Anbauten. Wie man dies am besten macht findet ihr hier:
Ansonsten läuft alles unter Linux und somit können die Standard Linux Tools verwendet werden, hier mal eine kleine Liste:
df -h
top
free
ps aux
less /var/log/syslog