pf als Firewall-System wurde für openBSD entwickelt und ist äußerst leistungsfähig. Wir werden aber nur grundlegende Regeln und Funktionen betrachten. Es gibt sehr umfangreiche Anleitungen und Dokumentationen. Aber wenn du das Prinzip verstanden hast, dann findest du dich damit schnell zurecht.

Dieses Tutorial ist lediglich als Grundlage zu verstehen. Wenn du dieses Tutorial durchgelesen hast und verstanden hast was hier passiert, dann kannst du dich auf den tiefer gehenden Webseiten (siehe Links) weiter einlesen.
Ich beschreibe hier nicht alle Funktionen von pf, das wäre viel zu umfangreich. Es gibt ein sehr gutes Handbuch, welches auch in der Linksammlung zu finden ist. Weiters gehe ich hier von einem Server aus und nicht von einem Gateway, sodass Weiterleitungsregeln o.ä. hier nicht angesprochen werden.

ACHTUNG: Um die Firewall zu konfigurieren sind root-Rechte erforderlich. Du kannst die Befehle also alle mit einem vorangestellen 'sudo' ausführen. Details dazu im Kapitel "Einführung in sudo".

Um pf beim Systemstart zu starten, müssen wir es in der Datei /etc/rc.conf aktivieren. Dafür schreiben wir folgendes in die Datei:

