Eine öffentliche URL, und alles andere auf localhost
Drei Anwendungen, ein Eingang. Wie ich Pfade auf interne Dienste verteile, warum kein einziger Anwendungsport nach außen zeigt und welche Kopfzeilen dabei nicht fehlen dürfen.
959 Wörter · 5 Min. Lesezeit
- sicherheit
- infrastruktur
- nginx
Mit drei Anwendungen stellte sich die Frage, wie sie von außen erreichbar sind. Die naheliegende Antwort — jede auf einem eigenen Port — habe ich verworfen. Der Aufbau bei mir hat genau einen öffentlichen Eingang.
Der Grundsatz
Internet
│
▼
nginx (Ports 80 und 443, einzige offene Türen)
│
├── /admin/api/v2/* ──► 127.0.0.1:9110 Verwaltung
└── /* ──► 127.0.0.1:8059 Dashboard
Bot: kein eingehender Port
Die Firewall erlaubt eingehend nur SSH und den Webserver. Die Anwendungsports sind nicht offen — und zwar nicht nur „nicht freigegeben", sondern die Anwendungen lauschen ausdrücklich auf der lokalen Adresse.
Das ist doppelt gemoppelt, und das ist Absicht. Eine falsch gesetzte Firewallregel oder eine Anwendung, die an alle Adressen bindet, reicht sonst aus, um eine Verwaltungsschnittstelle ins Internet zu stellen.
Warum ein Eingang besser ist als drei
Ein Zertifikat. Eine Domain, eine Erneuerung, eine Stelle, an der etwas ablaufen kann.
Eine Stelle für Sicherheitskopfzeilen. Alles, was für jede Antwort gelten soll, steht einmal im Proxy statt dreimal in Anwendungen.
Eine Stelle für Zugriffsprotokolle. Wer wann was aufgerufen hat, steht in einer Datei — Grundlage für jede Analyse und für automatische Sperren bei auffälligem Verhalten.
Weniger Angriffsfläche. Jeder offene Port ist ein Angebot. Was nur lokal lauscht, ist von außen nicht ansprechbar, egal welche Schwachstelle die Anwendung hat.
Keine Aussage über die Struktur. Wer die Seite aufruft, sieht Pfade, keine Portnummern. Dass dahinter drei Anwendungen liegen, geht niemanden etwas an.
Die Konfiguration, mit Erklärung
server {
listen 443 ssl http2;
server_name beispiel.de;
ssl_certificate /etc/letsencrypt/live/beispiel.de/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/beispiel.de/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "no-referrer" always;
client_max_body_size 25m;
location /admin/api/v2/ {
proxy_pass http://127.0.0.1:9110;
include /etc/nginx/snippets/proxy-header.conf;
}
location / {
proxy_pass http://127.0.0.1:8059;
include /etc/nginx/snippets/proxy-header.conf;
}
}Drei Details, die ich mir erarbeiten musste:
Das always bei den Kopfzeilen. Ohne dieses Schlüsselwort werden sie bei Fehlerantworten nicht gesetzt. Ausgerechnet auf Fehlerseiten fehlen sie dann.
Die Reihenfolge der Pfade. Der spezifischere Eintrag muss vor dem allgemeinen stehen, sonst schluckt der allgemeine alles.
Die gemeinsamen Kopfzeilen in einer eingebundenen Datei. Bei zwei Zielen ist das Bequemlichkeit. Bei zehn ist es der Unterschied zwischen konsistent und „bei einem fehlt etwas".
Der Fehler, der mich eine Stunde gekostet hat
Nach dem Umbau funktionierte die Anmeldung nicht mehr. Der Nutzer landete nach der Bestätigung bei Discord auf einer Fehlerseite.
Ursache: Die Anwendung sah die interne Adresse und baute daraus die Rückkehradresse. Bei Discord hinterlegt war die öffentliche. Beide müssen exakt übereinstimmen.
Die Lösung besteht aus zwei Teilen — die richtigen Weiterleitungskopfzeilen im Proxy, und in der Anwendung die ausdrückliche Angabe der öffentlichen Adresse, statt sie zu erraten. Seitdem ist die öffentliche Adresse bei mir immer eine Konfiguration und nie eine Ableitung aus der Anfrage.
Warnung
Wer aus einer Anfrage die eigene Adresse ableitet, verlässt sich auf einen Wert, den der Aufrufer bestimmt. Das ist nicht nur fehleranfällig, sondern in bestimmten Konstellationen ausnutzbar: Ein manipulierter Host-Kopf kann dazu führen, dass eine Anwendung Links auf eine fremde Domain baut — etwa in einer E-Mail zum Zurücksetzen eines Passworts. Die eigene öffentliche Adresse gehört in die Konfiguration.
Die Prüfung von außen, die dazugehört
Der Aufbau ist nur so gut wie die Kontrolle, dass er tatsächlich so ist. Deshalb gehört zu jeder Änderung an Diensten oder Firewall eine Prüfung — und zwar von einem anderen Anschluss aus.
Welche Ports antworten von außen? Ein Scan über die üblichen Bereiche, verglichen mit einer Liste dessen, was offen sein soll.
Welche Namen zeigen auf diese Maschine? Bei einem Platzhaltereintrag in der Namensauflösung sind das potenziell beliebig viele — und jeder davon landet beim Proxy. Was passiert bei einem Namen, für den es keinen Eintrag gibt?
Antwortet der Dienst auch direkt? Wenn eine Anwendung an alle Adressen bindet statt nur lokal, ist sie unter Umständen unter der Serveradresse mit Port erreichbar — an Zertifikat und Kopfzeilen vorbei.
Der letzte Punkt hat mich zweimal erwischt. Beide Male lag es nicht an der Firewall, sondern an einer Anwendung, deren Standardeinstellung „an alle Adressen binden" ist.
Was ich beim Erweitern gelernt habe
Als weitere Dienste dazukamen, hat sich der Aufbau von Pfaden auf Unterdomains verschoben, und dabei sind zwei Dinge wichtig geworden.
Ein Platzhalterzertifikat spart Verwaltung, verlangt aber eine andere Art der Überprüfung bei der Ausstellung — über Einträge in der Namensauflösung statt über eine Datei auf dem Webserver. Das ist einmalig einzurichten und danach bequemer.
Jede Unterdomain braucht eine bewusste Entscheidung über Erreichbarkeit. Nicht alles, was einen Namen hat, gehört ins offene Internet. Für Verwaltungsoberflächen gilt bei mir eine Zugriffsbeschränkung auf bekannte Anschlüsse — und die ist wichtiger als jedes Passwort, weil sie greift, bevor die Anwendung überhaupt gefragt wird.
Was bei einer zweiten Domain dazukommt
Als später weitere Dienste dazukamen, hat sich der Aufbau von Pfaden auf eigene Namen verschoben. Drei Punkte, die dabei anfallen und die man vorher wissen sollte.
Zertifikate für mehrere Namen. Entweder eines pro Name oder ein Platzhalterzertifikat für alles darunter. Das zweite ist bequemer und verlangt eine andere Art der Überprüfung bei der Ausstellung.
Namensauflösung mit Platzhalter. Ein Eintrag, der alle Unternamen auf dieselbe Maschine zeigen lässt. Bequem — und man sollte wissen, dass damit jeder erfundene Name beim Proxy ankommt. Was der mit unbekannten Namen macht, ist eine bewusste Entscheidung.
Eine Standardantwort für Unbekanntes. Bei mir gibt es eine leere Antwort statt einer beliebigen Seite. Sonst beantwortet der erstbeste eingetragene Dienst Anfragen an Namen, die ihn nichts angehen.
Der letzte Punkt ist der, den ich anfangs übersehen hatte — und er ist der Grund, warum ein Aufruf mit falschem Namen bei mir heute nichts verrät.
Was ich beim Erweitern gelernt habe
Als später weitere Dienste dazukamen — eine Statistikseite, eine Kartenansicht, ein Ladensystem —, hat sich derselbe Grundsatz bewährt, nur mit Unterdomains statt Pfaden.
Und eine Sache, die ich mir angewöhnt habe: Vor jeder Freigabe die Gegenprobe von außen. Nicht von der Maschine selbst, sondern von einem anderen Anschluss. Ein Portscan auf die eigene Adresse zeigt in dreißig Sekunden, ob wirklich nur das offen ist, was offen sein soll.
Ich habe damit zweimal etwas gefunden, das ich nicht offen haben wollte. Beide Male war es kein Fehler in der Firewall, sondern eine Anwendung, die an alle Adressen gebunden hat, obwohl ich das Gegenteil angenommen hatte.