Ein Weg, wie man eine Webserver auf Service-Ebene schützen kann. Teil 2

In meinen letzten Artikel habe ich beschrieben, wie man die Sicherheit seines Webservers auf Server-Ebene etwas verbessern kann und somit ungewollte Verbindung zu unterbinden.

Au hier sein noch einmal gesagt, dass dies nicht der Weisheit letzter Schluss ist, sondern ein zusätzlicher Weg.

In diesem Artikel möchte ich nun darauf aufbauen und beschreiben, wie man nun diese Informationen dazu nutzen kann, eine eigene Block-Regel in einer opnsense-Firewall aufzubauen und mit diesen IP-Adressen zu füllen.

Da das Skript, dass die IP-Adresse bei der opnsense einträgt ein Python-Skript ist, muss also noch Python installiert sein. Dass sollte aber auch kein Problem sein. Wenn man z.B. den certbot für letsencrypt auf dem selben Webserver installiert hat, dann sollte Python bereits mit drauf sein. Es kann sein, dass man das Modul „py38-requests“ nachinstallieren muss. Diese brauchen wir um den API-Request an die Firewall aufzubauen und abzuschicken.

Inhalt:

  1. Erstellung eines Skriptes zum Sammeln von IP-Adressen in einer Text-Datei.
  2. Umbau der Server-Konfiguration zum Sammeln der IP-Adressen.
  3. Einrichten eines Benutzers auf der opnsense-Firewall zur Nutzung der API
  4. Aufbau einer Filter-Regel für die Blockierung von IP-Adressen

Erstellung eines Skriptes zum Sammeln von IP-Adressen in einer Text-Datei.

Damit wir die IP-Adressen sammeln können, die auf unserem Server versuchen, auf unserem Server fehlerhafte Zugriff zu verursachen, müssen ein Skript erstellen, dass Client-IP-Adressen bei Aufruf ermittelt und diese dann in eine Text-Datei schreibt.

Am Ende sollte da Skript außerdem dem „Besucher“ auf die Fehlerseite weiterleiten, damit diese auch weiß, dass er einen fehlerhaften Zugang erzeugt hat. Ob man ihn dann auch dort mitteilt, dass seine IP-Adresse nun auch geblockt wird, bleibt jedem selbst überlassen.

Ich habe hier mal ein php-Skript erstellt, da ich davon ausgehe, das die meisten eine php-Installation verwenden. Inhaltlich kann man das ganze auch bestimmt ganz schnell in andere Sprachen übersetzen. Das sollte recht einfach sein.

Das Skript server2pf.php:

<?php
$ip = isset($_SERVER['HTTP_CLIENT_IP'])  
   ? $_SERVER['HTTP_CLIENT_IP']  
   : (isset($_SERVER['HTTP_X_FORWARDED_FOR'])  
     ? $_SERVER['HTTP_X_FORWARDED_FOR']  
     : $_SERVER['REMOTE_ADDR']);

$s2pfile = fopen("./ip-liste.txt", "a");

fwrite($s2pfile, $ip."\n");
fclose($s2pfile);

echo "<script>location.href='error.html';</script>";

?>

In der Variable $ip wird die Client-IP-Adresse ermittelt.

In der Zeile 2 wird nun ein File-Handle erstellt der eine Textdatei mit Namen „ip-liste.txt“ im selben Ordner erstellt. Möchte man die Datei woanders ablegen, dann muss man hier den Dateipfad anpassen. Aber Vorsicht, der www Dienst muss diesen Pfad erreichen können.

In Zeile 3 wird nun die IP-Adresse und ein Zeilenumbruch in die Datei am Ende angehängt.

Mit der Zeile 4 wird die Datei wieder geschlossen.

Damit der „Besucher“ nun auch etwas davon hat, wird er danach auf die Seite „error.html“, die sich auch im selben Verzeichnis befindet, weitergeleitet. Ist die Datei in einen anderen Pfad oder möchte man einen anderen Namen dafür verwenden, so muss man nur den Wert für „location.href“ ändern.

Da dieses Skript unabhängig vom Server arbeitet also nur auf php angewiesen ist ,sollte diese Datei mit vielen Versionen arbeiten können.

Umbau der Server-Konfiguration zum Sammeln der IP-Adressen.

