Zum Inhalt springen

Reverse-Proxy & Absicherung von außen

LCM spricht ab Werk HTTPS mit einem selbstsignierten Zertifikat und bindet die Weboberfläche an 127.0.0.1. Für eine Installation, die über das eigene Netz hinaus erreichbar ist, gehört ein Reverse-Proxy davor - er terminiert TLS mit einem echten Zertifikat, bremst Anmeldeversuche über alle Quellen hinweg und begrenzt, was überhaupt bis zu LCM durchkommt.

Diese Seite behandelt den UI/REST-Port (9310). Der Agent-Port hat eigene Anforderungen (dauerhafte WebSocket-Verbindung, Zertifikats-Pin) und eine eigene Seite: LCM Remote → Hinter einem Reverse-Proxy.

Der Proxy ergänzt, er ersetzt nichts. Unabhängig von ihm gilt:

Schutzin LCM
Anmelde-Bremse5 Fehlversuche je Client-IP, 15 je Konto, wachsende Sperre bis 15 min; TOTP-Codes gelten genau einmal
Rumpf-Budget1 MiB je Anfrage, 64 MiB nur für den Backup-Upload - geprüft an der Kopfzeile, bevor der Rumpf gelesen wird
Passwortlänge beim Loginüber 1024 Bytes wird gar nicht erst gehasht
Sicherheits-HeaderCSP, HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy, Cache-Control: no-store auf der API
Sicherheitsprotokolleine Journal-Zeile security event=… je Anmeldung, Sperre, Passwort-, 2FA- und Schlüssel-Änderung

Was nur der Proxy leisten kann: ein gültiges Zertifikat, eine Bremse, die alle Clients auf einmal sieht, und eine Grenze, die einen Angreifer beschäftigt, bevor er den Go-Prozess überhaupt erreicht.

trusted_proxies: wem X-Forwarded-For geglaubt wird

Abschnitt betitelt „trusted_proxies: wem X-Forwarded-For geglaubt wird“

Hinter einem Proxy sieht LCM als direkte Gegenstelle nur den Proxy. Damit IP-Allowlist, Anmeldesperre und Protokolle den echten Client sehen, schreibt der Proxy X-Forwarded-For, und LCM liest es - aber nur von Adressen, die in trusted_proxies stehen:

{
"trust_proxy_header": true,
"trusted_proxies": ["127.0.0.1", "::1"]
}

Einträge sind IP-Adressen, CIDR-Bereiche oder die Schlüsselwörter localhost/private. Kommt eine Anfrage von einer anderen Adresse, zählt die Kopfzeile nicht - die Peer-Adresse gilt.

Nach der Änderung Dienst neu starten. Prüfen: Fehlversuche im Journal tragen die Client-Adresse, nicht die des Proxys:

Terminal-Fenster
journalctl -u lcm -p warning | grep 'security'

Die vollständige Referenzkonfiguration liegt im Repository unter packaging/nginx/lcm.conf (samt lcm-proxy.conf-Schnipsel): TLS, beide Bremsen, Rumpf- und Zeitgrenzen je Pfad, Health und Agent-Download nur aus dem eigenen Netz. Das Folgende ist die gekürzte Fassung.

# Anmelde-Bremse: 10 Anfragen pro Minute und Quelle auf den Anmelde-Pfaden -
# zusätzlich zur Bremse in LCM, die nur je Prozess zählt.
limit_req_zone $binary_remote_addr zone=lcm_auth:10m rate=10r/m;
server {
listen 443 ssl;
http2 on;
server_name lcm.example.com;
ssl_certificate /etc/letsencrypt/live/lcm.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/lcm.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# LCM sendet HSTS selbst; der Proxy reicht die Kopfzeilen nur durch.
# Vorgabe für alles: kleine Rümpfe. LCM lehnt darüber ohnehin ab -
# hier endet der Versuch schon am Proxy.
client_max_body_size 1m;
# Anmeldung, zweiter Faktor, Passwort-Reset, Aktivierungslinks.
location ~ ^/api/v1/(auth/|users/activation-links/consume) {
limit_req zone=lcm_auth burst=10 nodelay;
proxy_pass https://127.0.0.1:9310;
include /etc/nginx/lcm-proxy.conf;
}
# Backup-Upload: der einzige große Rumpf.
location = /api/v1/system/backups/restore-upload {
client_max_body_size 64m;
proxy_request_buffering off;
proxy_pass https://127.0.0.1:9310;
include /etc/nginx/lcm-proxy.conf;
}
# Web-Konsole (WebSocket) - die Sitzung bleibt offen.
location ~ ^/api/v1/servers/[0-9]+/terminal$ {
proxy_pass https://127.0.0.1:9310;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
include /etc/nginx/lcm-proxy.conf;
}
# Live-Protokoll (Server-Sent Events) - nicht puffern.
location = /api/v1/system/logs/stream {
proxy_pass https://127.0.0.1:9310;
proxy_buffering off;
proxy_read_timeout 3600s;
include /etc/nginx/lcm-proxy.conf;
}
location / {
proxy_pass https://127.0.0.1:9310;
include /etc/nginx/lcm-proxy.conf;
}
}

