Webserver, Reverseproxy, Jails und TLS
Wenn man einen Webserver ins Internet stellt sollte man sich mal eine Weile die Zugriffsprotokelle von SSH und dem Webserver ansehen. Da wird einem sofort klar wieviele hundert oder tausend Einbruchsversuchen man sich pro Tag aussetzt.
Es empfiehlt sich also, sich genau zu überlegen was man diesem Ansturm aussetzt, insbesondere, wenn man einigermaßen stabile Dienste im Internet bereitstellen will.
Natürlich kann ich jetzt ein Webspace mieten und Apps und Datenbanken in der Cloud verteilen, aber die schlanke Methode ist ein kleiner Server beim Hoster. Volle Freiheit, volle Kontrolle, geringe Kosten und ich kann viel überflüssiges weglassen.
Der Host
Ich nehme da gerne FreeBSD. Eine frische Installation hat genau drei Ports offen: SSH, ntp und syslogd auf den Ports 22, 123 und 514. Wenn man die Kiste wegen der besseren Leitung beim Provider stehen hat kann man SSH nicht zumachen, ntp braucht man auch, aber syslogd sollte nicht gegenüber dem Internet offen stehen.
Syslogd
Den macht man mit syslogd_flags="-ss"
in /etc/rc.conf
zu. Wenn man
Logs von wo anders empfangen will setzt man die zugelassenen
IP-Adressen mit "-b x.x.x.x"
.
SSH
Bei ssh stellt man sicher, dass in /etc/ssh/sshd_config
PermitRootLogin no
gesetzt ist und erlaubt, wenn möglich, nur login
mit keys, nicht mit Passworten. Wer sich ansieht wieviele Angriffe
über ipv4 und wie wenige über ipv6 reinkommen der überlege sich, ob er
ssh auf ipv4 abschalten kann. Das ipv6 um soviel weniger angegriffen
wird liegt einfach daran, dass der Adressraum, der zu scannen wäre, so
große ist. Veraltet, da das inzwischen geht: Ich für meinen Teil habe
ipv4 noch an, weil Vodafone im Jahre 2020 immer noch nicht in der Lage
ist mir eine Mobilverbindung mit ipv6 zu schalten.
Blacklistd und Firewall
FreeBSD bringt aktuell blacklistd
mit, ein Tool, dass unter anderem IP
Adressen, von denen Loginversuche mit falschen Logindaten kommen, in
der Firewall zu Sperrlisten hinzufügt. Da muss man natürlich
aufpassen sich nicht selbst auszusperren.
Hierfür bekommt /etc/rc.conf
:
pf_enable="YES" pflog_enable="YES" sshd_flags="-o UseBlacklist=yes" blacklistd_enable="YES"
Jails
Da man immer damit rechnen muss, dass ein Angreifer eine Schwachstelle in komplexerem Code findet sollte auf dem Host jetzt nichts laufen, dass von außen Verbindungen annimmt. Alles was von außen erreichbar ist (außer SSH) gehört in Jails. In meinem Fall sind das
- Webserver / reverse Proxy
- TLS Schlüsselverwaltung
- Applikation für Kunden
Verkehr von drinnen nach draußen
FreeBSDs Packagemanager kann vom Host aus die Jails bedienen wenn man
-j
mit angibt. Die Jails müssen also dafür nicht nach außen
kommunizieren. Man macht die Firewall auf dem Host, FreeBSD hat drei
von Haus aus dabei, also so zu, dass die Jails erstmal keinen Verkehr
ins Internet initiieren dürfen.
Wenn man dann doch Zugriffe braucht macht man die einzeln wieder auf.
Verkehr von außen nach innen
Die IP-Adressen der Jails lege ich in ein privates Netz, sie sollen von außen nicht adressierbar sein.
Den einzigen Verkehr, den man jetzt von außen rein läßt, ist der http
und https Verkehr zum Webserver im Webserverjail. In meinem Fall habe
ich es diesmal mit h2o
aufgesetzt.
Firewall redirect für das Webserverjail
in pf wären das z.B. die Regeln:
rdr pass on em0 inet proto tcp from any to 46.4.89.24 port = http -> x.x.x.x port 80 rdr pass on em0 inet proto tcp from any to 46.4.89.24 port = https -> x.x.x.x port 443
welche ich in einen pf
Anchor
packe.
Einer der für mich nützlichsten Tipps ist bei pf nicht (nur) zu
versuchen die Konfigurationsdatei zu verstehen, sondern sich mit pfctl
-s rules
und Unterbefehlen die live Regeln anzeigen zu lassen. Da
sieht man die Variablen nämlich aufgelöst. Dabei hilft natürlich
wieder unser alter Freund grep
und seine -v
Option, die von Zeilen mit
Treffern auf Zeilen ohne Treffer umschaltet.
TLS Zertifikate
Webserver haben mit TLS immer ein Henne und Ei Problem mit den Zertifikaten. Außerdem will ich das System schlank halten und mir für ein Zertifikatmanagement nicht python oder perl oder sonst was an Abhängikteiten einfangen.
Ich probiere es daher mal mit acme.sh
1 aus den Ports. In einem
eigenen Jail installiert legt es sich als $HOME
/var/db/acme
an. Das
ist ungewöhnlich, aber nicht dumm. Es liegt daher im zfs
Bootenvironment
.
Ein Grund dafür acme.sh
zu nutzen ist beim Zertifikatupdate ohne
Rootrechte arbeiten zu können.
Die Kommunikation zwischen dem Gefängnis des Webservers und von acme
löse ich über ein eigenes zfs Dataset
, dass beide mounten.
Wenn acme.sh
von letsencrypt neue Zertifikate anfordert wird
letsencrypt mir einen Erkennungscode geben und auf die Domäne
zugreifen für die ich ein Zertifikat anfordere und dort eben diesen
Code sehen wollen. So stellt letsencrypt sicher, dass das auch meine
Domäne ist. Ich sorge also dafür, dass es auf dem gemeinsamen zfs
Dataset ein Verzeichnis gibt, in das acme.sh
diesen Code legen kann
und das von h2o
ausgeliefert wird wenn letsencrypt die URL
anfordert. Dieser Übergabepunkt ist https-certs/acme
.
acme.sh --issue -d markusgraf.net -w /mnt/https-certs/acme
Daran hängt acme.sh
.well-known/acme-challenge/
an.
Die Certifikate landen danach in /var/db/acme/.acme.sh/markusgraf.net
.
Um sie für h2o
zu hinterlegen befiehlt man einmalig
acme.sh --install-cert -d markusgraf.net --key-file /mnt/https-certs/markusgraf.net-privkey.pem --fullchain-file /mnt/https-certs/markusgraf.net-fchain.pem
acme.sh
merkt sich die Konfiguration und versucht sogar einen Cronjob
anzulegen, der die Zertifikate alle 60 Tage erneuert. Ich beschreibe
aber weiter unten, wie ich den Cronjob einrichte.
Hier das zugehörige Setup von h2o
um die letsencrypt Anfragen zu beanworten.
hosts: "markusgraf.net:80": listen: port: 80 paths: "/": redirect: status: 302 url: https://markusgraf.net # Leite das ACME Protokoll auf den gemeinsamen Pfad "/.well-known/acme-challenge": file.dir: "/mnt/https-certs/acme/.well-known/acme-challenge" "markusgraf.net:443": listen: port: 443 ssl: minimum-version: TLSv1.2 dh-file: /usr/local/etc/ssl/dhparam.pem # Zertifikate müssen nach der Challenge hier hinterlegt werden certificate-file: /mnt/https-certs/markusgraf.net-fchain.pem key-file: /mnt/https-certs/markusgraf.net-privkey.pem <<: !file /usr/local/etc/h2o/default_ssl.conf file.send-gzip: on paths: # Verzeichnis für Blogposts "/": file.dir: "/var/www/markusgraf.net" # Leite / nach index.html "/": redirect: status: 301 url: https://markusgraf.net/index.html # Leite das ACME Protokoll auf den gemeinsamen Pfad "/.well-known/acme-challenge": file.dir: "/mnt/https-certs/acme/.well-known/acme-challenge"
Zertifikate über Cron erneuern
Da acme.sh
sich die Einstellungen gemerkt hat reicht für ein Update
der Zertifikate
acme.sh --cron
Im richtigen Verzeichnis ausgeführt werden damit nicht nur von der Domäne, die ich hier beschreibe, markusgraf.net, sondern auch von allen anderen hier eingerichteten, die Zertifikate überprüft und bei Bedarf erneuert.
Hierfür gibt es crontab
. Crontab editiert man mit
crontab -u acme -e
im acme Jail.
Die Zeile, die man dem Crontab einfügt ist:
45 0 * * * acme.sh --cron --home /var/db/acme/certs
Die Zeile sorgt dafür, dass ohne Rootrechte, täglich um 0:45, jede Nacht, alle Zertifikate überprüft und bei Bedarf erneuert werden.
Jetzt muss nur noch der Webserver seine Konfiguration neu einlesen damit die neuen Zertifikate auch verwendet werden.
Hierfür legt man im Webserverjail die Datei /usr/local/etc/cron.d/h2o
an.
Sie bekommt die Zeile
# SHELL=/bin/sh PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin # #minute hour mday month wday who command # # Reload h2o config for certificate update 5 1 * * * root service h2o reload
Die Zeile sorgt dafür, dass täglich um 1:05, also 20 Minuten nachdem
unter Umständen neue Zertifikate installiert wurden, h2o
seine
Konfiguration neu einliest.
Was nun?
Damit steht das Grundsetup und ich kann schon darüber bloggen indem ich das ox-publish Paket in Emacs aufrufe. Es nimmt eine Org-Datei, wandelt sie in html und legt sie in das oben konfigurierte Verzeichnis.
Natürlich kommen dann noch das Logging, Konfiguration von h2o für die Anwendungen die hinter dem Reverseproxy laufen und diese Anwendungen selbst.