HAProxy - Firewall Regeln

Started by ldg346, May 16, 2021, 02:34:20 PM

Previous topic - Next topic
Hallo zusammen,
ich hätte eine kurze Frage, die ich mit der Suchfunktion und dem Internet leider nicht beantwortet bekomme, vermutlich weil ich die falschen Begrifflichkeiten verwende  ;D

Ich habe mir das HAProxy Plugin (inkl. ACME) eingerichtet, und auch Port 80+443 in den WAN Regeln geöffnet, funktioniert soweit auch wie gewollt.
Nun würde ich aber gerne gewisse Dienste für jeden bereitstellen/öffnen, und andere Dienste nur für gewisse IP-Adressen. z.B. soll ein Webserver in der DMZ für jeden erreichbar sein, und eine Nextcloud im LAN nur für mich (bzw. mein Handy; IP-Ranges des Mobilfunkanbieters habe ich schon).

Wie bekomme ich das denn hin? (ohne VPN)
Ich hab mich schon mit den Firewallregeln gespielt, schaff das aber leider nicht ganz wie gewünscht...

LG

Moin,
das regelst du über rules und conditions in haproxy selbst. Du kannst entweder IP basierte Regeln erstellen oder ein Login auf einer Art Vorschaltseite einbinden oder auch mit Clientzertifkaten arbeiten. Letzteres ist eine sehr effektive Möglichkeit den Zugriff abzuschotten, ohne sich selbst doch irgendwie auszusperren bzw potentielle Fremde zuzulassen, wie bei der Freigabe alle IP Bereiche eines Providers. Du musst lediglich das Zertifikat in deinem Browser oder auf dem Mobilgerät hinterlegen und dann wird es beim Aufruf der Seite verwendet. Wer kein Zert hat, kommt nicht weiter. Das Zert selbst wiederum erstellst du mit wenigen Klicks in Opnsense.

Servus Puldi,
Vielen Dank, das hat mir schonmal gut geholfen!

Unter dem Login mit Vorschaltseite meinst Du vermutlich dieses "http basic auth", welches man über das HAProxy-Plugin unter Usermanagement einstellen kann. Das kannte ich bereits, wollte ich aber für den Fall nicht verwenden.
Das mit der IP und den Conditions/Rules habe ich mir gerade angeschaut, wäre auch eine Lösung mit der ich zufrieden wäre, jedoch habe ich im Plugin keine Möglichkeit gefunden eine IP-Range auszuwählen, sondern nur eine exakte IP als Source. Das hab ich auch probiert, jedoch hat das mit der exakten IP nicht funktioniert hat, warum auch immer :/

Egal, das mit dem Zertifikat ist wohl die beste Art das zu lösen - Kannst Du mir kurz auf die Sprünge helfen wie man das angeht?

Bisher habe ich unter System -> Trust eine Authority (wählt man hier intern oder intermediate?) erstellt, und mit dieser Authority ein internes Client-Zertifikat erstellt. Kann man soweit was falsch machen?
Weiter komm ich jetzt leider nicht, weil ich nicht weiß wie man nun im HAProxy-Plugin bei den Conditions das Zertifikat auswählen bzw. die Überprüfung einstellen kann...

In den HowTo's und Docs wurde ich leider wieder nicht fündig, wäre toll wenn Du mir ein paar Stichpunkte geben könntest :)

Super, das meiste hast du schon erledigt. Du hast ein Serverzertifikat und ein Clientzertifikat. Letzteres lädst du herunter und speicherst es im Browser. Dann gehst du in haproxy in den public service für deine Webseite und kannst dort im Bereich "Client Certificate Auth" das Serverzertifikat auswählen, das verwendet werden soll. Bei verification gibst du "required" an, dann ist das Zertifikat Pflicht.
Wenn haproxy die config neu geladen hat, kannst du das sofort testen. Einmal mit Browser mit Zertifikat und mit einem anderen ohne Zertifikat.  :)

Wenn ich das richtig verstehe, brauche ich aber dann praktisch für jeden Backend Pool und somit jeden Server dieses Zertifikat, weil ich ja für den geöffneten Port 80 u. 443 je nur einen Public Service machen kann?


Weiters kam mir gerade eine neue Frage, wie sieht denn das mit diversen Smartphone Apps aus? Also wenn ich den Webservice, welcher ein Zertifikat benötigt, nicht per Browser sondern per App ansteuere, wie zum Beispiel eine NextCloud? Sollte die jeweilige App es nicht ermöglichen Zertifikate einzupflegen, gibt's da einen Umweg, vielleicht das Zertifikat direkt im Handy integrieren oder so?