Dieses Skript fügt man nun bei der Webserver Konfiguration an der Stelle ein, bei der das Fehlerhandling behandelt wird. Im ersten Artikel haben wir dies so eingerichtet, dass die Datei error.html ausgegeben werden soll.

Konfiguration des nginx, nginx.conf, aus dem letzten Artikel.

error_log  /var/log/nginx/error.log;

error_page   403 404 500 502 503 504  /error-doc/error.html;

       location = /error.html {
           root   /var/www/error-doc/;
       }

Dieses bauen wir nun um. Das Skript legen wir in den selben Ordner wie die Datei error.html, also in /error-doc/. So stimmen die oben bei dem Skript angegeben Pfade. Möchte man eigen Pfade verwenden, so muss man dies auch in der Server-Konfiguration beachten. Hier verwenden wir nginx.

Die neue Konfiguration des nginx (nginx.conf) sieht nun so aus.

error_log  /var/log/nginx/error.log;

error_page   403 404 500 502 503 504  /error-doc/server2pf.php;

       location = /error.html {
           root   /var/www/error-doc/;
       }

Es wird nur bei der Zeile „error_page“ auf das php-Skript verwiesen, da am Ende des Skriptes server2pf.php das Dokument error.html geöffnet wird und dann gelten die Regeln aus den nachfolgenden Zeilen aus der nginx.conf für localtion.

Wenn nun ein „Besucher einen Fehler auf dem Webserver erzeugt, sollte in dem Ordner /error-doc/ eine Textdatei mit Namen „ip-liste.txt“ erstellt werden und mit jedem Mal die Client-IP-Adresse eingefügt werden. Es kann sein, dass IP-Adressen auch doppelt drinnen stehen. Das macht nichts, diese werden vor dem Eintragen auf der opnsense bereinigt.

Einrichten eines Benutzers auf der opnsense-Firewall zur Nutzung der API

Damit wir später über die API die IP-Adressen auf der opnsense eintragen können, benötigen wir noch einen API Zugang. Dafür macht es Sinn, einen Benutzer für API-Nutzung auf der opnsense Firewall einzurichten. Dafür gehen wir zu System -> Benutzer und erstellen einen neuen Benutzer.

Benutzereinstellungen für den API-Benutzer

Folgendes muss bei dem Benutzer eingerichtet werden.

  • Man kann irgendein Passwort einrichten, weil dieses nie verwendet wird.
  • Als Anmeldeshell wählt man „/sbin/nologin“, damit man sich auch nicht irgendwie anmelden kann.
  • Dieser Benutzer muss Mitglied der Gruppe „admins“ sein, damit er auch IP-Adressen in Tabellen == Alias eintragen kann.
  • Unter „API-Schlüssel“ Klickt man auf das Plus-Zeichen um einen neuen API-Schlüssel zu erzeugen und man lädt die Daten herunter, da wir diese später für das Skript server2pf.py benötigen. Diese besteht aus einem Code-Schnipsel mit einem API-Key und API-Secret.

Sind alle Einträge gemacht, kann man auf „Speichern und zurückkehren“ klicken. Der Benutzer wurde nun erstellt und wir haben die API-Zugangsdaten. Diese sind wichtig. damit man sich zur Sicherheit nicht als ein echter Benutzer dort anmelden kann.

Über diesen Benutzer werden die IP-Adressen auf die opnsense übertragen.

Aufbau einer Filter-Regel für die Blockierung von IP-Adressen

Wenn wir schon auf der opnsense-Firewall sind, dann können wir auch gleich die Filterregel aufbauen, in der die IP-Adressen vom Webserver aus eingetragen werden sollen.

Da die Liste am Anfang leer ist, und wir diese auf der Seite einrichten, die Zugriffe aus dem Internet in Richtung Webserver,unterbinden sollen, ist das kein Problem.

Zunächst gehen wir zu Firewall -> Aliase.

Dort erstellen wird einen neuen Eintrag mit den folgenden Einstellungen.

Einstellungen für das externe Alias

Alias sind Tabellen im echten BSD Jargon. Wichtig ist, dass das Alias vom Typ „extern“ ist, damit man dieses auch über die API verwenden kann.

