Virtual Server als Mail und Webserver mit Docker

Aus Laub-Home Wiki

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.

  1. Docker erstellt seine eigenen Regeln und ist komplett gelöst vom den fail2ban und iptables Regeln
  2. 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.
  3. 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:

  1. Externe Firewall vor den Docker Node stellen (sicher gut, wenn man eine hat)
  2. iptables in der Docker Engine abschalten und alle Regeln händisch einrichten (von Docker "not recommended"
  3. 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:

  1. 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 Standard INPUT und die eigene FILTERS Chain. Die Regeln der FILTERS 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.

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

Quellen