Schritt 2: txAdmin hinter einem Apache-Reverse-Proxy

Das txAdmin-Panel über Apache 2.4 mit HTTPS auf einer eigenen Subdomain erreichbar machen, inklusive WebSocket-Weiche für die Live-Konsole und den Fallen, die dabei niemand bemerkt.

Das hier ist der zweite Teil der FiveM-Reihe. Nach Schritt 1 ist Port 40120 von außen gesperrt und das Panel nur über einen SSH-Tunnel erreichbar. Das ist sicher, aber unbequem, und für mehr als eine Person taugt es nicht.

Am Ende dieses Teils nimmt Apache die Anfragen unter einer eigenen Subdomain per HTTPS entgegen, die Live-Konsole bekommt eine echte WebSocket-Verbindung, und der Tunnel ist überflüssig. Port 40120 bleibt dabei geschlossen, das ist der ganze Punkt.

Voraussetzungen

  • ein laufender FXServer nach Schritt 1, das Panel ist lokal unter 127.0.0.1:40120 erreichbar und von außen gesperrt
  • Apache 2.4 mit funktionierendem HTTPS, entweder über Certbot oder über ein Wildcard-Zertifikat
  • eine Subdomain, die auf den Server zeigt, im Beispiel tx.example.de

Die Apache-Version ist wichtiger als sie aussieht. Ab 2.4.47 gibt es die Option upgrade=websocket an ProxyPass, und damit wird die WebSocket-Weiche zu einer einzigen Zeile. Darunter braucht es den Umweg über mod_rewrite, beide Varianten stehen unten.

apache2 -v

Schritt 2.1: Apache-Module aktivieren

a2enmod ssl proxy proxy_http proxy_wstunnel headers rewrite
systemctl restart apache2

proxy_wstunnel wird auch dann gebraucht, wenn Du die Variante mit upgrade=websocket nimmst. Die Option an ProxyPass stellt nur die Weiche, das Tunneln selbst macht weiterhin dieses Modul.

Schritt 2.2: vHost anlegen

Die Konfiguration unten gibt es als kommentierte Vorlage:

curl -fsSL https://uploads.musiker15.de/fivem/txadmin-vhost.conf \
  -o /etc/apache2/sites-available/tx.example.de.conf
nano /etc/apache2/sites-available/tx.example.de.conf
<VirtualHost *:80>
    ServerName tx.example.de
    Redirect permanent / https://tx.example.de/
</VirtualHost>
 
<VirtualHost *:443>
    ServerName   tx.example.de
    ServerAdmin  info@example.de
 
    SSLEngine on
    SSLCertificateFile    /etc/apache2/ssl/example.de/fullchain.cer
    SSLCertificateKeyFile /etc/apache2/ssl/example.de/example.de.key
 
    # Security-Header
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Content-Type-Options       "nosniff"
    Header always set X-Frame-Options              "SAMEORIGIN"
    Header always set Referrer-Policy              "strict-origin-when-cross-origin"
    Header always set Cross-Origin-Opener-Policy   "same-origin"
    Header always set Cross-Origin-Resource-Policy "same-origin"
 
    # Reverse Proxy inklusive WebSocket-Upgrade
    ProxyPreserveHost On
    ProxyTimeout      600
 
    ProxyPass        / http://127.0.0.1:40120/ upgrade=websocket
    ProxyPassReverse / http://127.0.0.1:40120/
 
    RequestHeader set X-Forwarded-Proto "https"
</VirtualHost>
a2ensite tx.example.de.conf
apachectl -t && systemctl reload apache2

Wenn Apache älter als 2.4.47 ist

Dann kennt ProxyPass die Option upgrade=websocket nicht und quittiert mit Unknown parameter. Stattdessen übernimmt mod_rewrite die Weiche, und zwar vor dem ProxyPass:

    RewriteEngine On
    RewriteCond %{HTTP:Upgrade} =websocket [NC]
    RewriteRule ^/?(.*) ws://127.0.0.1:40120/$1 [P,L]
 
    ProxyPass        / http://127.0.0.1:40120/
    ProxyPassReverse / http://127.0.0.1:40120/

Was der Live-Konsole sonst noch das Genick bricht

ProxyTimeout 600 ist kein Schmuck. Ohne die Zeile gilt der globale Timeout, also 60 Sekunden. Eine Live-Konsole, auf der eine Minute lang nichts passiert, zählt damit als tote Verbindung und wird gekappt. Sichtbar wird das als Panel, das sich alle paar Minuten neu verbindet und dabei den Konsolenpuffer verliert.

Schritt 2.3: Zwei Fallen in typischen Konfigurationen

Beides sieht man häufig, beides funktioniert scheinbar, und beides ist falsch.

X-Forwarded-For nicht von Hand setzen

# Nicht so:
RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}s"