Nun bauen wir eine Filter Regel auf.

  • Aktion setzt man auf „Blockieren“
  • bei „Schnell“ wird das Haken gesetzt, damit keine weitere Aktion anschließend ausgeführt werden soll.
  • Die Schnittstelle soll das externe Interface sein.
  • Quelle wird auf das neue externe Alias gesetzt
  • Man kann als Ziel die IP-Adresse des Webservers setzen oder einfach alle lassen.

Diese Regel wird nun abgespeichert und möglichst ganz weit nach oben verschoben. Idealerweise ganz an den Anfang, da diese Regel zu aller erst geprüft werden soll.

Die Regel kann man nun so aktivieren.

Installation des Skriptes server2pf.py auf dem Webserver

Wir haben nun eine Datei mit IP-Adressen, die wir auf der opnsense Firewall zum Blockieren eintragen wollen, wir haben auf der opnsense-Firewall eine API-Benutzer, über den wir uns an der opnsense anmelden können und wir haben eine passende Filter-Regel, die unser externes Alias(=Tabelle) nutzt um Verbindungen zu blockieren.

Das nächste ist nun das Skript.

Folgendes Skript wird nun auf dem Webserver eingerichtet:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Dieses Skript holt die IP-Adressen aus dem Alias 'server2pf',
vergleicht den Inhalt mit der Liste der IP-Adressen, die der
Webserver aus dem error.log gesammelt hat und schreibt die  
neuen IP-Adressen in das Alias rein.
"""
import requests
import json
import time
import os
# --------------------------------------------------------------------- #
class Server2PF:
   def __init__(self):
       # Die Startwerte
       self.server2ip = {'ipListe': '/<pfad>/<zu>/error-doc/ip-liste.txt',
                         'url_list': 'http://<ip der firewall>/api/firewall/alias_util/list/server2pf',
                         'url_add': 'http://<ip der firewall>/api/firewall/alias_util/add/server2pf',
                         'url_reconfigure': 'http://<ip der firewall>/api/firewall/alias/reconfigure',
                         'key': "<API-Key>",
                         'secret': "<API-secret>",
                         'fw_cert': "/<pfad>/<zum>/zertifikat.pem",
                         'waitIntervall': 2}
       self.badiplist = []
       self.alias_table = []
       self.worklist = []
       self.command = ''
       self.zeile = ''

   def read_file(self):
       if not os.path.isfile(self.server2ip['ipListe']):
           print("Es sind keine IP-Adressen vorhanden.")
           os._exit(0)
       print("Die Liste wird eingelesen")
       self.ipfile = open(self.server2ip['ipListe'], "r")
       self.content = self.ipfile.readlines()
       for self.zeile in self.content:
           self.badiplist.append(self.zeile.strip())
       self.ipfile.close()
       print("Es wurden %i IP-Adressen importiert." % self.badiplist.__len__())
       self.command = "rm " + self.server2ip['ipListe']
       os.system(self.command)

   def get_tablelist(self):
       print("Hole die aktuelle Liste")
       self.request = requests.post(url=self.server2ip['url_list'],
                                    auth=(self.server2ip['key'], self.server2ip['secret']),
                                    verify=self.server2ip['fw_cert'])
       self.alias_content = json.loads(self.request.text)
       for self.zeile in self.alias_content['rows']:
           self.alias_table.append(self.zeile['ip'])
def check_list(self):
       print("Nun werden die Listen verglichen.")
       for self.content in self.badiplist:
           if (not self.content in self.alias_table) & (not self.content in self.worklist):
               self.worklist.append(self.content)

   def block_ip(self):
       print("Die IPs werden geschrieben.")
       if len(self.worklist) != 0:
           print("Es werden %i IP-Adressen uebernommen." % self.worklist.__len__())
           for self.content in self.worklist:
               print("IP %s wird hinzugefügt" % self.content)
               self.payload = {'address': self.content + '/32'}
               self.request = requests.post(url=self.server2ip['url_add'],
                                            auth=(self.server2ip['key'], self.server2ip['secret']),
                                            verify=self.server2ip['fw_cert'], json=self.payload)
               print(self.request.status_code, self.request.text)
               print("Warte %i Sekunden" % self.server2ip['waitIntervall'])
               time.sleep(self.server2ip['waitIntervall'])
       else:
           print("Es gibt nichts zu tun. :)")
       print("Fertig!")
# --------------------------------------------------------------------- #


def main():
   # Hier passiert der ganze Spaß.
   server2pf = Server2PF()     # Das Programm wird gestartet.
   server2pf.read_file()        # Es wird die IP-Liste eingelesen.
   server2pf.get_tablelist()    # Es wird das Alias/Table geladen.
   server2pf.check_list()       # Die Listen werden verglichen.
   server2pf.block_ip()         # Die IP-Adressen werden geschrieben.


if __name__ == '__main__':
   main()

Folgende Anpassungen muss man noch in dem Script machen

  • Zeile 17: Der Pfad zu der Datei ip-liste.txt muss eingetragen werden.
  • Zeile 18, 19 & 20: Die IP-Adresse der opnsense muss bei <ip der firewall> eingetragen werden
  • Zeile 21: Hier muss der API-Key eingetragen werden
  • Zeile 22: Hier muss das API-Secret eingetragen werden.
  • Das Zertifikat benötigen wird zum Aufbau einer https-Verbindung.

Das Skript muss und sollte nicht im Webroot landen. man kann es z.B. beim root in den bin-Ordner ablegen und dieses in der crontab regelmäßig ausführen. Ich lasse es alle 2 Minuten laufen, um das Fenster möglichst klein zu halten.

Was macht das Skript nun?

  1. Es werden die IP-Adressen aus der Textdatei geladen. Sind alle IP-Adressen geladen wird die Datei ip-liste.txt wieder gelöscht. Gibt es keine Datei mit dem Namen ip-liste.txt, dann wird das Skript beendet.
  2. Es werden die IP-Adressen aus dem externen Alias von der Firewall geholt
  3. Nun wird verglichen, ob die IP-Adressen aus der Textdatei bereits in der Liste sind. Wenn nein, dann werden diese in eine Arbeitsliste übertragen. Wenn ja, dann wird diese IP-Adresse ignoriert.
  4. Sind alle IP-Adressen abgeglichen, dann werden die IP-Adressen aus der Arbeitsliste (diese sind ja noch nicht eingetragen) über die API auf der opnsense eingetragen. Zwischen jeden Schritt wird 2 Sekunden gewartet.

Ist bereits eine Datei mit IP-Adressen da, so kann man das Skript einmal von Hand starten und beobachten wie die IP-Adressen in dem externen Alias landen. Dies kann man auf der opnsense unter Firewall -> Diagnose -> Aliase machen. Im Dropdownfeld muss man dafür nur das externe Alias auswählen.

Bereinigen des externen Alias

Da sich das Alias mit der Zeit immer mehr füllt, sollte man es nach und nach bereinigen. Dafür kann man auf der opnsense ein Shell-Skript für den cron in dem Pfad /usr/local/opnsense/service/conf/actions.d/ erstellen.

Skript /usr/local/opnsense/service/conf/actions.d/actions_server2pf.conf

[start]
command:/sbin/pfctl
parameters:-t server2pf -T expire 604800
type:script
description:Bereinige server2pf
message:server2pf wurde bereinigt

Damit diese dann auch unter System -> Einstellungen -> Cron nutzbar wird, müssen Sie auf der Shell noch folgenden Befehl ausführen:

service configd restart

Ich lasse das Skript jeden Tag um 00:00 Uhr laufen um Einträge aus dem Alias zu löschen, die älter als eine Woche sind. Auch hier gilt, dass man den Wert nach „expire“ dafür nach eigenen Belieben anpassen kann.

Nun sammelt der Webserver IP-Adressen, die Fehler produzieren, diese werden regelmäßig in das externe Alias eingetragen und blockiert. Am Ende werden jeden Tag Einträge, die älter als eine Woche sind wieder entfernt.

Es hat sich bei mir gezeigt, dass in der Regel ca.150 bis 180 IP-Adr5essefür eine Woche eingetragen sind.

Eine smarte Sache also.

Robert Friemer

Robert Friemer, mittlerweile 49, arbeitet mit Windows seit Version 3.11, mit Linux seit Version 2.0 und mit FreeBSD seit Version 3.8. Er hat schon so einige Irrungen und Wirrungen in der IT mit erlebt und ist seit einigen Jahren (fast) Windows-los. Dank Pinguin und vor allem dank Beastie.

More Posts - Website