Umzug des ReverseProxy

Der Grund

Als ich diesen Artikel begonnen habe, lief der ReverseProxy als Jail auf einem FreeBSD-Host. Leider stößt der Rechner mittlerweile öfters an seine Grenzen, da ich auf Sicherheit setze und MariaDB, WordPress, Nextcloud, Mailserver, Webmailer und GitHub in eigene Jails gesteckt habe. Damit hat auch jedes Jail seine eigene php-Engine und eigenen Webserver. Also Leistungsfresser pur.

Ich will also den Host entlasten. Mal abgesehen von der MariaDB, die mit Abstand den größten RAM-Fingerprint hat, gibt es da noch den ReverseProxy, der mit Abstand den größten Rechen- und Bandbreitenbedarf hat.

Also soll der ReverseProxy vom FreeBSD-System (XigmaNAS) auf ein eigens dafür eingerichtetet Debian-basiertes System (Raspberry Pi 4) umziehen.

Mit diesem Artikel möchte ich euch die Möglichkeit geben, mich bei dem Umzug zu begleiten, falls ihr ähnliches vorhaben solltet.

Der Umfang

Um wirklich eine spürbare Entlastung zu haben, muss auch Fail2Ban zukünftig direkt auf dem ReverseProxy laufen. Sonst würde der Proxy die Anfragen einfach an die Jails weiterleiten und dem FreeBSD-Host wäre nicht geholfen, da er weiter so viele Anfragen abarbeiten müsste.

Also müssen umziehen:

  • nginx
  • CertBot (damit nginx immer die richtigen und gültigen Zertifikate ausliefern kann)
  • Fail2Ban

Ebenso müssen die nachgelagerten Server ihre Logdateien an den ReverseProxy senden, sonst bekommt dessen Fail2Ban ja nichts von Angriffen auf die Server mit. Das erledige ich mit syslog-ng(8).

Klingt trivial, oder? But, it isn’t.

Warum? Ganz einfach aus zwei Gründen:

  1. Damit Fail2Ban direkt auf dem Proxy die Anfragen, die als “missbräuchlich” (engl. abuse) eingestuft werden, auch blockieren kann, muss er die Logdateien der angegriffenen Systeme lesen können. Das kann Fail2Ban aber nicht, da die Systeme physikalisch auf einem anderen Gerät laufen. Auf dem XigmaNAS-Host war das einfach, denn dort lief Fail2Ban auf dem Host und der kann in den Jails Daten lesen, also auch die Logs.
  2. Die Verzeichnisstruktur von FreeBSD ist grundlegend anders. Alle Verweise, die auf absolute Verzeichnisnamen zeigen, müssen angepasst werden. Und das sind bei meinen ganzen Subdomains ganz schön viele…

Die Vorbereitung

Als erstes habe ich mich per WinSCP (manchmal nehme ich auch den Bitvise SSH Client, da ich hier schnell mal eine Konsole aufmachen kann) in den XigmaNAS-Host eingewählt und die Ordner /usr/local/etc/letsencrypt und /usr/local/etc/nginx aus dem Jail des ReverseProxy geholt.

Anschließend werden nginx, CertBot und Fail2Ban auf das neue System gebracht, um die Verzeichnisstruktur aufzubauen. Würde man zuerst die Daten aufspielen und dann erst installieren, könnte die Installation wichtige Konfigurationen der vorhandenen Daten überschreiben.

apt-get update
apt-get upgrade
apt-get install python3-acme python3-certbot python3-mock python3-openssl python3-pkg-resources python3-pyparsing python3-zope.interface
apt-get install nginx certbot python-certbot-nginx syslog-ng

Vorsicht, an dieser Stelle folge ich nicht der Anleitung von CertBot, denn ich will keine neuen Zertifikate erstellen. Ich habe ja schon welche, und diese werde ich in einem späteren Schritt einfügen.

Ebenso wird fail2ban erst später installiert, da ich hier nicht die veraltete Version aus den Debian-Paketen haben will (dort aktuell 0.9.x), sondern mit der (tages-)aktuellen Version in GitHub arbeite.

Daten kopieren, Pfade anpassen

Nun werden die Daten, die ich vom alten Server geholt habe, auf den Raspberry geschoben. Auch müssen einige Verweise in den Konfigurationsdateien angepasst werden.

nginx

Daten kopieren

