App Volumes hinter HAProxy: die Konfiguration, die läuft
Zwei App Volumes Manager, ein Name, eine IP. Mehr wollte der Kunde nicht. Fällt ein Manager aus, soll der andere übernehmen, und die Agents auf den virtuellen Desktops sollen davon nichts merken.
Omnissa (vormals VMware) beschreibt dafür einen Load Balancer vor den Managern. Beim Kunden stand kein NetScaler zur Verfügung, also haben wir zwei kleine Ubuntu-VMs mit HAProxy und keepalived davorgestellt. Das ist schnell gebaut. Bis es wirklich lief, hat es trotzdem drei Anläufe gebraucht. Hier steht die Konfiguration, die am Ende funktioniert, und die Stellen, an denen sie es vorher nicht tat.
Der Aufbau
- Zwei Manager, im Beispiel
avm01(10.0.0.38) undavm02(10.0.0.39). Beide sprechen HTTPS auf Port 443. - Zwei Load-Balancer-Knoten,
lb01(10.0.0.41) undlb02(10.0.0.40), Ubuntu 24.04 mit HAProxy 2.8 und keepalived. - Eine virtuelle IP, 10.0.0.22, auf die der DNS-Name
appvolumes.example.localzeigt. Sie liegt immer auf genau einem der beiden Knoten.
HAProxy terminiert TLS nicht. Er reicht die Verbindung durch (TLS-Passthrough), das Zertifikat liegt auf den Managern. Das hat zwei Gründe: Der Load Balancer braucht dann keinen privaten Schlüssel, und die Agents sehen genau das Zertifikat, das sie sonst auch sähen.
haproxy.cfg
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
daemon
defaults
log global
option dontlognull
timeout connect 5s
timeout client 1m
timeout server 1m
# Statistik nur lokal erreichbar
listen stats
bind 127.0.0.1:8404
mode http
stats enable
stats uri /stats
stats refresh 10s
# Port 80 leitet nur auf HTTPS um
frontend fe_avm_http
bind 10.0.0.22:80
mode http
option httplog
http-request redirect scheme https code 301
# TLS-Passthrough: das Zertifikat liegt auf den Managern
frontend fe_avm_https
bind 10.0.0.22:443
mode tcp
option tcplog
default_backend be_avm_https
backend be_avm_https
mode tcp
balance leastconn
# Ein Client bleibt beim selben Manager
stick-table type ip size 1m expire 200m
stick on src
# Health-Check über TLS: /health_check liefert nur 200, wenn der Manager gesund ist
option httpchk
http-check send meth HEAD uri /health_check
http-check expect status 200
timeout check 10s
server avm01 10.0.0.38:443 check check-ssl verify none inter 30s fastinter 5s rise 5 fall 2
server avm02 10.0.0.39:443 check check-ssl verify none inter 30s fastinter 5s rise 5 fall 2Was davon wichtig ist:
mode tcpim HTTPS-Frontend und -Backend. Sonst versucht HAProxy, HTTP zu lesen, wo verschlüsselte Daten kommen.stick on src. Ohne Sticky-Sessions springt ein Client zwischen den Managern hin und her. Für die Weboberfläche heißt das: Man wird abgemeldet. Die Persistenz läuft über die Client-IP, weil HAProxy bei Passthrough kein Cookie sehen kann.- Der Health-Check. Ein reiner TCP-Check meldet einen Manager als gesund, sobald Port 443 offen ist, auch wenn der Dienst dahinter hängt.
/health_checkist der Endpunkt, den Omnissa für App Volumes 4.x empfiehlt. Er antwortet nur mit 200, wenn der Manager arbeitet.check-sslschickt den Check über TLS,verify nonespart uns die CA auf dem Load Balancer, denn hier geht es um Erreichbarkeit, nicht um Vertrauen. rise 5 fall 2. Ein Manager fliegt nach zwei Fehlschlägen raus und kommt erst nach fünf erfolgreichen Checks zurück. Ein Manager, der beim Hochfahren kurz zuckt, bekommt so keinen Verkehr, bevor er wirklich bereit ist.
keepalived.conf
Die virtuelle IP wandert per VRRP zwischen den beiden Knoten. Wir nutzen Unicast statt Multicast, weil Multicast in virtualisierten Netzen gern irgendwo verloren geht.
global_defs {
router_id lb01
script_user root
enable_script_security
}
# Die VIP nur auf einem Knoten halten, auf dem HAProxy läuft
vrrp_script chk_haproxy {
script "/usr/bin/systemctl is-active --quiet haproxy"
interval 2
fall 2
rise 2
}
vrrp_instance VI_HAPROXY {
state BACKUP
interface ens192
virtual_router_id 22
priority 150 # lb02: 100
advert_int 1
unicast_src_ip 10.0.0.41 # lb02: 10.0.0.40
unicast_peer {
10.0.0.40 # lb02: 10.0.0.41
}
authentication {
auth_type PASS
auth_pass geheim12 # höchstens 8 Zeichen
}
virtual_ipaddress {
10.0.0.22/24 dev ens192
}
track_script {
chk_haproxy
}
}Beide Knoten starten als BACKUP, die höhere Priorität gewinnt. Die virtual_router_id muss im Netzsegment eindeutig sein. Das prüft niemand für einen, und zwei Cluster mit derselben ID streiten sich um die Adresse.
Dazu eine Zeile, ohne die HAProxy auf dem passiven Knoten gar nicht startet: Er soll an die VIP binden, die dort gerade nicht liegt.
# /etc/sysctl.d/60-haproxy.conf
net.ipv4.ip_nonlocal_bind = 1Erste Bruchstelle: alles grün, und niemand kommt an
Nach dem Ausrollen haben wir geprüft. Die VIP lag auf lb01, auf beiden Knoten waren die Frontends offen, avm01 war im Backend UP. Den Failover haben wir auch getestet: HAProxy auf lb01 gestoppt, die VIP sprang nach lb02, zurück ebenso. Alles grün.
Eine Woche später rief jemand die VIP im Browser auf. Nichts.
Die Statistikseite verriet es, wenn man genau hinsah: Die Frontends hatten seit dem Start null Verbindungen gezählt. HAProxy lauschte korrekt auf 10.0.0.22:443, das zeigte ss -tlnp. Die Pakete kamen nur nie bei ihm an.
$ sudo ufw status verbose
Status: active
Default: deny (incoming), allow (outgoing), disabled (routed)
To Action From
22/tcp (OpenSSH) ALLOW IN AnywhereDie Grundhärtung unserer Linux-Server schaltet ufw ein, Policy deny, nur SSH offen. Das ist richtig so. Nur hatte niemand daran gedacht, dass ein Load Balancer Ports braucht. Unser Test hatte das nicht gezeigt, weil er ausschließlich auf den Knoten lief: ip a, die Statistik über 127.0.0.1, der Failover. Von außen hatte niemand gefragt.
Die Regeln, die fehlten:
sudo ufw allow 80/tcp comment 'HAProxy HTTP'
sudo ufw allow 443/tcp comment 'HAProxy HTTPS'
# VRRP vom jeweils anderen Knoten (IP-Protokoll 112)
sudo ufw allow from 10.0.0.40 comment 'keepalived Partner' # auf lb01
sudo ufw allow from 10.0.0.41 comment 'keepalived Partner' # auf lb02Die letzten beiden sind die gefährlicheren. Blockt die Firewall VRRP, hört keiner der beiden Knoten den anderen. Jeder hält sich dann für den einzigen und nimmt die VIP. Zwei Rechner mit derselben IP im selben Netz: Welcher antwortet, hängt davon ab, wessen ARP-Antwort der Switch zuletzt gesehen hat.
Eigentlich würde man nur VRRP freigeben. ufw kann das auch (proto vrrp), das Ansible-Modul community.general.ufw in der Version, die wir einsetzen, kennt den Wert aber nicht. Wir erlauben deshalb alles vom Partnerknoten. Die beiden vertrauen sich ohnehin.
Seitdem gehört zum Test ein Aufruf von einem anderen Rechner:
curl -sk -o /dev/null -w '%{http_code}\n' https://10.0.0.22/health_checkKommt 200 zurück und steigt der Zähler im Frontend, kommt der Verkehr wirklich durch.
Zweite Bruchstelle: das Zertifikat liegt im falschen Speicher
Über die VIP kam jetzt etwas an, aber mit dem falschen Zertifikat. Die Manager lieferten ihr selbstsigniertes Standardzertifikat aus, ausgestellt auf <HOSTNAME>.WORKGROUP. Zum DNS-Namen der VIP passte das nicht.
Ein Zertifikat der internen CA hatten wir längst: Common Name appvolumes.example.local, im SAN zusätzlich beide Manager-Hostnamen, damit es auch beim direkten Zugriff passt. Es lag im Windows-Zertifikatsspeicher des Managers. Und genau dort nutzt es nichts.
Der App Volumes Manager liefert HTTPS nicht über IIS oder den Windows-Speicher aus, sondern über ein mitgeliefertes nginx. Und nginx liest PEM-Dateien aus seinem eigenen Verzeichnis:
C:\Program Files (x86)\CloudVolumes\Manager\nginx\conf\nginx.conf
ssl_certificate app_volumes_self.crt;
ssl_certificate_key app_volumes_self.key;Ältere Anleitungen nennen hier https.crt und https.key. In der Version, die wir vor uns hatten, heißen die Dateien anders. Erst nachsehen, dann tauschen.
Aus dem exportierten PFX werden zwei PEM-Dateien, am einfachsten auf einem Linux-Rechner:
openssl pkcs12 -in avm.pfx -clcerts -nokeys | sed -n '/BEGIN/,/END/p' > appvolumes.crt
openssl pkcs12 -in avm.pfx -cacerts -nokeys | sed -n '/BEGIN/,/END/p' >> appvolumes.crt
openssl pkcs12 -in avm.pfx -nocerts -nodes | sed -n '/BEGIN/,/END/p' > appvolumes.key
# Passen Schlüssel und Zertifikat zusammen? Beide Werte müssen gleich sein.
openssl x509 -in appvolumes.crt -noout -pubkey | sha256sum
openssl pkey -in appvolumes.key -pubout | sha256sumIn der .crt steht zuerst das Serverzertifikat, danach die CA. Der Schlüssel muss unverschlüsselt sein, nginx fragt beim Start nach keinem Passwort. Die erste Zeile lautet dann -----BEGIN PRIVATE KEY----- und nicht ENCRYPTED.
Beide Dateien kommen neben die vorhandenen. In der nginx.conf werden nur die zwei Zeilen umgestellt, eine Sicherung der alten Datei bleibt liegen. Danach den Dienst „App Volumes Manager“ neu starten.
Die Originaldateien zu überschreiben wäre kürzer gewesen. Wir wollten aber nicht darauf wetten, dass der Manager seine eigenen Dateien nicht irgendwann neu erzeugt. Ein Risiko bleibt trotzdem: Ein Update von App Volumes kann die nginx.conf zurücksetzen. Deshalb steht in der Dokumentation des Kunden, dass nach jedem Update genau diese zwei Zeilen zu prüfen sind.
Und hinterher aufräumen: Das PFX und der unverschlüsselte Schlüssel lagen während der Arbeit in zwei temporären Verzeichnissen. Dort haben sie nichts verloren.
Dritte Bruchstelle: der Test, der zu früh kam
Direkt nach dem Neustart des Dienstes haben wir getestet:
$ echo | openssl s_client -connect 10.0.0.22:443 -servername appvolumes.example.local 2>/dev/null \
| openssl x509 -noout -subject -issuer
Could not read certificate from <stdin>Kurz Panik. Dabei war nichts kaputt. nginx war nach dem Neustart noch nicht wieder oben, HAProxy hatte den Manager für ein paar Sekunden aus dem Backend genommen, und die VIP hatte niemanden, an den sie weiterreichen konnte. Ein paar Minuten später:
subject=CN = appvolumes.example.local
issuer=DC = local, DC = example, CN = EXAMPLE-CAGekostet hat uns das 2>/dev/null. Es hatte die eigentliche Fehlermeldung verschluckt, und ohne sie sah ein Timing-Problem aus wie ein kaputtes Zertifikat. Beim Diagnostizieren bleibt stderr seitdem stehen. Und neben jeden Test über die VIP gehört ein Blick auf den Zustand der Backends:
curl -s "http://127.0.0.1:8404/stats;csv" | cut -d, -f1,2,18 | grep avmWas wir jetzt immer prüfen
- Von außen testen, nicht nur auf dem Load Balancer. Ein Frontend, das null Verbindungen zählt, bekommt keine Pakete.
- Die Firewall der Knoten gehört zur Konfiguration des Load Balancers: 80/443 und VRRP zum Partner.
- Feste IP-Adressen für die Knoten. keepalived im Unicast-Betrieb kennt seinen Partner nur über dessen Adresse. Vergibt DHCP eine neue, ist der Cluster gespalten, ohne dass es jemand merkt. Bei uns ist genau das passiert, bevor wir beide Knoten per netplan fest eingestellt haben.
- Das Zertifikat dort hinterlegen, wo der Dienst es liest. Bei App Volumes ist das die
nginx.conf, nicht der Windows-Speicher. - Erst die Backends ansehen, dann die VIP. Ist das Backend DOWN, kann die VIP nichts ausliefern, egal wie gut sie konfiguriert ist.