Nein, du kannst so viele Frontends, also public services, einrichten wie du willst. Du kannst lediglich Port 80 und 443 nicht mehr mit anderen Services außer haproxy verknüpfen, weil eben der haproxy Prozess diese Ports geöffnet hält. Also zusätzlich eine Domain über nginx bspw funktioniert nicht.

Ob die nextcloud app mit einem clientzertifikat umgehen kann weiss ich nicht. Interessante Frage! Es kann durchaus sein, dass ein im android truststore hinterlegtes client cert auch für die nextcloud app verwendet wird. Sicher bin ich mir da nicht. Aber das ist eine Konstellation die auch selbst mal prüfen werde. Ich bin sowieso dabei, den Zugriff auf meine NXC Instanzen auf opnsense/haproxy umzustellen.

Irgendwas mache ich wohl falsch, habe nun zwei (bzw. drei) Frontends und wenn ich die Domain ansurfe scheint es so, als ob immer nur dasselbe Frontend angesprochen wird.

Kurz zu meinem Setup: ich habe über das LetsEncrypt-Plugin ein (nur eines) Zertifikat für meine Domain erstellt, und dort als "Alt-Names" ein paar subdomains angegeben. Das Zertifikat deckt also ab. Und mit den automatisch erstellten Regeln von LetsEncrypt im HAProxy-Plugin habe ich dann ein Frontend für Port 80 [FE1] und eines für Port 443 [FE2] erstellt, inkl. HTTP Redirect.

Dann habe ich eine neue Subdomain, test3.meinedomain.abc, erstellt, auf die ich ein Server- und ein Client-Zertifikat ausgestellt habe. Wenn ich jetzt also ein weiteres Frontend für Port 443 [FE3] erstelle, und dort unter "SSL Offloading - Certificates" mein eigens erstelltes Server-Zertifikat auswähle, dann bekomme ich beim ansurfen von test3 die Fehlermeldung, dass das Zertifikat nur für die 3 oben genannten Domains gilt. Interpretiere ich also so, dass nur das FE2 mit dem LetsEncrypt-Zert. angesprochen wird.
Als Gegenprobe habe ich bei FE2 nun auch das eigens erstellte Zertifikat angegeben, und dann funktioniert das Zertifikat, und ich bekomme lediglich die Warnung, dass es ein selbsterstelltes ist.
Die Weiterleitung auf den Server funktioniert natürlich nicht und ich bekomme einen Fehler 503, da die entsprechende Condition/Rule ja nur bei FE3 hinterlegt ist.

OK, du musst trennen zwischen dem Serverzertifikat, mit dem der Server sich dem Client gegenüber ausweist (TLS Verbindung) und dem Zertifikat, das der Server nutzt um das eingereichte Clientzertifikat zu prüfen (Client Authentifizierung). Das KÖNNEN serverseitig die gleichen Zertifikate sein, müssen es aber nicht.

Also im Klartext: Der Server weist sich für den Aufbau der TLS Verbindung mit einem Lets Encrypt Zertifikat dem Client gegenüber aus. Dafür muss die Domain im Zertifikat enthalten sein, sonst gibt der Client (meistens also der Browser) eine Warnung aus. Dann steht die TLS Verbindung und der Server fragt den Client im nächsten Schritt nach einem Zertifikat, das den Client berechtigt, die aufgerufene Seite zu sehen. Dafür verwaltet der Server ein oder mehrere weitere Zertifikate, die an dieser Stelle nichts mit TLS zu tun haben.

Du hast auch zwei Stellen in der haproxy Konfig wo du diese Zertifikate auswählst. Einmal beim TLS Offloading, da wählst du das Serverzertifikat für die TLS Verbindung. Und im Bereich Client Authentication wählst du das Zertifikat, mit dem du dein Clientzertifikat signiert hast.

Danke für die Erklärung, das war mir vorher nicht ganz klar!

Jetzt funktioniert es denke wie ich es haben wollte  :D

(Sollte mal jemand dasselbe Problem haben)
Beim Frontend musste ich bei Client Authentification auf "optional" stellen, anschließend eine neue Condition erstellen bei der überprüft wird ob ein Client Certificate vorhanden ist (siehe dieser Thread: https://forum.opnsense.org/index.php?topic=22321.0) und diese Condition dann in der entsprechenden Rule für das gewollte Backend auswählen.

Bezüglich App sieht es zumindet bei Android ganz gut aus, man kann in den Einstellungen Certificates einspielen, die dann an die App weitergegeben werden. Mit Nextcloud habe ich es noch nicht probiert, aber Brave, Firefox und Chrome Browser übernehmen die Certificates wie gewollt :)
(siehe hier: https://www.lastbreach.com/blog/importing-private-ca-certificates-in-android)
Und auch am PC, zumindest Linux, sollte das recht einfach machbar sein.

Vielen Dank Puldi, das hat mir sehr weitergeholfen :)

