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

  1. Webserver / reverse Proxy
  2. TLS Schlüsselverwaltung
  3. 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.

Fußnoten:

1 ACME ist das Automatic Certificate Management Environment aus rfc8555