Externe Freigabe via Synology NAS
Komplette NAS-Hosting-Strecke für Ein IT-Projekt mit KI mit Let's Encrypt-HTTPS, PHP-Session-Login und Security-Baseline – damit Voggenreiter extern reinkommt.
Komplette NAS-Hosting-Strecke für Ein IT-Projekt mit KI mit Let's Encrypt-HTTPS, PHP-Session-Login und Security-Baseline – damit Voggenreiter extern reinkommt.
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.
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) → statische Dateien
direkt, .php an PHP-FPM (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.
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.
Diese Darstellung stand bis zum 10.08.2026 falsch hier –
sie führte ein Apache-Backend auf, das die .htaccess liest.
Das gab es nie. Was daraus folgte, steht weiter unten unter
„Was sich als falsch herausgestellt hat“.
/web, eigener User Claude für SFTP-Deploys angelegtintern, hinter Login aktiviert, zeigt automatisch auf die öffentliche Heim-IPintern, hinter Login via DSM beantragt, als Default-Cert gesetzt, Auto-Renewal aktiv.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 hochder 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.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 URLKIR_SESSION: 30 Tage Lebensdauer, Secure (nur HTTPS), HttpOnly (kein JS-Zugriff), SameSite=Lax (kein CSRF aus Fremdseiten)der Zugangsschutz über PHP-Sitzungen, ist KEIN DSM-User – damit kein NAS-/SFTP-/DSM-Zugriff möglich, selbst wenn das Web-Passwort kompromittiert wäreder Zugangsschutz über PHP-Sitzungen heraus, bevor das Login-Formular je im Klartext ausgeliefert wird – lange einer mod_rewrite-Regel zugeschrieben, die nie lief. Weil er im PHP-Code sitzt, gilt er nur für .php: Stylesheet und robots.txt kommen über HTTP unverschlüsselt zurückmax-age=31536000; includeSubDomains – Browser merkt sich „nur noch HTTPS für diese Domain" für 1 JahrDENY – Schutz gegen Click-Jacking via iframe-Einbettungnosniff – Browser darf MIME-Typen nicht erratenstrict-origin-when-cross-origin – weniger Referrer-Leakage beim Folgen externer Linksder Zugangsschutz über PHP-Sitzungen per PHP-header(), und damit auf jeder .php-Antwort. Bis zum 10.08.2026 stand hier „doppelt gesetzt (Belt-and-Suspenders)" – die zweite Ebene existierte nichtder Zugangsschutz über PHP-Sitzungen ist PHP-geparst, deshalb wird der Quellcode nie ausgeliefert. Ein Dotfile-Schutz per <Files ".ht*"> war dokumentiert, wirkte aber nie – die Datei wurde deshalb entfernt statt verstecktDisallow: / – Anti-Indexierung als zusätzliche Hürde/web + /web_packages – kein Zugriff auf /volume1, /etc, /home, /rootDer 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.
Die Zeile „.htaccess ist mit 403 blockiert“ stand hier bis
zum 10.08.2026 – abgeschrieben aus der Konfiguration, nie abgerufen.
Tatsächlich lieferte der Server die Datei mit HTTP 200 aus.
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.
Am 10.08.2026 stellte sich heraus, dass ein Teil der oben beschriebenen Absicherung nie existiert hat. Der Befund gehört auf diese Seite, weil er über das Ergebnis mehr aussagt als die Teile, die funktioniert haben.
Der Befund. Die .htaccess im Web-Root wurde
mit HTTP 200 ohne Anmeldung im Klartext ausgeliefert –
obwohl drei Dokumente behaupteten, sie verstecke sich selbst hinter 403.
Keine ihrer Direktiven hat je gegriffen.
Die Ursache war nicht die naheliegende. Die erste
Vermutung lautete „AllowOverride ist abgeschaltet, Apache
liest die Datei nicht“. Tatsächlich steht Apache in diesem Pfad gar
nicht: Der Synology-Nginx liefert statische Dateien direkt aus, nur
.php geht an PHP-FPM. Eine .htaccess ist dort
keine abgeschaltete Konfiguration, sondern eine Datei ohne Bedeutung.
Wie es auffiel – und warum erst so spät. Der Beweis war
nicht die Datei selbst, sondern die harmloseste Regel darin: eine
301-Umleitung alter .html-Adressen auf .php.
Der Aufruf lieferte 404 statt 301. Damit war
klar, dass nicht eine Direktive fehlschlug, sondern alle. Zweieinhalb
Monate lang hatte niemand die Datei abgerufen, deren Schutz drei
Dokumente zusicherten – geprüft worden war die Konfiguration,
nie die Auslieferung.
Was das praktisch bedeutete. Die als doppelt
dokumentierte Absicherung war einfach. Die Security-Header kamen
ausschließlich aus der Zugangsschutz über PHP-Sitzungen; wäre die Datei ausgefallen,
wären Zugangsschutz und Header gemeinsam weggefallen – und niemand hätte
eine zweite Ebene vermisst, weil alle Unterlagen eine versprachen.
Die Entscheidung. Die Datei wurde entfernt statt repariert. Sie hätte sich wirksam machen lassen, indem man den Virtual Host auf ein Apache-Backend umstellt – ein Eingriff in die ausliefernde Schicht einer laufenden Seite, und ausgerechnet diese Kombination war auf dieser NAS schon zweimal gescheitert. Ein Leck ohne Gegenwert wird geschlossen, nicht gepflegt; die zusätzliche Schicht bleibt eine eigene, separat entschiedene Frage.
Was bleibt. Eine Sicherheitsaussage, die in der Dokumentation steht und in der Realität nicht gilt, ist gefährlicher als eine fehlende – sie verhindert, dass jemand nachsieht. Geprüft wird seitdem gegen den ausgelieferten Stand, nicht gegen die Konfiguration im Projektordner.
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.
Dieses Projekt wird in folgenden anderen Projekten verwendet: