Samstag, 19. April 2025

Reverse Proxy

Eine IP Adresse, mehrere Webdienste und alle verwenden die Ports 80 und 443. Ein Reverse Proxy kann z.B. anhand von der URL verschiedene Webdienste an "einem Anschluss" zur Verfügung stellen.

Ausgangspunkt bei mir: ich habe einen Webdienst, der funktioniert und soll vorerst so verfügbar bleiben. Ich möchte aber noch ein paar Dinge dazu schalten. Der HAProxy  ist wie der Name sagt: als Proxy entwickelt und nicht wie Apache oder nginx ein Webserver der auch Proxy kann. Für mich war es komplettes Neuland, ich will hier die gesamte Konfiguration beschreiben.

Mein Betriebssystem ist ein debian 12 LXC, die Installation von HAProxy ist erstmal simpel...

apt update
apt install haproxy

... es wird noch etwas aufwendig.

DNS und Router

Ich habe eine gehosteten Webauftritt domain.tld, dort hatte ich schon einen cname Eintrag auf cloud.domain.cloudns.cl. Der existierende Webdienst hat per cronjob die IPv4 und IPv6 Adresse diese Dienstes dort aktualisiert.

Achtung! Die dyndns Registrierung durch die Fritzbox ist für IPv6 an dieser Stelle unbrauchbar, bei IPv6 wird kein NAT gemacht, man braucht die IPv6 Adresse des Webdienstes.

Mit diesem Setup ändert sich das für den existierenden Webdienst: Der Proxy übersetzt die IP Adressen (v4 und v6), d.h. die Registrierung der IPv6 Adresse muss in Zukunft durch das OS vom HAProxy erfolgen. Der HAProxy soll der Endpunkt für alle Dienste sein und übersetzt intern.

Um flexibel zu sein, habe ich noch einen cname Eintrag *.home.domain.tld auf cloud.domain.cloudns.cl erzeugt. Damit kann man später cloud.home.domain.tld oder dienst1.home.domain.tld usw. verwenden ohne am DNS neu konfigurieren zu müssen.

Die alte Portfreigabe im Router muss, wenn der Proxy läuft, durch eine neue Freigabe ersetzt werden!

Test des HAProxy

Die Proxy Funktionalität wird durch frontend | backend Abschnitte konfiguriert. Die Abschnitte werden ähnlich wie Firewallregeln von oben nach unten abgearbeitet. Eine umfangreiche Doku gibt es hier.

Die Datei /etc/haproxy/haproxy.cfg wird um die hier gezeigten Abschnitte ergänzt!

Ein schneller Test kann mit dieser kurzen Konfiguration erfolgen:

frontend test-front
  bind [::]:80
  default_backend test-back

backend test-back
  server localhost 0.0.0.0:8000

Nach dem restart des haproxy ( systemctl restart haproxy ) erzeugt man eine Webservice mit python:

python3 -m http.server --bind ::

Sowohl der Browser Zugriff  über http://ip_des_proxy:8000 als auch http://ip_des_proxy sollte das aktuelle Directory listen.

Für den folgenden Abschnitt muss diese Testkonfiguration wieder gelöscht werden.

Man kann die Funktion des Proxy relativ umfangreich und im Detail testen, ohne dabei auf die DNS Auflösung angewiesen zu sein. Viele Beispiele findet man hier. Man kann nach dem folgenden Abschnitt erstmal testen, bevor man den Webdienst hinter den Proxy stellt.

HAProxy Konfiguration

Die 3 Frontend Definitionen sollen mehrere Funktionen abdecken, die eigentlich etwas konträr sind.

  • HTTP mit Erkennung der acme_challenge für die Ausstellung und Erneuerung von letsencrypt Zertifikaten und Weiterleitung auf HTTPS
  • SSL Passthrough (mode tcp) für Webdienste mit eigener SSL Terminierung.
  • SSL Terminierung (mode http) am Proxy für alle weiteren Webdienste

Ich habe hier Konfigurationen aus Beiträgen des Users oezh , Paul und C.Rieger eingearbeitet. Alle haben mir sehr beim schrittweisen Verständnis von HAProxy geholfen!

Die Abschnitte verwenden ganz bewusst unterschiedliche (Syntax) Möglichkeiten von HAProxy zum Erkennen und Verzweigen: 

  • acl setzt eine Zustandsvariable auf true oder false, 
  • redirect verzweigt wenn die Bedingung erfüllt ist, 
  • use_ verzweigt zu einem backend wenn die Bedingung wahr ist, 
  • default_ ist quasi der letzte Ausweg.

Soll ein neues SSL Passthrough Backend installiert werden, muss die passende Proxy Konfiguration vorher erweitert und aktiviert werden um die initiale Installation der Zertifikate zu gewährleisten! 

Wahrscheinlich fehlt hier noch etwas und muss im Laufe des Betriebes noch ergänzt werden.

frontend HTTP_ACME
	mode            http
	bind            [::]:80 v4v6
	maxconn         200
	acl             acmerequest path_beg /.well-known/acme-challenge/
	acl             backend1_host hdr(host) -i cloud.domain.tld
	acl             backend2_host hdr(host) -i nc.home.domain.tld
	acl             backend3_host hdr(host) -i opencloud.domain.org opencloud-collabora.domain.org opencloud-wopiserver.domain.org
	redirect        scheme https code 301 if !acmerequest
	use_backend     ACME_backend1 if acmerequest backend1_host
	use_backend     ACME_backend2 if acmerequest backend2_host