Vorsicht, Stolperfalle: Optional sollte die Prüfung nur sein, wenn tatsächlich in einem nachgelagerten Schritt das Zertifikat dann geprüft wird. Falls diese Prüfung nicht erfolgt, gibt es dieses auf den ersten Blick unsinnige Verhalten:
Wenn du ein Zertifikat hast, das zu dem hinterlegten Serverzertifikat passt, dann muss es auch gültig sein, damit du zugreifen darfst.
Wenn du kein Zertifikat hast, dann darfst du sofort zugreifen!

Gib doch bitte nochmal deine jetzige Konfig an, denn ich vermute, dass du möglicherweise etwas zu kompliziert rangegangen ist. Ich hatte dich so verstanden, dass du eine eigene Domain für dein Nextcloud nutzt, damit kannst du die Prüfung doch direkt im Public Service erledigen, ohne mit weiteren Conditions arbeiten zu müssen. Denn das macht die Konfig nur unnötig komplex und fehleranfällig.

Quote from: puldi on May 17, 2021, 07:23:33 PM
Wenn du ein Zertifikat hast, das zu dem hinterlegten Serverzertifikat passt, dann muss es auch gültig sein, damit du zugreifen darfst.
Wenn du kein Zertifikat hast, dann darfst du sofort zugreifen!
Das ist mir aufgefallen, der Browser fragt dann nicht nur beim gewünschten (zu kontrollierndem) Backend sondern auch bei allen im Frontend hinterlegten Backends nach dem Zertifikat, sobald ein Cert vorhanden ist welches von der zu überprüfenden Authority ausgestellt wurde.
Ich habe auch versucht mit einem anderen Cert Zugriff zu bekommen, aber wenn ein Cert von einer anderen Authority ausgestellt wurde, fragt er erst garnicht und es ist auch nur Zugriff auf die "offenen" Backends möglich - so soll's sein.

Hier meine Konfig, die Überprüfung erfolgt mit "ssl_c_used 1".

#
# Automatically generated configuration.
# Do not edit this file manually.
#

global
    uid                         80
    gid                         80
    chroot                      /var/haproxy
    daemon
    stats                       socket /var/run/haproxy.socket group proxy mode 775 level admin
    nbproc                      1
    nbthread                    1
    tune.ssl.default-dh-param   2048
    spread-checks               2
    tune.chksize                16384
    tune.bufsize                16384
    tune.lua.maxmem             0
    log /var/run/log local0 info
    ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets prefer-client-ciphers ssl-min-ver TLSv1.2
    ssl-default-bind-ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256
    ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256

defaults
    log     global
    option redispatch -1
    timeout client 30s
    timeout connect 30s
    timeout server 30s
    retries 3
    default-server init-addr last,libc

# autogenerated entries for ACLs


# autogenerated entries for config in backends/frontends

# autogenerated entries for stats




# Frontend: port80 ()
frontend port80
    bind 0.0.0.0:80 name 0.0.0.0:80
    mode http
    option http-keep-alive
    # tuning options
    timeout client 30s

    # logging options
    # ACL: find_acme_challenge
    acl acl_609eaac8987754.71326232 path_beg -i /.well-known/acme-challenge/
    # ACL: NO_find_acme_challenge
    acl acl_609eb1733b0be2.33488218 path_beg -i /.well-known/acme-challenge/
    # ACL: SSL_Established
    acl acl_609eb14e0d1744.30300512 req.ssl_ver gt 0

    # ACTION: redirect_acme_challenges
    use_backend acme_challenge_backend if acl_609eaac8987754.71326232
    # ACTION: HTTP_Redirect
    http-request redirect scheme https code 301 if !acl_609eb1733b0be2.33488218 !acl_609eb14e0d1744.30300512

