Zum Inhalt

TLS / Reverse-Proxy

Drei Pfade.

Pfad 1 — Selbstsigniert (Standard)

Ohne eigenes Zertifikat erzeugt der nginx-Container beim ersten Start ein selbstsigniertes Paar (10 Jahre, CN = Host aus BASE_URL) im Volume ssl. HTTPS funktioniert damit sofort, der Browser warnt aber. Vesana bringt keinen ACME-Client mit — ein Let's-Encrypt-Zertifikat kommt über Pfad 2 oder Pfad 3.

Pfad 2 — Eigenes Zertifikat ins Stack-Volume

Der Stack-nginx liest genau EIN Paar aus dem Volume ssl: /etc/nginx/ssl/fullchain.pem + privkey.pem. Es gilt für alle Hostnamen, die dieser nginx bedient — bei aktivem Split-Container-Modus also auch für den Admin-Namen (das Zertifikat muss dann beide Namen decken).

cd /opt/vesana   # dein Vesana-Verzeichnis

# Let's Encrypt mit certbot auf dem Host (Port 80 kurz freigeben):
docker compose -f docker-compose.prod.yml stop nginx
sudo certbot certonly --standalone -d vesana.example.com --agree-tos --non-interactive --email admin@example.com
docker compose -f docker-compose.prod.yml start nginx

# Paar ins Volume kopieren und nginx neu laden (gilt genauso für ein eigenes CA-Zertifikat):
docker compose -f docker-compose.prod.yml cp /etc/letsencrypt/live/vesana.example.com/fullchain.pem nginx:/etc/nginx/ssl/fullchain.pem
docker compose -f docker-compose.prod.yml cp /etc/letsencrypt/live/vesana.example.com/privkey.pem  nginx:/etc/nginx/ssl/privkey.pem
docker compose -f docker-compose.prod.yml exec nginx nginx -s reload

Cert-Format: PEM (X.509 mit Intermediate-Chain). Key: unverschlüsselt PEM. Bei Erneuerung dieselben zwei cp-Zeilen + reload — als certbot-Deploy-Hook (/etc/letsencrypt/renewal-hooks/deploy/vesana.sh) läuft das automatisch.

Pfad 3 — Reverse-Proxy davor

Wenn schon ein nginx / Traefik / HAProxy / Caddy vor dem Server steht und TLS dort terminiert wird:

In .env:

TRUST_PROXY=true
HTTP_PORT=8080      # statt 80
HTTPS_PORT=8443     # statt 443 — wir brauchen TLS hier nicht, aber Container will den Port

Im externen Reverse-Proxy:

server {
  listen 443 ssl http2;
  server_name vesana.example.com;

  ssl_certificate     /etc/letsencrypt/live/vesana.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/vesana.example.com/privkey.pem;

  location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host  $host;

    proxy_http_version 1.1;
    proxy_set_header   Upgrade $http_upgrade;
    proxy_set_header   Connection "upgrade";

    # Antwort-Header der Anmeldung (Set-Cookie mit Sitzungs-Token, der die
    # Berechtigungen der Rolle trägt) sind größer als der nginx-Standardpuffer
    # (4 KB). Ohne diese Zeilen: „upstream sent too big header" → 502 beim
    # Login normaler Benutzer (Super-Admins passen noch durch).
    proxy_buffer_size       32k;
    proxy_buffers           8 32k;
    proxy_busy_buffers_size 64k;
  }

  # Receiver-Endpoint für Agent / Collector
  location /receiver {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_request_buffering off;
    client_max_body_size 50m;       # für komprimierte Log-Pakete
  }
}

Wichtig: TRUST_PROXY=true setzen, sonst sieht das Backend nicht die echte Client-IP, sondern die des Reverse-Proxys — Audit-Log und Rate-Limit wären verfälscht.

Beispiel: nginx davor mit HTTPS zum Stack

So läuft app.vesana.org: ein nginx auf dem Host terminiert TLS und reicht per HTTPS an den Stack durch, der TLS terminiert und auf den Stack auf Ports 8180/8181 proxiert:

server {
  listen 443 ssl;
  server_name app.vesana.org;
  ssl_certificate     /etc/letsencrypt/live/app.vesana.org/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/app.vesana.org/privkey.pem;

  location / {
    proxy_pass https://127.0.0.1:8181;
    proxy_ssl_verify off;     # interner Stack hat self-signed
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_buffer_size 32k;
    proxy_buffers 8 32k;
    proxy_busy_buffers_size 64k;
  }
}

proxy_ssl_verify off ist akzeptabel für intern, weil der Traffic auf 127.0.0.1 nicht öffentlich.

CSP

Frontend setzt strikte CSP. Wenn du eigene Iframes oder eingebettete Inhalte willst, must die iframe_allowlist in system_settings ergänzen.

Header-Hygiene

Der eingebaute nginx setzt:

  • Strict-Transport-Security: max-age=31536000; includeSubDomains
  • X-Content-Type-Options: nosniff
  • X-Frame-Options: SAMEORIGIN
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy: geolocation=(), microphone=()

Bei eigenem Reverse-Proxy: gleichen Set einbauen.

TLS-Test

curl -I https://vesana.example.com
# Sollte HSTS-Header und 200 zurückgeben

Externe Werkzeuge:

  • SSL Labs Test für Cert-Chain und Cipher-Suite-Bewertung
  • testssl.sh für lokale Validierung

Ziel: A oder A+ bei SSL Labs.

Anschluss