Hier gibt es erhebliche Unterschiede in der Ordnerstruktur. Während nginx unter FreeBSD einfach alle Dateien als aktiv erkennt, die auf die Maske /usr/local/etc/nginx/conf.d/*.conf passen, ist es unter Debian usus, die Dateien im Verzeichnis /etc/nginx/sites-available/ abzulegen und per symlink in /etc/nginx/sites-enabled/ dorthin zu verweisen.

Praktisch ist es, dass eine frische Installation von nginx beide Versionen erkennt. Theoretisch könnte man die Daten 1:1 kopieren und fertig. Ich habe mich aber entschieden, dem Debian-usus zu folgen.

Da ich nur eine IPv4 besitze, habe ich – der Übersichtlichkeit halber – eine Unterstruktur angelegt, da ich Dienste als http, mail und stream darüber laufen lasse. Meine alte nginx.conf enthält daher folgendes:

http { 
...
    include /usr/local/etc/nginx/conf.d/http/*.conf;
}

stream {
...
    include /usr/local/etc/nginx/conf.d/stream/*.conf;
}

mail { include /usr/local/etc/nginx/conf.d/mail/*.conf; }

Für die korrekte Funktion von nginx muss ich auch weiterhin http, mail und stream trennen, da die Dateien in verschiedenen Abschnitten der local.conf geladen werden. Daher behalte ich diese Ordnerstruktur bei und kopiere die Daten aus /usr/local/etc/nginx/conf.d/ einfach nach /etc/nginx/sites-available/, so dass es später diese Struktur gibt:

/etc/nginx/sites-available/http/
/etc/nginx/sites-available/mail/
/etc/nginx/sites-available/stream/

Die gleiche Struktur bekommt auch /etc/nginx/sites-enabled/ verpasst.

Dateien anpassen

Wie ihr gesehen habt, benutzen FreeBSD und nginx unterschiedliche Verzeichnisstrukturen. Die Verzeichnisse in den Konfigurationsdateien enthalten leider absolute Verweise. Daher müssen die Daten hier noch angepasst werden, sonst verweigert nginx den Start, da die Verzeichnisse, bzw. die referenzierten Dateien, nicht gefunden werden.

Das lässt sich per Hand machen, was eine sehr mühevolle Arbeit darstellt, oder man lässt die Maschine die Daten automatisch ändern. Aufgrund der Menge der Fundorte von absoluten Verzeichnisverweisen habe ich mich für die maschinelle Version entschieden.

Dazu nutze ich den “stream editor” sed(1). (Wenn ihr die Dateien unter FreeBSD manipulieren wollt, schaut euch diesen Artikel an, da sed unter FreeBSD anders funktioniert.)

Nun soll aus /usr/local/etc/nginx/conf.d/http/*.conf die Zeichenfolge /etc/nginx/sites-enabled/ werden. Der Befehl dazu lautet:

sed -i 's/suchen/ersetzen/g' dateiname

Und da ist er, der fiese Fallstrick! Man beachte, dass der Befehlstrenner nicht im zu suchenden oder zu ersetzenden Text enthalten sein darf. Hier steht es etwas ausführlicher.

Ich habe mich für einen abweichenden Trenner entschieden, da ich keine Lust auf die Maskierung hatte. Also:

sed -i 's%/usr/local/etc/nginx/conf.d/%/etc/nginx/sites-enabled/%g' /etc/nginx/nginx.conf

Da ich meine alte nginx.conf ansonsten weiterverwenden will, werden nun noch die Verzeichnisse für die SSL-Zertifikate angepasst.

sed -i 's%/usr/local/etc/letsencrypt/%/etc/letsencrypt/%g' /etc/nginx/sites-available/*.conf

Wie das sonst so mit letsencrypt läuft, steht unter der nächsten Überschrift.

Ich inkludiere noch weitere Konfigurationen um allen Seiten einheitliche Header zu geben und die Haupt-Konfiguration nicht aufzublähen. Auch da müssen die Pfade angepasst werden.

sed -i 's%/etc/nginx/conf.d/includes/%/etc/nginx/includes/%g' /etc/nginx/sites-available/http/*.conf

Ein Test ist an dieser Stelle noch nicht möglich, da erst noch die Installation von letsencrypt angepasst werden muss. Im Verzeichnis /etc/letsencrypt/live/ liegt noch nichts, was nginx verwenden kann, ein Test bzw. ein Start von nginx würde noch zu folgendem Fehler führen:

# nginx -t
nginx: [emerg] BIO_new_file("/usr/local/etc/letsencrypt/live/www.beispiel.de/fullchain.pem") failed (SSL: error:02001002:system library:fopen:No such file or directory:fopen('/usr/local/etc/letsencrypt/live/beispiel.de/fullchain.pem','r') error:2006D080:BIO routines:BIO_new_file:no such file)
nginx: configuration file /etc/nginx/nginx.conf test failed

CertBot / letsencrypt

Daten kopieren

Die Daten lassen sich sehr einfach auf den neuen Server bringen. Einfach alles, was bei FreeBSD in /usr/local/etc/letsencrypt/ war unter Debian in den Ordner /etc/letsencrypt/ kopieren.

Pfade anpassen

Wieder wird sed(1) benutzt, um die Pfade zu korrigieren. Im folgenden Codeschnipsel wechsel ich in den Ordner der Konfiguartionsdateien und passe die Pfadstruktur an Debian an:

cd /etc/letsencrypt/renewal/
sed -i 's%/usr/local/etc/%/etc/%g' *.conf

Das war es auch schon. Jetzt kommt der arbeitsintensive Teil…

Symlinks wieder aufbauen

Dicker Fallstrick! Eine 1:1 Kopie von letsencrypt funktioniert hier nicht.
Hier gibt es mit Abstand nun die meiste Arbeit. CertBot arbeitet mit symbolischen Links (symlinks). Diese lassen sich erstens nicht per SSH auf den “Zwischenwirt” holen und zweitens würden sie eh auf der FreeBSD-Verzeichnisstruktur aufbauen.

Daher heißt es nun: Symlinks wieder aufbauen! Das geschieht mit ln(1). Für den folgenden Code nehme ich an, die Domain heißt www.beispiel.de und es ist der siebte Schlüssel, der aktuell ist. Welcher Schlüssel aktuell ist, kann man sehen, wenn man das Verzeichnis /etc/letsencrypt/archive/www.beispiel.de/ anzeigt. Die höchste Zahl ist der aktuelle Schlüssel.

# ls -l /etc/letsencrypt/archive/www.beispiel.de/
total 112K
-rw-r--r-- 1 root 2260 Jan 23  2019 cert1.pem
-rw-r--r-- 1 root 2260 Apr  4  2019 cert2.pem
-rw-r--r-- 1 root 2260 Jun  6  2019 cert3.pem
-rw-r--r-- 1 root 2260 Sep  3  2019 cert4.pem
-rw-r--r-- 1 root 2260 Nov  3  2019 cert5.pem
-rw-r--r-- 1 root 2260 Jan 21 15:24 cert6.pem
-rw-r--r-- 1 root 2260 Mar 21 17:06 cert7.pem
-rw-r--r-- 1 root 1647 Jan 23  2019 chain1.pem
-rw-r--r-- 1 root 1647 Apr  4  2019 chain2.pem
-rw-r--r-- 1 root 1647 Jun  6  2019 chain3.pem
-rw-r--r-- 1 root 1647 Sep  3  2019 chain4.pem
-rw-r--r-- 1 root 1647 Nov  3  2019 chain5.pem
-rw-r--r-- 1 root 1647 Jan 21 15:24 chain6.pem
-rw-r--r-- 1 root 1647 Mar 21 17:06 chain7.pem
-rw-r--r-- 1 root 3907 Jan 23  2019 fullchain1.pem
-rw-r--r-- 1 root 3907 Apr  4  2019 fullchain2.pem
-rw-r--r-- 1 root 3907 Jun  6  2019 fullchain3.pem
-rw-r--r-- 1 root 3907 Sep  3  2019 fullchain4.pem
-rw-r--r-- 1 root 3907 Nov  3  2019 fullchain5.pem
-rw-r--r-- 1 root 3907 Jan 21 15:24 fullchain6.pem
-rw-r--r-- 1 root 3907 Mar 21 17:06 fullchain7.pem
-rw-r--r-- 1 root 3268 Jan 23  2019 privkey1.pem
-rw-r--r-- 1 root 3272 Apr  4  2019 privkey2.pem
-rw-r--r-- 1 root 3272 Jun  6  2019 privkey3.pem
-rw-r--r-- 1 root 3272 Sep  3  2019 privkey4.pem
-rw-r--r-- 1 root 3272 Nov  3  2019 privkey5.pem
-rw-r--r-- 1 root 3272 Jan 21 15:24 privkey6.pem
-rw-r--r-- 1 root 3272 Mar 21 17:06 privkey7.pem

Dann würde der Befehl für diese Domain lauten:

ln -s /etc/letsencrypt/archive/www.beispiel.de/cert7.pem /etc/letsencrypt/live/www.beispiel.de/cert.pem
ln -s /etc/letsencrypt/archive/www.beispiel.de/chain7.pem /etc/letsencrypt/live/www.beispiel.de/chain.pem
ln -s /etc/letsencrypt/archive/www.beispiel.de/fullchain7.pem /etc/letsencrypt/live/www.beispiel.de/fullchain.pem
ln -s /etc/letsencrypt/archive/www.beispiel.de/privkey7.pem /etc/letsencrypt/live/www.beispiel.de/privkey.pem

Vieeeel Handarbeit wenn der ReverseProxy viele (Sub-)Domains bedient. Aber eine kleine Formel in OpenOffice Calc (oder wahlweise Excel) hilft da ungemein und macht da einen recht flott zu erledigenden copy&paste-Marathon draus.

nginx: Test und Start

Bevor die Installation von CertBot getestet werden kann, muss nginx laufen. Da im vorigen Schritt die erforderlichen Symlinks aufgebaut wurden, sollte der Test von nginx erfolgreich sein.

# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Jawoll, das passt, keine Fehler. Also wird jetzt für den nächsten Schritt nginx gestartet.

systemctl start nginx

CertBot: Test und Cronjob einrichten

Ist die Symlink-Orgie vorbei, kommt nun der Test, ob alles soweit funktionert. Dazu wird CertBot mit der Option –dry-run ausgeführt, deren Aufgabe es ist, einen Zertifikatstausch zu simulieren.

# certbot --nginx --dry-run renew
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/www.beispiel.de.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator nginx, Installer nginx
Renewing an existing certificate
Reusing existing private key from /etc/letsencrypt/live/www.beispiel.de/privkey.pem.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed with reload of nginx server; fullchain is
/etc/letsencrypt/live/www.beispiel.de/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
...
...
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates below have not been saved.)

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/www.beispiel.de/fullchain.pem (success)
  ...
  ...
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Da das fehlerfrei durchgelaufen ist, kann nun der cronjob eingerichtet werden, der regelmäßig CertBot aufruft, damit wir nicht mehr dran denken müssen, wann Zertifikate zu erneuern sind.

Der Server von LetsEncrypt soll nicht überlastet werden. Um das zu verhindern wird ein kleines Script /usr/local/renew.sh mit folgendem Inhalt angelegt:

#!/bin/sh
python -c 'import random; import time; time.sleep(random.random() * 1800)'
certbot renew --nginx

Anschließend wird das Script noch ausführbar gemacht.

chmod u+x /usr/local/renew.sh

Dieses Script startet einen Zufallstimer von 30 Minuten. Wenn der abgelaufen ist, wird CertBot ausgeführt. Mit dem Befehl crontab -e wird der Crontab-Editor aufgerufen. Dort wird folgende Zeile angelegt:

0 5,17 * * * /bin/sh /usr/local/renew.sh >/dev/null 2>&1

Diese Zeile bedeutet übersetzt: Führe täglich um 5 und 17 Uhr den Befehl /bin/sh /usr/local/renew.sh aus. Dabei werden keine Ausgaben erstellt.

Besonderheit: Mailserver

Die Mailserver nutzen die Zertifikate selbst, aber die Zertifikate werden zentral vom ReverseProxy verwaltet. Wie bekommen nun die Mailserver die neuen Zertifikate?

Dafür nutze ich in CertBot einen sog. “Hook”, der ausgeführt wird, wenn für die Domain ein neues Zertifikat bezogen wurde. Der Hook kopiert das neue Zertifikat per SSH auf die Mailserver und führt anschließend einen Neustart der Dienste aus.

syslog-ng

Die nachgelagerten Server werden so eingerichtet, dass sie ihre Logs an den ReverseProxy senden. In der /etc/syslog.conf der nachgelagerten Server setze ich dafür:

*.* @ip.des.proxy

Nach einem Neustart von syslog sendet dieser Server damit sein syslog per UDP-Port 514 an den ReverseProxy. Hier werden die Daten mit der folgenden Konfig aufgefangen und in eine Logdatei geschrieben, die im nächsten Abschnitt von Fail2Ban ausgelesen wird. Die Datei, die das steuert, ist die /etc/syslog-ng/conf.d/external.conf. Die kann auch anders heißen, aber hütet euch, die Datei /etc/syslog-ng.conf zu ändern, denn bei einem Update kann auch diese Datei überschrieben werden!

source      s_network { syslog(ip(interne.IP.v4) port(514) transport("udp")); };
destination d_external { file("/var/log/external/${HOST}.log"); };
log         { source(s_network); destination(d_external); };

Das Makro ${HOST} sorgt dafür, dass pro sendendem Host eine Datei geschrieben wird. Das sorgt für Übersichtlichkeit und gibt später in Fail2Ban weitere Filtermöglichkeiten.

So, das war es hier auch schon. Wenn syslog-ng noch nicht läuft, dann starten:

systemctl start syslog-ng 

Oder, falls es schon läuft, einfach die Konfiguration neu einlesen:

systemctl reload syslog-ng

Fail2Ban

Wie oben geschrieben, will ich nicht die Standardinstallation von Fail2Ban verwenden, da diese Version noch keine rezidiven Filter unterstützt.

Installation

Ich lege ein temporäres Verzeichnis an, hole die aktuelle Version von Github, entpacke und installiere sie und kopiere zum Schluss noch die fail2ban.service an den richtigen Ort (aus irgendwelchen Gründen hat es der Installer nicht selbst gemacht). Fail2Ban kann auch direkt schon gestartet werden, um schon mal einen Grundschutz zu haben.

mkdir /tmp/fail2ban
cd /tmp/fail2ban

wget https://github.com/fail2ban/fail2ban/archive/0.11.1.tar.gz -I /tmp/
tar -xvf /tmp/0.11.1.tar.gz -C /tmp/
cd /tmp/fail2ban-0.11.1
sudo python setup.py install
cp /opt/fail2ban-github/fail2ban/build/fail2ban.service /etc/systemd/system/fail2ban.service
service fail2ban start

Konfiguration

Die Konfiguration von Fail2Ban geschieht ausschließlich in der /etc/fail2ban/jail.local. Warum? Dies ist die einzige Datei, die von einem Update nicht überschrieben wird. Natürlich kann man auch direkt die jail.conf bearbeiten, aber bei einem Update werden die Änderungen auf Standartwerte zurückgesetzt. Hier ein Auszug aus meiner jail.local:

[DEFAULT]
dbpurgeage =		6w
ignoreip =		127.0.0.1/8, interne-IPv4.0/24 externe-IPv6::/62 interne-IPv6::/64 ::ffff:127.0.0.0/104 ::1/128 
ignorecommand =
bantime =		1m
findtime =		7d
maxretry =		5
backend =		auto
usedns =		warn
logencoding =		auto
enabled =		false
bantime.increment =	true
bantime.maxtime =	10w
#bantime.factor =	1
#bantime.formula =	ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor
#bantime.formula =	ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)
#bantime.multipliers =	1 2 4 8 16 32 64
#bantime.multipliers =	1 5 30 60 300 720 1440 2880
bantime.overalljails =	true
destemail =		XXX
sender =		XXX
mta	 =		sendmail
protocol =		tcp
chain =			INPUT
#port =			0:65535
fail2ban_agent =	Fail2Ban/%(fail2ban_version)s
action =		%(action_)s
			%(action_abuseipdb)s[abuseipdb_category="18"]

[sshd]
enabled =		true
mode =			aggressive
action =		%(action_)s
			%(action_abuseipdb)s[abuseipdb_category="18,22"]
maxretry =		2
logpath = 		%(sshd_log)s

[sshd-server7]
enabled =		true
mode =			aggressive
filter =		sshd[mode=%(mode)s]
action =		%(action_)s
maxretry =		2
logpath =		%(server7_log)s
port =			%(sshd/port)s

[sshd-server11]
enabled =		true
mode =			aggressive
filter =		sshd[mode=%(mode)s]
action =		%(action_)s
maxretry =		2
logpath =		%(server11_log)s
port =			%(sshd/port)s

[sshd-server36]
enabled =		true
mode =			aggressive
filter =		sshd[mode=%(mode)s]
action =		%(action_)s
maxretry =		2
logpath =		%(server36_log)s
port =			%(sshd/port)s
...

Die vom ReverseProxy betreuten Server senden ihre Logdateien an den Proxy. Mit einer angepassten /etc/fail2ban/paths-override.local werden die von syslog-ng aufgefangenen Logmeldungen für Fail2Ban nutzbar gemacht:

# Overrides

[INCLUDES]

before =	paths-common.conf

[DEFAULT]
external_log =	/var/log/external/external.*

#xigmanas
server7_log =	/var/log/external/*server.7.*

#mysql
server11_log =	/var/log/external/*server.11.*
...

Ist die Konfiguration entsprechend der lokalen Bedürfnisse angepasst, werden die Konfig-Dateien neu geladen. Da der Dienst ja schon oben mit einer Grundkonfiguration gestartet wurde, reicht:

fail2ban-client reload

Sollte Fail2Ban noch nicht laufen, gibt der Befehl einen Fehler aus. Dann muss der Dienst gestartet werden:

service fail2ban start

Fertig!

So, nun laufen CertBot, nginx, Fail2Ban und syslog-ng auf einem eigenen Server. Der Host auf dem die Jails laufen ist nun spürbar entlastet.

Sollte ich was vergessen haben, oder etwas unklar sein, einfach unten in die Kommentare!

Kommentar verfassen