/etc/nginx/lcm-proxy.conf - die Kopfzeilen, die jede Weiterleitung braucht:

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;
# LCM intern mit Selbstsignat: die Strecke Proxy → LCM läuft über Loopback.
proxy_ssl_verify off;

Dazu in LCM: "trusted_proxies": ["127.0.0.1"] und public_base_url auf https://lcm.example.com.

lcm.example.com {
reverse_proxy https://127.0.0.1:9310 {
transport http {
tls_insecure_skip_verify # nur bei internem Selbstsignat
}
}
request_body {
max_size 64MB
}
}

Caddy setzt X-Forwarded-For selbst und erkennt WebSocket und SSE ohne Sonderbehandlung. Eine Anfragen-Bremse gibt es in Caddy nur als Erweiterung (caddy-ratelimit); wer sie nicht einbaut, verlässt sich auf die Bremse in LCM.

  • Bind belassen. host bleibt 127.0.0.1, der Proxy läuft auf demselben Rechner. Muss LCM auf einer Netzschnittstelle lauschen, gehört eine Host-Firewall davor, die Port 9310 nur vom Proxy zulässt - sonst greift trusted_proxies als zweite Linie.
  • IP-Allowlist zusätzlich. Erreichen nur bekannte Netze die Oberfläche (Büro, VPN), sperrt allowed_ips alle anderen, bevor die Anmeldung überhaupt antwortet - siehe IP-Allowlist.
  • fail2ban kann das Sicherheitsprotokoll lesen. Muster für die Anmeldefehler: security event=login.failed ip=<HOST>, Journal-Einheit lcm. Die Adresse ist die des Clients, sobald trusted_proxies stimmt.
  • Journal weiterleiten. Das Audit-Log liegt in der Datenbank auf demselben Rechner. Wer den Host übernimmt, kann es ändern - eine Kopie des Journals auf einem anderen System (Syslog, SIEM) ist der Nachweis, der bleibt. LCM schreibt jeden Audit-Eintrag zusätzlich als Journal-Zeile audit … und jedes Anmelde-/Rechte-Ereignis als security …; siehe unten.
  • Kein Skript-Injizieren. Die Content-Security-Policy erlaubt nur eigene Skripte. Ein Proxy, der Analytics oder Banner einfügt, wird vom Browser blockiert - wer das braucht, ersetzt die Kopfzeile bewusst und weiß, welchen Schutz er aufgibt.

Alles, was LCM protokolliert, landet im Journal der Unit lcm: die Sicherheitsereignisse (security event=…), der Audit-Spiegel (audit action=…), das Zugriffsprotokoll und die Dienstmeldungen. Mit rsyslog gehen genau diese Zeilen an einen zweiten Rechner - über TLS, damit unterwegs niemand mitliest oder fälscht:

/etc/rsyslog.d/50-lcm-forward.conf
module(load="imjournal" StateFile="imjournal.state")
module(load="omfwd")
global(DefaultNetstreamDriver="gtls"
DefaultNetstreamDriverCAFile="/etc/ssl/certs/ca-certificates.crt")
# Nur die Zeilen der LCM-Unit; Reihenfolge und Zeitstempel bleiben erhalten.
if ($programname == "lcm") then {
action(type="omfwd" target="syslog.example.com" port="6514" protocol="tcp"
StreamDriver="gtls" StreamDriverMode="1" StreamDriverAuthMode="x509/name"
StreamDriverPermittedPeers="syslog.example.com"
queue.type="LinkedList" queue.filename="lcm-fwd" queue.saveOnShutdown="on"
action.resumeRetryCount="-1")
stop
}

Danach apt install rsyslog rsyslog-gnutls, systemctl restart rsyslog und auf der Gegenseite prüfen, ob security event=login.ok nach einer Anmeldung ankommt. Die Warteschlange auf der Platte sorgt dafür, dass ein Ausfall der Gegenstelle keine Zeile verliert. Ohne eigenen Syslog-Server tut es auch systemd-journal-upload zu einem systemd-journal-remote - dann bleibt es das Journal-Format, mit denselben Feldern.