mod_proxy_http setzt X-Forwarded-For, X-Forwarded-Host und X-Forwarded-Server von allein. Die RequestHeader-Zeile läuft davor, danach hängt das Modul die Client-IP an den vorhandenen Wert an. Beim Backend kommt dann 1.2.3.4, 1.2.3.4 an. Für txAdmin heißt das: die IP in den Logs und in der Ban-Ansicht ist unbrauchbar, und Ratenbegrenzungen greifen an der falschen Stelle.

X-Forwarded-Proto setzt das Modul dagegen nicht, diese Zeile bleibt also stehen.

Die CSP muss zur eigenen Domain passen

Der Wert connect-src entscheidet, wohin der Browser die WebSocket-Verbindung aufbauen darf. Steht dort eine Domain aus einer alten Konfiguration, blockiert der Browser die Verbindung und die Live-Konsole bleibt leer.

# Aus einer anderen Installation uebernommen, passt hier nicht:
connect-src 'self' wss://irgendeine-andere-domain.de;

Brauchst Du trotzdem eine eigene CSP, muss connect-src die eigene Subdomain nennen:

Header always set Content-Security-Policy "connect-src 'self' wss://tx.example.de; frame-ancestors 'self';"

Schritt 2.4: txAdmin die eigene Adresse mitteilen

Unter Settings → General die txAdmin-URL auf https://tx.example.de setzen. Aus dieser Adresse baut txAdmin die Links, die es an Discord und in Einladungen verschickt. Ohne die Angabe stehen dort http://<server-ip>:40120-Adressen, die von außen nicht erreichbar sind.

Ab jetzt ist der SSH-Tunnel aus Schritt 1 überflüssig. Die ufw-Regel deny 40120/tcp bleibt selbstverständlich stehen.

Prüfen

# Ist der Port von aussen dicht (von einem anderen Rechner aus)
curl -m 5 http://<server-ip>:40120/ ; echo "exit: $?"
 
# Antwortet der Proxy
curl -sI https://tx.example.de/ | head -1

Den WebSocket-Upgrade misst man im Browser, nicht mit curl

Richtig ist der echte Browser. Panel öffnen, Entwicklerwerkzeuge, Netzwerk, Filter WS. Dort muss eine Verbindung mit Status 101 stehen. Oder in der Konsole:

new WebSocket("wss://tx.example.de/socket.io/?EIO=4&transport=websocket").onopen = () =>
  console.log("Upgrade steht");

Bleibt es beim Polling, siehst Du im Netzwerk-Tab stattdessen alle paar Sekunden neue transport=polling-Anfragen.

Wenn der Upgrade nicht durchgeht

upgrade=websocket ist die Standardantwort, und sie reicht nicht immer. Auf einem meiner Server blieb es trotz upgrade=websocket, upgrade=ANY, mod_proxy_wstunnel mit Rewrite-Weiche und abgeschaltetem h2c bei Status 400.

Die Messung, die dort weitergeholfen hat, ist der Vergleich der Antwortgröße. Das Backend antwortet auf denselben Handshake mit 34 Bytes, durch den Proxy kamen 1414. Der 400 stammte also von Apache selbst, nicht von txAdmin:

# Direkt am Backend, auf dem Server
curl -s -o /dev/null -w '%{size_download}\n' \
  "http://127.0.0.1:40120/socket.io/?EIO=4&transport=websocket"
 
# Durch den Proxy
curl -s -o /dev/null -w '%{size_download}\n' \
  "https://tx.example.de/socket.io/?EIO=4&transport=websocket"

Regel daraus: liefern Proxy und Backend denselben Statuscode, ist der Statuscode kein Beweis. Erst Größe oder Rumpf sagen, wer geantwortet hat. Wer weitersucht, beginnt bei LogLevel alert proxy:trace3.

Häufige Fehler

Meldung oder SymptomUrsacheLösung
503 Service UnavailableFXServer läuft nichtsystemctl status fivem, ss -tlnp
Panel lädt, Live-Konsole nur traege (Polling)WebSocket-Upgrade kommt nicht durchWeiche aus Schritt 2.2 prüfen, 101 erwarten
Panel verbindet sich alle paar Minuten neuProxyTimeout zu kurzProxyTimeout 600 setzen
CSP-Verletzung zu wss:// in der Browser-Konsolefalsche Domain in connect-srceigene Subdomain eintragen oder CSP weglassen
Alle Zugriffe kommen mit derselben IP anX-Forwarded-For doppelt gesetztmanuelle RequestHeader-Zeile entfernen
Unknown parameter: upgrade=websocketApache älter als 2.4.47Rewrite-Variante aus Schritt 2.2 nehmen
Zertifikatswarnung mit fremder Domainkein passender :443-vHost, SNI-FallbackServerName prüfen, apache2ctl -S
Discord-Links zeigen auf :40120txAdmin-URL nicht gesetztsiehe Schritt 2.4

Weiter mit Schritt 3

Der Server läuft, das Panel ist sicher erreichbar. Bleibt die Frage, wer die Artefakte aktuell hält. Der letzte Teil automatisiert das:

Schritt 3: Artefakte automatisch aktualisieren