Souveränes Hosting in Europa
von Daniel

Wie gut funktioniert Web-App-Hosting ohne Cloud-Act-Anbieter?
Der ehrliche Bauplan hinter The Honeypot Files: was zuerst kam, was ich verbockt habe und wie ich's das nächste Mal machen würde.
Willkommen in den Honeypot Files. Ich zeige dir, wie dieser Blog hier gebaut ist - Schicht für Schicht, in der Reihenfolge, in der es passiert ist. Inklusive der Stellen, an denen ich mir selbst ins Knie geschossen habe. Denn: Jeder macht Security. Am ehrlichsten lernt man sie, wenn jemand offenlegt, wo er falsch abgebogen ist.
Keine Cloud-Act-Anbieter - I bims 1 Hirngespinst
Am Anfang stand keine Technologie, sondern eine Haltung: kein Cloud-Act-Anbieter im kritischen Pfad. Klingt nach einem Detail für Datenschutz-Nerds, ist aber die Wurzel, aus der jede spätere Entscheidung wächst. Setz einmal „souverän, self-hosted, in Europa" als Moonshot - und plötzlich beantworten sich Dutzende Fragen von selbst: nicht GitHub, sondern Codeberg. Nicht Google Fonts, sondern self-hosted. Nicht Vercel, sondern ein eigener Server. Nicht Google Analytics, sondern etwas Cookieloses in Eigenregie. YouTube darf mit rein. Wir lieben YouTube. Aber es muss klopfen und draußen warten, bis es reingelassen wird.
Der ganze Rest des Projekts ist im Grunde die konsequente Ableitung aus diesem einen Satz. Jede Schicht ist eine Antwort auf dieselbe Frage: Wie bleibe ich souverän, ohne an Sicherheit oder Nutzbarkeit zu verlieren.
Zuerst das virtuelle Metall, dann die App
Wichtigste Reihenfolge-Regel im ganzen Projekt: Du härtest den leeren Server, bevor irgendetwas Wertvolles drauf liegt. Erst das Fundament - ein einzelner, überschaubarer Server in Deutschland. Bewusst ein Server statt eines verteilten Zoos, weil man nur schützen kann, was man versteht. Naja... ansatzweise versteht in meinem Fall.
Darauf sofort die Basis-Härtung: SSH nur mit Schlüssel (Passwort-Login komplett aus), Root-Login tot, Alltag über einen unprivilegierten deploy-User. Eine Host-Firewall, die nur das Nötigste offen lässt - und ufw-docker obendrauf, weil Docker sonst deine Firewall für seine Ports einfach umgeht. Ja, richtig gelesen: Du stellst brav deine ufw-Regeln auf, und Docker published seinen Port trotzdem ins Internet. Eine der gemeinsten Fallen überhaupt. Wie kannst du nur, Docker?
Bewusste Entscheidung außerdem: erstmal kein PaaS (Coolify sollte eigentlich alles pipelinen), sondern schlankes docker-compose. Der Preis ist etwas mehr Handarbeit. Aber wir sind ITler. Wir sind Handarbeit gewohnt. Der Gewinn: Alles ist nachvollziehbar und selbst reparierbar - kein magischer Layer, der im Ernstfall zwischen dir und deinem Server steht. Für ein Projekt, das seine eigene Infrastruktur verstehen will, ist das die richtige Abwägung. Vielleicht kommt Coolify noch, wenn das Projekt wächst. Ma guggn...
Das Rückgrat: zwei Türen
Bevor die App überhaupt erreichbar sein durfte, kam die Architektur-Entscheidung, die alles Spätere trägt: ein Reverse-Proxy (Traefik) mit zwei getrennten Türen.
Die öffentliche Tür führt durch die WAF zur App und zeigt nur den Blog. Die Admin-Tür existiert ausschließlich über einen WireGuard-VPN-Tunnel - dort hängen /backdoor (ja, meine Admin-Oberfläche heißt so), die API und das Analytics-Dashboard. Fürs offene Internet ist die Verwaltung schlicht nicht vorhanden. 404. Nada. Tote Hose.
Das ist der eleganteste Sicherheitsgewinn im ganzen Stack: Er entfernt eine ganze Angriffsklasse - alles rund um Login und Admin - einfach aus dem Internet. Man kann nicht angreifen, was man nicht sieht.
Zwei Details, die sich zu erwähnen lohnen: Traefik bekommt keinen Docker-Socket (das wäre faktisch Root auf dem Host), sondern liest sein Routing aus einer Datei. Und die Admin-Tür umgeht die WAF absichtlich - sonst würden die Rich-Text-POSTs des CMS ständig Fehlalarm auslösen, und der Admin-Traffic ist ja eh schon per VPN abgeriegelt. Genau solche „warum eine Ausnahme hier sicherer ist"-Momente sind das Salz in der Suppe des armen Mannes.
Zutritt regeln, bevor jemand klopft
Noch vor dem ersten öffentlichen Byte kam die Zugriffskontrolle - man baut Türschlösser ein, bevor Gäste kommen, nicht danach. Wir wissen, welche Gäste gemeint sind. Nicht wahr, Barbara? Rollen (Owner > Admin > Editor), Field-Level Access Control gegen „Mass Assignment" (jemand schmuggelt beim Speichern ein Feld rein, das er gar nicht setzen dürfte), und verpflichtende MFA per TOTP für alle.
Dazu eigene Recovery-Codes - zehn einmalig nutzbare, gehashte Codes, nirgends im Klartext - und ein Owner-Break-Glass-Konto, dessen Zugangsdaten offline liegen und das im Alltag gar nicht benutzt wird. Das Muster „Alltags-Account ≠ Notfall-Account" ist simpel und brutal wirksam. Sei nicht der, der sich mit 1 Passwort namens "Passwort" und 1 Post-it am Monitor absichert.
Nach außen gehen - mit Schutzschild und Privacy
Als der Blog öffentlich ging, kam die WAF davor (OWASP CRS via ModSecurity) - zunächst im DetectionOnly-Modus. Sie kam, sah und loggte, aber (noch) sie nix blockte. Das war Absicht, kaum Faulheit: Erst ein, zwei Wochen echten Traffic beobachten, Fehlalarme raustunen, dann scharfstellen. Eine WAF, die am ersten Tag blockt und dabei legitime Nutzer aussperrt, richtet mehr Schaden an als Nutzen.
Parallel die Privacy-Schicht, wieder aus dem Moonshot abgeleitet: cookielose Reichweitenmessung, YouTube nur per Click-to-Load (kein Kontakt zu Google, bis du aktiv auf Play klickst), self-hosted Fonts. Das Schöne: Privacy by Design macht das Cookie-Banner überflüssig. Wenn nichts Einwilligungspflichtiges auf deinem Gerät landet, gibt es schlicht nichts, wozu man dich nerven müsste.
Daten in Sicherheit bringen - und mein Brain Poop
Jetzt kommt die wichtigste Reihenfolge-Lektion des Projekts: Media und Backups. Bilder liefen erst über ein lokales Volume, später auf privaten Object Storage (S3, durch die App gestreamt), mit automatischer WebP-Konvertierung. Und separat ein verschlüsseltes, versioniertes Offsite-Backup mit Borg, das DB, Analytics-DB und Media in einem täglichen Snapshot sichert - inklusive Restore-Test, denn ein ungetestetes Backup ist nur ein Gerücht. Nicht wahr, Barbara?
Jetzt der Teil mit dem Brain Poop: Backups gehören eigentlich ganz nach vorn - bevor das erste echte Datum existiert. Ich hab das Media-Backup erst nachgezogen, als die Bilder schon in S3 lagen, und dabei gemerkt, dass die erste Backup-Version nur die DB erfasste. Ein Backup, das du erst baust, wenn du schon Produktivdaten hast, deckt genau die riskante Anfangsphase nicht ab.
Der IPv6-Krimi
Dann Dual-Stack IPv6 - technisch „nur" Erreichbarkeit über beide Protokolle, in Wahrheit ein kleiner Detektivroman. Server IPv6-bereit, Ports gebunden, Firewall offen, ping ging durch - und trotzdem hing jeder Seitenaufruf.
Nach einer Runde durch alle üblichen Verdächtigen (MTU, Firewall, Routing) war der Täter ein falscher AAAA-Record: Er zeigte auf die Netz-Adresse des /64 statt auf den Host. Ein tcpdump-Mitschnitt zeigte die SYN-Pakete ins Leere laufen - klingt nach mir. Die Lehre und der beste Aufhänger daraus: Die Infrastruktur war die ganze Zeit korrekt. Der Bug saß in einem einzigen DNS-Feld.
Der Feinschliff - und eine lehrreiche Bruchlandung
Zum Schluss die Politur, in einem konzentrierten Härtungs-Sprint. Auf der Transport-Schicht: TLS auf 1.3-only (nichts darunter, pentest-sauber), dann via Traefik-Versionssprung Post-Quantum-Key-Exchange (X25519MLKEM768) - Schutz gegen „harvest now, decrypt later", also heute mitschneiden, in X Jahren mit Quantencomputer knacken. Dazu sniStrict und ein CAA-Record, der festlegt, dass nur Let's Encrypt Zertifikate für die Domain ausstellen darf.
Auf der Runtime-Schicht: Rate-Limiting und Container-Hardening (no-new-privileges, Memory-Limits, Capabilities gedroppt). Und hier die lehrreichste Bruchlandung: cap_drop: ALL auf Traefik nahm dem root-Prozess die Fähigkeit, seine eigene gemountete Konfig zu lesen. Traefik startete nicht, Seite kurz down. Fix: Bei einem root-Container, der Bind-Mounts liest, kein cap_drop: ALL. Härtung kann eben auch nach hinten losgehen.
# compose.yaml — container hardening (excerpt)
services:
app:
security_opt:
- no-new-privileges:true # no privilege gain at runtime
cap_drop:
- ALL # all Linux capabilities stripped
mem_limit: 512m # memory cap against runaway processes
restart: unless-stoppedBeweisführung statt Behauptung
Ein Baustein, der oft fehlt: Ich hab das eigene Dixi-Klo von außen mit Angreifer-Augen angeschaut (ein Mini-Threat-Model), die Findings priorisiert, zwei davon direkt geschlossen (Rate-Limiting, Container-Hardening) und den Rest ehrlich als offen markiert. Finding → Fix → Re-Test ist nicht nur gute Security-Praxis, sondern auch besonders langweilig.
Und was wäre die beste Reihenfolge gewesen?
Würde ich vom heutigen Stand aus neu bauen, wäre die ideale Abfolge:
- Server- und OS-Härtung auf dem leeren System
- Die Zwei-Türen-Architektur
- Auth & MFA
- Backups - schon jetzt, vor dem ersten echten Datum
- Die App und ihre Daten
- Media + S3
- Die öffentliche Schicht (WAF im Beobachtungsmodus, Privacy)
- Transport- und Runtime-Härtung
- IPv6 und Feinschliff
- Kontinuierliche Verifikation
Roter Faden: Schutz und Wiederherstellbarkeit kommen früh, Features spät. Mein realer Weg hat Features (S3, Umami, IPv6) teils vor die letzte Härtung gezogen - hat funktioniert, aber die Härtung sollte Features idealerweise begleiten, nicht hinterherlaufen.
Was ich bewusst offen gelassen habe - und warum
Zum Schluss das Ehrliche, das einem Security-Publikum am meisten Respekt abnötigt: was nicht umgesetzt ist, mit Begründung.
- HSTS-Preload ist bewusst aufgeschoben: Der Header schützt schon praktisch alle. Die Eintragung in die Browser-Preload-Liste ist aber quasi permanent und würde jede künftige Subdomain für immer auf HTTPS festnageln - ein einziger TLS-Ausfall sperrt dann Nutzer komplett aus. Für ein noch wachsendes Projekt ist der Zusatznutzen die starre Bindung nicht wert.
- Netbird (MFA auf dem Tunnel) ist zurückgestellt: Self-hosted zöge einen ganzen Identity-Stack nach sich, schafft eine Zirkularität (die Zugangskontrolle liefe auf dem Server, den sie schützt) - und hinter dem Tunnel sitzt ohnehin schon echte TOTP-MFA.
- Dependabot fällt raus, weil GitHub-eigen (US); der souveräne Ersatz Renovate ist für ein Solo-Projekt noch Overkill. Vorerst reichen ein periodischer
npm auditund der Trivy-Scan.
Das war der Rundgang. Kein Stack ist je „fertig" - Security ist ein Speedrun ohne Ziellinie, bei dem regelmäßig ein neuer Skip auftaucht. Aber jetzt weißt du, wie hier jede Schicht entstanden ist, in welcher Reihenfolge und wo ich beim nächsten Mal früher abbiegen würde. Muss man einen kleinen privaten Blog so härten? Klar nicht, aber das Prinzip ist generell auf Web-Applikationen mit ähnlichem Stack anwendbar - vielleicht sogar auf einen Web-Shop. Außerdem wollen wir die Grenzen des souveränen EU-Hostings erkunden. Und zudem ist der Blog nun recht Laser. Wie ein 20-Kilo-Kettenhemd auf einem Styropor-LARP.
Viel Spaß beim Stöbern. Mach MFA an. Think purple.