Status Produktiv seit 23.05.2026, Auth-Wechsel 24.05.2026
Öffentliche URL intern, hinter Login
Hardware Synology DS2018j im Heimnetz
Cert Let's Encrypt, Auto-Renewal alle 60 Tage
Auth PHP-Session-Login (Cookie KIR_SESSION, bcrypt)

Worum es geht

Ein IT-Projekt mit KI soll erstmal nur ein eng definierter Personenkreis lesen können – aktuell Brokmann selbst und Nicolas Voggenreiter. Gleichzeitig soll die Seite ohne fremde Cloud auskommen und nicht bei einem öffentlichen Hoster liegen. Lösung: Hosting auf dem privaten Synology NAS im Heimnetz, kontrolliert nach außen geöffnet und mit Passwort geschützt.

Architektur

Der Pfad eines Requests von Voggenreiters Browser bis zur Seite:

Internet → FRITZ!Box (Portfreigaben 80/443) → Synology NAS → Nginx (SSL-Termination mit Let's Encrypt-Cert) → Apache 2.x mit PHP-FPM (liest .htaccess, parst der Zugangsschutz über PHP-Sitzungen) → /volume1/web/ (vom Ein IT-Projekt mit KI-Generator erzeugte .php-Seiten)

Nginx terminiert SSL, weil das Synology-DSM darauf spezialisiert ist. Apache dient als Backend, weil Synology-Nginx keine eigene PHP-FPM- Integration unterm Web-Station-Dach bietet – Apache schon. PHP-FPM ist Pflicht, weil jede Seite per require_once der Zugangsschutz über PHP-Sitzungen ihren Auth-Layer mitbringt. So entsteht ein Authentifizierungs-Layer ohne Custom-Nginx-Konfiguration, die unter DSM-Updates wegfliegen würde.

Phasen der Einrichtung

  • NAS-Hosting: Web Station aktivieren, Default-Webroot /web, eigener User Claude für SFTP-Deploys angelegt
  • DDNS: Synology-Account verknüpft, Subdomain intern, hinter Login aktiviert, zeigt automatisch auf die öffentliche Heim-IP
  • Router: FRITZ!Box-Portfreigaben für Port 80 (Let's Encrypt-Verifikation) und 443 (HTTPS) zur NAS – ALLE anderen Ports gehen nicht durch
  • Zertifikat: Let's Encrypt-Cert für intern, hinter Login via DSM beantragt, als Default-Cert gesetzt, Auto-Renewal aktiv
  • Virtual Host: Web Station Name-based Portal für die Subdomain, Backend Apache HTTP Server 2.x mit PHP-FPM-Profil (Wechsel von Nginx, weil .htaccess + PHP-Parsing gebraucht werden)
  • Erster Auth-Versuch (23.05.2026): Apache Basic Auth via .htpasswd (APR1-Hash für User voggenreiter) und .htaccess-Direktiven – funktional sauber, aber bei Voggenreiter kam in seiner Browser-/Proxy-Konstellation gar kein Anmelde-Dialog hoch
  • Umstellung auf PHP-Session-Login (24.05.2026): der Zugangsschutz über PHP-Sitzungen in /web/ mit bcrypt-Hash-Liste, in-page Login-Form (HTTP 401 bei fehlendem Cookie), Cookie KIR_SESSION 30 Tage Secure/HttpOnly/SameSite=Lax. Generator-Output von .html auf .php umgestellt (AddHandler für .html greift im PHP-FPM-Profil nicht), jede Seite bindet der Zugangsschutz über PHP-Sitzungen per require_once ein

Security-Layer auf der Seite

  • PHP-Session-Login als Auth-Gate: jede .php-Seite bindet der Zugangsschutz über PHP-Sitzungen per require_once ein. Ohne Cookie ⇒ HTTP 401 + in-page Login-Form. POST mit korrekten Credentials ⇒ password_verify (bcrypt) ⇒ session_regenerate_id ⇒ 302 auf die ursprüngliche URL
  • Session-Cookie KIR_SESSION: 30 Tage Lebensdauer, Secure (nur HTTPS), HttpOnly (kein JS-Zugriff), SameSite=Lax (kein CSRF aus Fremdseiten)
  • Strikte User-Trennung: Voggenreiter existiert NUR in der bcrypt-Liste in der Zugangsschutz über PHP-Sitzungen, ist KEIN DSM-User – damit kein NAS-/SFTP-/DSM-Zugriff möglich, selbst wenn das Web-Passwort kompromittiert wäre
  • HTTP → HTTPS Redirect via mod_rewrite mit X-Forwarded-Proto-Check (nötig, weil der vorgeschaltete Nginx SSL terminiert und das Backend sonst nicht erkennt, ob Original-Request HTTP oder HTTPS war)
  • HSTS: max-age=31536000; includeSubDomains – Browser merkt sich „nur noch HTTPS für diese Domain" für 1 Jahr
  • X-Frame-Options: DENY – Schutz gegen Click-Jacking via iframe-Einbettung
  • X-Content-Type-Options: nosniff – Browser darf MIME-Typen nicht erraten
  • Referrer-Policy: strict-origin-when-cross-origin – weniger Referrer-Leakage beim Folgen externer Links
  • Permissions-Policy: FLoC/Topics-Tracking deaktiviert
  • Security-Header doppelt gesetzt (Belt-and-Suspenders): einmal in .htaccess via <IfModule mod_headers.c> und zusätzlich in der Zugangsschutz über PHP-Sitzungen per PHP-header() – Letzteres greift zuverlässig, weil mod_headers im Synology-PHP-FPM-Profil unzuverlässig ist
  • Dotfile-Schutz: <Files ".ht*"> Deny from all versteckt .htaccess hinter 403; der Zugangsschutz über PHP-Sitzungen ist PHP-geparst, deshalb wird der Quellcode nie ausgeliefert
  • robots.txt: Disallow: / – Anti-Indexierung als zusätzliche Hürde

Security-Layer auf dem NAS

  • DSM-Firewall: Web (80/443) für alle, DSM-Admin (5000/5001), File Station, SFTP (22) und SMB nur aus dem LAN-Subnet einer Adresse im Heimnetz, Rest deny
  • Auto-Block: 5 Fehlanmeldungen in 5 Minuten ⇒ 24 h IP-Block
  • DoS Protection aktiv
  • Claude-User über SFTP chrooted auf /web + /web_packages – kein Zugriff auf /volume1, /etc, /home, /root
  • DSM-Auto-Update für Security-Patches aktiviert
  • Mail-Notifications über Office-365-SMTP für Cert-Renewal-Failures, Login-Failures und Update-Events

Verifikation extern

Der gesamte Aufbau wurde extern via curl aus Claude Code verifiziert: nur Ports 80 und 443 sind öffentlich erreichbar (22, 445, 5000, 5001 sind dicht), das Let's Encrypt-Cert wird korrekt für die Subdomain ausgeliefert, HTTP wird mit 301 auf HTTPS umgeleitet, .htaccess ist mit 403 blockiert.

Der End-to-End-Login-Flow wurde mit einem Cookie-Jar nachgespielt: Root ohne Cookie liefert HTTP 401 mit Login-Karte im Body, der POST mit korrekten Credentials liefert HTTP 302 mit Set-Cookie: KIR_SESSION, der Folge-Request mit Cookie liefert HTTP 200 und die echte Landing-Seite. Unterseiten verhalten sich identisch. Alle fünf Security-Header sind im Response sichtbar – auch im authentifizierten Zustand, weil sie defensiv in der Zugangsschutz über PHP-Sitzungen gesetzt sind.

Verbindung zu Ein IT-Projekt mit KI

Diese Strecke ist die Hosting-Schicht unter Ein IT-Projekt mit KI – die eigentliche Webseite wird mit der schlanken Deployment-Strecke NAS über die SFTP-Integration zu Synology NAS ins /web/-Verzeichnis geschoben. Diese Externe-Freigabe-Strecke sorgt dafür, dass das Ergebnis dann auch von außen sichtbar wird – genau für die Personen, die sich mit ihren Credentials anmelden können.

Falls die Anforderungen wachsen (Kunden-Öffnung, mehr Nutzer, höhere Last), lässt sich der gleiche Aufbau ohne große Umbauten erweitern: weitere bcrypt-Einträge in der Zugangsschutz über PHP-Sitzungen für mehr Personen, eine zweite Subdomain für eine öffentliche Variante, Reverse-Proxy- Verkettung für andere Dienste am gleichen NAS. Für ein größeres Nutzer-Setup wäre der nächste logische Schritt, die User-Liste aus der Zugangsschutz über PHP-Sitzungen in eine kleine SQLite-Datei oder gegen einen OIDC-Provider auszulagern – die Auth-Mechanik selbst (Cookie, Session-Handling, in-page-Form) bleibt identisch.

Eingesetzt in

Dieses Projekt wird in folgenden anderen Projekten verwendet:

← zurück zur Projekt-Übersicht