#	use_backend     ACME_backend3 if acmerequest backend3_host
	default_backend HTTP_certbot

frontend SSL_PassThrough
	mode            tcp
	bind            [::]:443 v4v6
	tcp-request     inspect-delay 5s
	tcp-request     content accept if { req_ssl_hello_type 1 }
	  # ssl passthrough backends
	use_backend     backend1 if { req_ssl_sni -i cloud.domain.tld }
	use_backend     backend2 if { req_ssl_sni -i nc.home.domain.tld }
	  # redirect for SSL termination
	default_backend tcp_to_https

backend tcp_to_https
	mode            tcp
	server          haproxy-https 127.0.0.1:44443 check

frontend SSL_Termination
	mode            http
	bind            127.0.0.1:44443 ssl crt /usr/local/ssl/certs/sites
	  #ssl crt /etc/haproxy/certs/ggg.hhh.com.pem crt /etc/haproxy/certs/iii.kkk.com.pem
	use_backend     backend4 if { hdr(host) -i home.domain.tld }
	use_backend     backend5 if { hdr(host) -i test.home.domain.tld }

# SSL Passthrough backends (every manage their own SSL termination)
backend backend1
	mode            tcp
	server          server1 192.168.90.160:443 check
backend backend2
	mode            tcp
	server          server2 192.168.90.90:443 check

# SSL Terminated by HAProxy Backends (plain http traffic between HAProxy and these backends)
backend backend4
	mode            http
	server          server4 192.168.90.75:8000 check
	http-request    set-header X-Forwarded-Port %[dst_port]
	http-request    add-header X-Forwarded-Proto https if { ssl_fc }

backend backend5
	mode            http
	server          server5 192.168.90.75:8000 check
	http-request    set-header X-Forwarded-Port %[dst_port]
	http-request    add-header X-Forwarded-Proto https if { ssl_fc }

# ACME / certbot backends for acme-challenge
backend ACME_backend1
	mode            http
	fullconn        100
	balance         source
	server          nextcloud 192.168.90.160:80 check maxconn 100

backend ACME_backend2
	mode            http
	fullconn        100
	balance         source
	server          nextcloud 192.168.90.90:80 check maxconn 100

backend HTTP_certbot
	mode            http
	log             global
	# this server only runs when we are renewing a certificate
	server          localhost 0.0.0.0:54321

Die Anzahl Backends für SSL PassThrough bzw. Termination kann man quasi beliebig erweitern: im Abschnitt frontend muss die Zeile use_backend ... und die dazu passenden backend Abschnitte kopiert und angepasst werden.

Letsencrypt Zertifikate mit certbot 

Ich wollte in dem debian System nicht extra snap installieren, deshalb habe ich mich für die pip Methode entschieden. Hier ist die offizielle Doku dazu und die Kurzform der Befehle.

apt update
apt install python3 python3-venv libaugeas0
python3 -m venv /opt/certbot/
/opt/certbot/bin/pip install --upgrade pip
/opt/certbot/bin/pip install certbot
ln -s /opt/certbot/bin/certbot /usr/bin/certbot

Um die Vorschläge von hier zu verwenden, habe ich die beiden Scripts auf meinem Github abgelegt. Das Script certbot-new erzeugt Zertifikate im standalone Modus und startet für die challenge response temporär einen Webservice auf Port 54321. Das hook Script kombiniert beide Zertifikate in die notwendige Form für HAProxy.

wget -N -P /usr/bin/  https://raw.githubusercontent.com/heinz-otto/scripts/master/Bash/{letsencrypt-reload-hook,certbot-new}
chmod +x /usr/bin/{letsencrypt-reload-hook,certbot-new}

Damit ist die Ausstellung und Erneuerung der Zertifikate angepasst an die Proxy Konfiguration simpel

certbot-new -d sub1.home.domain.tld

Die Erzeugung eines neuen Zertifikates erzeugt eine conf Datei im Pfad /etc/letsencrypt/renewal/ damit werden alle Parameter dort gespeichert, beim renew muss nichts angegeben werden. 

Die Erneuerung der Zertifikate muss noch automatisiert werden. Ich werde dafür einen timer und einen service erzeugen.

/usr/bin/certbot renew --dry-run

Die Service Unit wird nur vom Timer verwendet und selbst nicht aktiviert.

cat <<EOISERV |sudo SYSTEMD_EDITOR=tee systemctl edit --full --force certbot.service
[Unit]
Description=Certbot
Documentation=file:///usr/share/doc/python-certbot-doc/html/index.html
Documentation=https://certbot.eff.org/docs
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot -q renew
PrivateTmp=true
EOISERV

Der Timer wird aktiviert und startet zweimal am Tag zu zufälligen Zeitpunkten.

cat <<EOISERV |sudo SYSTEMD_EDITOR=tee systemctl edit --full --force certbot.timer
[Unit]
Description=Run certbot twice daily

[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=43200
Persistent=true

[Install]
WantedBy=timers.target
EOISERV
systemctl enable --now certbot.timer


Fehlt etwas?

  • Authentication
  • Websocket


Es gibt eine ausführliche Doku.

Nextcloud hinter HAProxy mit SSL Passthrough (Layer4) link.

ToDo

Code


Keine Kommentare:

Kommentar veröffentlichen