pf_enable="YES"
pf_rules="/etc/pf.conf"
pflog_enable="YES"
pflog_logfile="/var/log/pflog"

  1. Wenn wir mit pf experimentieren, kann schnell mal ein Fehler unterlaufen. Um zu verhindern, dass wir uns versehentlich aussperren, erstellen wir einen cronjob, der in regelmäßigen Abständen die Firewall wieder deaktiviert:

    # crontab -e

    Jetzt tragen wir folgende Zeilen ein:

    ### Firewall alle 10 Minuten deaktivieren ###
    */10   *   *   *   *   root   /usr/local/bin/sudo /sbin/pfctl -d

    Durch Abspeichern wird der cronjob aktiviert.

    Tipp: der Befehl crontab -l listet alle installierten user-cronjobs auf.
  2. Wir beginnen unsere Experimente indem wir die Datei pf.conf im Home-Verzeichnis erstellen. Diese werden wir später mit sudo laden und nach erfolgreichem Test über die Datei /etc/pf.conf kopieren.

    Zunächst aber ein paar Grundlagen:
    1. Makros sind eine Art Array, also eine Liste von Daten. In unserem Fall Ports, wie wir sie aus der Datei/etc/services bereits kennen. Wir verwenden diese Makros um eine Regel für mehrere Ports gültig zu machen, ohne diese Regel für jeden Port neu einzufügen. Ein Makro sieht so aus (Port 80 für Webserver, 2031 für unseren SSH):
    2. tcp_pass = "{ 80 2031 }"
    3. pf liest Regeln von oben nach unten
    4. Port-Ranges können so geschrieben werden: bspw. 120:135
    5. pass on => ein- und ausgehender Datenverkehr
      pass in => eingehender Datenverkehr
      pass out => ausgehender Datenverkehr
    6. Das Wort 'quick' in einer Regel bewirkt, dass bei Zutreffen der Regel die nachfolgenden Regeln ignoriert werden.
    7. Der Tag 'keep state' sorgt dafür, dass eine einmal hergestellte Verbindung nicht nochmal geprüft wird. Wenn du also einen Webserver betreibst und er über den Port 80 angesprochen wird, werden die Firewall-Regeln nicht  noch einmal durchlaufen, sondern pf geht davon aus, dass diese Verbindung erlaubt ist, da sie ja schon besteht.
  3. Wir öffnen also unsere Datei aus dem Homeverzeichnis. Der Übersichtlichkeit zu liebe nenne ich meine Test-Datei pf-ben.conf. Zunächst legen wir ein Makro für die eingebauten Interfaces an, falls wir die Konfiguration mal auf einen anderen Server übertragen wollen.
    Hinweis: lnc0 musst du natürlich durch dein Interface ersetzen.

    if = "{ lnc0 }"
  4. Anschließend legen wir zwei Makros an: offene tcp- und udp-Ports:

    tcp_pass = "{ 2031 }"
    udp_pass = "{ 2031 }"
  5. Nach diesen Makros (und nur da!) ergänzen wir nun unsere erste Regel, und zwar zur Paket-Normalisierung:
    scrub in all
  6. Jetzt schützen wir uns noch gegen spoofed oder forged IP-Adressen und blocken alle Ports um nur ausgesuchte freizugeben:

    antispoof for $if
    block in all
  7. Unsere erste Regel sieht also folgendermaßen aus:

    if = "{ lnc0 }"
    tcp_pass = "{ 2031 }"
    udp_pass = "{ 2031 }"
    scrub in all
    antispoof for $if
    block in all
  8. Bevor wir nun unseren ersten Port öffnen, will ich noch auf zwei Besonderheiten von pf eingehen: tables undanchors.
    • tables  - Speichern mehrere IP-Adressen oder ganzer IP-Kreise

      Die tables werden über den Regeln eingetragen und meist mit dem Zusatz 'quick', der oben schon erwähnt wurde, versehen, sodass diese IP-Adressen direkt verarbeitet werden, ohne extra alle Regeln durchlaufen.
      Du kannst diese Tabellen beispielsweise einsetzen, wenn du Zugriffen aus dem eigenen IP-Netz (bspw. 192.168.0.XXX) grundsätzlich erlauben willst. Dann könntest du weit oben eine entsprechende Regel anlegen, die jeden Datenverkehr erlaubt.

      Die Syntax zum Anlegen einer Tabelle lautet wie folgt:

      table <intranet> { 192.168.0.0/24, 192.168.1.0/24, !192.168.0.1 }

      Hinweis: Die spitzen Klammern müssen bleiben. 'intranet' ist hier der Name der Tabelle. Die letzte IP-Adresse wird durch das '!' ausgeschlossen.

      Um nun, wie oben beispielhaft erwähnt, jede Verbindung aus den in der Tabelle 'intranet' hinterlegten IP-Netzen zu erlauben, könntest du folgende Regel möglichst weit oben einfügen (pf liest ja von oben nach unten!):

      pass in quick from <intranet> to any keep state

      Um nun die angelegten Tabellen zu prüfen, kannst du diese über folgenden Befehl einsehen:

      # pfctl -t intranet -T show

      Um die Tabelle nun zu laden ohne den Server neu zu starten (das kann im Produktivbetrieb nicht so ohne weiteres möglich sein), kannst du dies mit folgendem Befehl tun (bei jeder Änderung an der Tabelle muss dies getan werden!):

      # pfctl -t intranet -Tl -f /etc/pf.conf

      Hinweis: /etc/pf.conf ist die Datei in der wir die Tabelle hinzugefügt haben. Ich würde also, wie oben erwähnt, zunächst /home/sz_benedikt/pf-ben.conf schreiben.

      Um eine Tabelle nun wieder zu leeren, gibst du folgenden Befehl ein:

      # pfctl -t intranet -T flush
    • anchors - Speichern Regeln, die on the fly geladen und entladen werden können

      Es gibt eigentlich zwei Hauptszenarien, in denen man anchors einsetzen will:

      Fall 1: Man will nur gelegentlich Ports öffnen, bspw. bei einem Serverupdate
      Wir speichern einen Regelsatz in die Datei '/etc/ftp-anchor' (Name frei wählbar) und fügen folgende Zeile ans Ende unserer pf.conf ein:

      anchor ftpanchor

      Jetzt können wir den Regelsatz mit folgendem Befehl laden und damit aktivieren:

      # pfctl -a ftpanchor -f /etc/ftp-anchor

      Um den anchor nun zu prüfen, verwende folgenden Befehl:

      # pfctl -a ftpanchor -s rules
      Fall 2: Man will nur gelegentlich Ports schließen, bspw. bei Verdacht auf eine Bruteforce-Attacke
      Wir speichern wieder einen Regelsatz in die Datei '/etc/ssh-anchor' (Name frei wählbar) und fügen folgende Zeile ans Ende unserer pf.conf ein:

      anchor sshanchor

      Bisher ist es identisch zu Fall 1, allerdings schreiben wir jetzt noch zusätzlich in die Zeile darunter

      load anchor sshanchor from "/etc/ssh-anchor"

      Dies ist vergleichbar mit einem 'include'-Befehl.

      Um nun einen anchor wieder zu entladen, verwendest du folgenden Befehl:

      # pfctl -a sshanchor -F rules

      Dann kannst du wieder prüfen ob der anchor entladen ist:

      # pfctl -a sshanchor -s rules

      Für jede Art von Regel gibt es eine eigene Anchor-Syntax. Siehe hierzu bitte im Handbuch nach.
  9. Jetzt noch eine kleine Befehlsübersicht für pf. Beachte bitte auch das Unterkapitel "pf: Bruteforce und mehr" sowie das Kapitel "pf-Überwachung: pftop".

    • pf deaktivieren: pfctl -d
    • pf aktivieren: pfctl -e
    • Regeln aus einem anderen Verzeichnis laden: pfctl -ef /home/sz_benedikt/pf-ben.conf
    • Default-Regeln laden: pfctl -f /etc/pf.conf
    • kurzer Status von pf: pfctl -s info
    • Detailstatus zu jeder Regel: pfctl -vs info
  10. Um die Regeln, die ich in der Datei /home/sz_benedikt/pf-ben.conf nun dauerhaft als /etc/pf.conf zu speichern, entferne ich wieder den cronjob (Zeile einfach löschen oder ein '#' davor schreiben -> crontab -e) gehe wie folgt vor:

    # cp /home/sz_benedikt/pf-ben.conf /etc/pf.conf

    # pfctl -f /etc/pf.conf

    Hiermit werden die Regeln aus pf.conf geladen und die, die sich noch im Arbeitsspeicher befinden, gelöscht!

    # pfctl -s rules

    Hiermit gebe ich die nun aktivierten Regeln aus und kann nochmals prüfen ob alles stimmt und die richtige Datei geladen ist.
  11. Um dir ein Beispiel zu zeigen, poste ich meine 'pf.conf'. Beachte bitte, dass ich noch zwei Regeln eingebaut habe, die PING und TRACEROUTE erlauben:

    ### INTERFACES ###
    if = "{ lo0, rl0 }"

    ### SETTINGS ###
    set block-policy drop

    ### OFFENE TCP/UDP-PORTS ###
    tcp_pass = "{ 53 2031 }"
    udp_pass = "{ 53 2031 }"
    icmp_types = "echoreq"

    ### NORMALISATION ###
    scrub in all
    antispoof for $if

    ### TABLES ###
    table <intranet>   { 192.168.0.0/24 }
    table <bruteforce> persist

    ### RULES ###
    set skip on lo0
    block all
    block quick from <bruteforce>
    pass in quick from <intranet> to any keep state
    pass in on $if proto tcp from any to any port $tcp_pass flags S/SA keep state (max-src-conn 100, max-src-conn-rate 15/5, overload <bruteforce> flush global)
    pass in on $if proto udp to any port $udp_pass keep state
    pass out quick all keep state

    # PING #
    pass in on $if inet proto icmp all icmp-type $icmp_types keep state

    # TRACEROUTE #
    pass in on $if inet proto udp from any to any port 33433 >< 33626 keep state