# Frontend: port443 ()
frontend port443
    http-response set-header Strict-Transport-Security "max-age=15768000"
    bind 0.0.0.0:443 name 0.0.0.0:443 ssl no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets prefer-client-ciphers ssl-min-ver TLSv1.2 ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256 ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ca-file /tmp/haproxy/ssl/609eb4cbd74584.90464975.calist verify optional crt-list /tmp/haproxy/ssl/609eb4cbd74584.90464975.certlist
    mode http
    option http-keep-alive
    # tuning options
    timeout client 30s

    # logging options
    # ACL: find_acme_challenge
    acl acl_609eaac8987754.71326232 path_beg -i /.well-known/acme-challenge/
    # ACL: test1_meinedomain_com
    acl acl_60a29594e32966.45233940 hdr(host) -i test1.meinedomain.com
    # ACL: test2_meinedomain_com
    acl acl_60a295b57d0052.54001161 hdr(host) -i test2.meinedomain.com
    # ACL: client-cert_used
    acl acl_60a27e722581b6.94430866 ssl_c_used 1
    # ACL: cloud_meinedomain_com
    acl acl_60a2ab299f28b3.83471719 hdr(host) -i cloud.meinedomain.com
    # ACL: meinedomain_com
    acl acl_609eb07d666f46.20425389 hdr(host) -i meinedomain.com
    # ACL: test3_meinedomain_com
    acl acl_60a2ab42c93ef1.65464706 hdr(host) -i test3.meinedomain.com

    # ACTION: redirect_acme_challenges
    use_backend acme_challenge_backend if acl_609eaac8987754.71326232
    # ACTION: test1_meinedomain_com
    use_backend test1.meinedomain.com if acl_60a29594e32966.45233940
    # ACTION: test2_meinedomain_com
    use_backend test2.meinedomain.com if acl_60a295b57d0052.54001161
    # ACTION: cloud_meinedomain_com
    use_backend cloud.meinedomain.com if acl_60a27e722581b6.94430866 acl_60a2ab299f28b3.83471719
    # ACTION: meinedomain_com
    use_backend meinedomain.com if acl_609eb07d666f46.20425389
    # ACTION: test3_meinedomain_com
    use_backend test3.meinedomain.com if acl_60a2ab42c93ef1.65464706

# Backend: acme_challenge_backend (Added by Let's Encrypt plugin)
backend acme_challenge_backend
    # health checking is DISABLED
    mode http
    balance source
    # stickiness
    stick-table type ip size 50k expire 30m 
    stick on src
    # tuning options
    timeout connect 30s
    timeout server 30s
    http-reuse safe
    server acme_challenge_host 127.0.0.1:43580

# Backend: meinedomain.com ()
backend meinedomain.com
    # health checking is DISABLED
    mode http
    balance source
    # stickiness
    stick-table type ip size 50k expire 30m 
    stick on src
    # tuning options
    timeout connect 30s
    timeout server 30s
    http-reuse safe
    server meinedomain.com 192.168.40.101:80

# Backend: test1.meinedomain.com ()
backend test1.meinedomain.com
    # health checking is DISABLED
    mode http
    balance source
    # stickiness
    stick-table type ip size 50k expire 30m 
    stick on src
    # tuning options
    timeout connect 30s
    timeout server 30s
    http-reuse safe
    server test1.meinedomain.com 192.168.40.105:80

# Backend: test2.meinedomain.com ()
backend test2.meinedomain.com
    # health checking is DISABLED
    mode http
    balance source
    # stickiness
    stick-table type ip size 50k expire 30m 
    stick on src
    # tuning options
    timeout connect 30s
    timeout server 30s
    http-reuse safe
    server test2.meinedomain.com 192.168.40.104:80

# Backend: cloud.meinedomain.com ()
backend cloud.meinedomain.com
    # health checking is DISABLED
    mode http
    balance source
    # stickiness
    stick-table type ip size 50k expire 30m 
    stick on src
    # tuning options
    timeout connect 30s
    timeout server 30s
    http-reuse safe
    server cloud.meinedomain.com 192.168.40.103:80

# Backend: test3.meinedomain.com ()
backend test3.meinedomain.com
    # health checking is DISABLED
    mode http
    balance source
    # stickiness
    stick-table type ip size 50k expire 30m 
    stick on src
    # tuning options
    timeout connect 30s
    timeout server 30s
    http-reuse safe
    server test3.meinedomain.com 192.168.40.102:80


Ich verstehe leider nicht ganz, wie das sonst gehen sollte, ohne Condition.
Mit 2 Frontends auf port 443 habe ich es leider nicht geschafft..

Ich hätte es gerne folgendermaßen:
Zugriff auf cloud.meinedomain.com soll nur mittels von mir erstelltem Zertifikat möglich sein, für alle anderen Backends soll der Zugriff für jedermann möglich sein.