Table of contents
CT 101 — NAS (Samba/SFTP/Filebrowser) — Rebuild-Runbook
Voraussetzung: ZFS-Pool nas mit Dataset nas/freigabe existiert auf dem PVE-Host (siehe 01-pve-host.md).
1. Container erstellen
pct create 101 local:vztmpl/<DEBIAN_TEMPLATE> \
--hostname nas \
--unprivileged 1 \
--cores 1 \
--memory 512 \
--swap 512 \
--rootfs local-lvm:4 \
--net0 name=eth0,bridge=vmbr0,ip=<LAN_IP_NAS>/24,gw=<LAN_IP_ROUTER> \
--features nesting=1 \
--timezone Europe/Berlin \
--onboot 1 \
--nameserver <LAN_IP_ADGUARD>
pct set 101 -mp0 /nas/freigabe,mp=/srv/freigabe
chown -R 100000:100000 /nas/freigabe
pct start 101
Ownership 100000:100000 = Standard-UID-Offset für unprivileged Container (Container-UID 0 = Host-UID 100000).
2. Samba
pct exec 101 -- apt-get update
pct exec 101 -- apt-get install -y samba openssh-server
Debians samba-Metapaket zieht standardmäßig samba-ad-dc + winbind mit (nicht gebraucht, kein AD-DC-Zweck):
pct exec 101 -- systemctl disable --now winbind
pct exec 101 -- systemctl mask samba-ad-dc
/etc/samba/smb.conf ergänzen:
[freigabe]
path = /srv/freigabe
browseable = yes
read only = no
guest ok = no
valid users = <PRIMARY_USER>
create mask = 0664
directory mask = 0775
Workgroup und Mindest-Protokoll im [global]-Block:
workgroup = WORKGROUP
server min protocol = SMB2
⚠️ NICHT TUN: das Standard-[homes]-Share per sed nur an der Kopfzeile auskommentieren. Der Block hat mehrere Folgezeilen (browseable, create mask, valid users = %S etc.) — werden die nicht mit auskommentiert, hängen sie als „verwaiste" Zeilen im vorherigen Abschnitt ([global]) und überschreiben dort Werte, u.a. valid users = %S. Folge: IPC$-Zugriff für alle Nutzer blockiert (NT_STATUS_ACCESS_DENIED), auch für die eigentlich erlaubte Freigabe. Immer den kompletten [homes]-Block (Kopfzeile + alle Optionszeilen bis zur nächsten Sektion) auskommentieren. Mit testparm -s nach jeder Änderung prüfen, dass [global] sauber bleibt.
Benutzer anlegen:
pct exec 101 -- useradd -M -s /bin/bash -d /srv/freigabe <PRIMARY_USER>
pct exec 101 -- smbpasswd -a <PRIMARY_USER>
pct exec 101 -- smbpasswd -e <PRIMARY_USER>
pct exec 101 -- chpasswd # gleiches Passwort auch als Unix-Login setzen (für SSH/SFTP)
pct exec 101 -- chown -R <PRIMARY_USER>:<PRIMARY_USER> /srv/freigabe
pct exec 101 -- systemctl restart smbd nmbd
Unix- und Samba-Passwort bewusst identisch gehalten — ein Passwort für SMB, SFTP und Filebrowser.
3. SSH/SFTP-Zugriff
Mit obigem useradd -s /bin/bash ist SSH bereits nutzbar (Shell + Home = /srv/freigabe, Passwort per chpasswd gesetzt). Kein separater Schritt nötig, wenn Schritt 2 komplett ausgeführt wurde.
⚠️ NICHT TUN: einen passwortlosen SSH-Key für Fernzugriff (z.B. aus Guacamole) auf dieses Share hinterlegen. Zugriff auf sensible Daten (Dokumente, Fotos) sollte immer noch ein Passwort verlangen — bei Kompromittierung der aufrufenden Instanz (z.B. Guacamole/VPS) sonst direkter, ungeschützter Zugriff. Auch verschlüsselte Keys mit Passphrase lösen kein echtes UX-Problem (siehe 06-vps-strato.md, Abschnitt Guacamole-Verbindungen) — einfach normales Passwort verwenden.
3b. Zweite Freigabe: Backups (read-only)
Bind-Mount und Samba-Share für /nas/backups (siehe 07-backup-restore.md) — bewusst read-only, da die Backups volle Container-Images inkl. aller Secrets enthalten:
pct set 101 -mp1 /nas/backups,mp=/srv/backups,backup=0
/etc/samba/smb.conf ergänzen:
[backups]
path = /srv/backups
browseable = yes
read only = yes
guest ok = no
valid users = <PRIMARY_USER>
Kein separater Nutzer nötig, <PRIMARY_USER> deckt beide Shares ab. Erreichbar unter \\nas.pve\backups.
4. Filebrowser (grafische Web-Oberfläche)
VER=$(curl -s https://api.github.com/repos/filebrowser/filebrowser/releases/latest | grep -o '"tag_name": "[^"]*"' | cut -d'"' -f4)
curl -fsSL -o /tmp/filebrowser.tar.gz "https://github.com/filebrowser/filebrowser/releases/download/$VER/linux-amd64-filebrowser.tar.gz"
tar xzf /tmp/filebrowser.tar.gz -C /tmp filebrowser
pct push 101 /tmp/filebrowser /usr/local/bin/filebrowser
pct exec 101 -- chmod +x /usr/local/bin/filebrowser
⚠️ NICHT TUN: das offizielle curl | bash-Installskript von Filebrowser verwenden — stattdessen Release-Binary direkt von GitHub laden (transparenter, keine Shell-Pipe von einem Skript, das man vorher nicht liest).
Config + Service:
pct exec 101 -- mkdir -p /var/lib/filebrowser
pct exec 101 -- chown <PRIMARY_USER>:<PRIMARY_USER> /var/lib/filebrowser
pct exec 101 -- su -s /bin/bash <PRIMARY_USER> -c '/usr/local/bin/filebrowser config init -d /var/lib/filebrowser/filebrowser.db'
pct exec 101 -- su -s /bin/bash <PRIMARY_USER> -c '/usr/local/bin/filebrowser config set -a 0.0.0.0 -p 8080 -r /srv/freigabe -d /var/lib/filebrowser/filebrowser.db'
pct exec 101 -- su -s /bin/bash <PRIMARY_USER> -c "/usr/local/bin/filebrowser users add <PRIMARY_USER> '<PASSWORT>' --perm.admin -d /var/lib/filebrowser/filebrowser.db"
cat > /etc/systemd/system/filebrowser.service <<'EOF'
[Unit]
Description=Filebrowser Web UI
After=network.target srv-freigabe.mount
[Service]
User=<PRIMARY_USER>
Group=<PRIMARY_USER>
ExecStart=/usr/local/bin/filebrowser -d /var/lib/filebrowser/filebrowser.db
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now filebrowser
Erreichbar unter http://nas.pve:8080 (LAN + Tailnet). Bewusst nicht öffentlich exponiert — für den Internetcafé-Fall gibt's den SFTP-Dateibrowser innerhalb der TOTP-gesicherten Guacamole-Verbindung (siehe 06-vps-strato.md).
5. Nutzerstruktur: gemeinsam + persönliche Bereiche mit Quota
Granulare Struktur statt eines flachen <PRIMARY_USER>-only-Shares — eigene ZFS-Datasets je Bereich (nicht bloße Ordner), damit Quotas gelten und Samba/Windows den korrekten freien Platz pro Share melden.
# Datasets (Host)
zfs create -o quota=200G nas/freigabe/gemeinsam
zfs create -o quota=200G nas/freigabe/<PRIMARY_USER>
zfs create -o quota=200G nas/freigabe/<SECONDARY_USER>
zfs set reservation=100G nas/backups
# Ownership: erst auf dem Host (unprivileged Container!), dann im Container
chown 100000:100000 /nas/freigabe/gemeinsam /nas/freigabe/<PRIMARY_USER> /nas/freigabe/<SECONDARY_USER>
⚠️ NICHT TUN: die neuen Dataset-Verzeichnisse direkt aus dem Container heraus chown/chmoden, ohne vorher den Host-seitigen Owner zu setzen. Frisch angelegte Kind-Datasets gehören zunächst root:root (UID 0) auf dem Host — für einen unprivileged Container ist das eine andere UID als sein eigenes "root" (UID-Offset), chown/chmod von innen schlägt mit "Operation not permitted" fehl. Immer zuerst chown 100000:100000 <pfad> auf dem Host, danach funktioniert chown/chmod von innen normal.
⚠️ Warum eigene ZFS-Datasets statt zfs create einmal + Unterordner: Nur echte Datasets haben ein eigenes quota; Windows-Explorer zeigt darüber den tatsächlich verbleibenden Platz je Share. Mit userquota@uid sähe der Client stattdessen den ganzen Pool und liefe erst beim Schreiben in einen Fehler.
⚠️ Korrektur — echter Kaltstart-Bug, ursprünglich falsch als „funktioniert automatisch" dokumentiert: Der hier ursprünglich stehende Satz „neue Kind-Datasets erscheinen automatisch im bestehenden mp0-Bind-Mount, kein zusätzlicher Eintrag nötig" war falsch — bzw. nur unter den damaligen Testbedingungen richtig (Container lief bereits, Datasets wurden live dazu angelegt; ein nicht-rekursiver Bind-Mount kann in diesem Fall per Shared-Mount-Propagation live nachziehen). Bei einem echten Kaltstart des gesamten Hosts (z. B. nach Stromausfall) gilt das nicht: startet CT101, bevor die ZFS-Kind-Datasets erneut gemountet sind (oder greift die Propagation aus welchem Grund auch immer nicht), bleibt /srv/freigabe/{<PRIMARY_USER>,<SECONDARY_USER>,gemeinsam} dauerhaft leer (nobody:nogroup-Stub-Verzeichnisse) — auch ein Neustart des Containers allein behebt das nicht, das Bind-Mount muss neu mit den dann schon gemounteten Kind-Datasets aufgebaut werden. Reale Folge: genau das ist nach dem ersten echten Stromausfall dieses Projekts passiert, keine Ordner mehr sichtbar unter dem persönlichen Bereich. Fix, exakt das gleiche Muster wie an anderer Stelle im Setup mit ZFS-Kind-Datasets:
pct set 101 --mp2 /nas/freigabe/<PRIMARY_USER>,mp=/srv/freigabe/<PRIMARY_USER>,backup=0
pct set 101 --mp3 /nas/freigabe/<SECONDARY_USER>,mp=/srv/freigabe/<SECONDARY_USER>,backup=0
pct set 101 --mp4 /nas/freigabe/gemeinsam,mp=/srv/freigabe/gemeinsam,backup=0
pct reboot 101
Jedes Kind-Dataset bekommt einen eigenen, expliziten mp-Eintrag statt sich auf Propagation durch den Eltern-Bind-Mount (mp0) zu verlassen — robust gegen jede Boot-Reihenfolge. /nas/backups (Abschnitt 3b) ist davon nicht betroffen, hat keine Kind-Datasets.
Gruppe für den gemeinsamen Bereich, zweiter Nutzer (gleichrangig zu <PRIMARY_USER>):
groupadd teilen
usermod -aG teilen <PRIMARY_USER>
useradd -M -s /bin/bash -d /srv/freigabe/<SECONDARY_USER> <SECONDARY_USER>
usermod -aG teilen <SECONDARY_USER>
smbpasswd -a <SECONDARY_USER> # + smbpasswd -e <SECONDARY_USER>
chpasswd <<< "<SECONDARY_USER>:<Passwort>" # gleiches Passwort wie SMB, wie beim Hauptnutzer
chown root:teilen /srv/freigabe/gemeinsam
chmod 2770 /srv/freigabe/gemeinsam # setgid: neue Dateien erben automatisch die Gruppe
chown <PRIMARY_USER>:<PRIMARY_USER> /srv/freigabe/<PRIMARY_USER> && chmod 0700 /srv/freigabe/<PRIMARY_USER>
chown <SECONDARY_USER>:<SECONDARY_USER> /srv/freigabe/<SECONDARY_USER> && chmod 0700 /srv/freigabe/<SECONDARY_USER>
Drei neue Samba-Shares zusätzlich zum bestehenden [freigabe] (der bleibt vorerst bestehen, siehe unten):
[gemeinsam]
path = /srv/freigabe/gemeinsam
browseable = yes
read only = no
guest ok = no
valid users = <PRIMARY_USER>, <SECONDARY_USER>
create mask = 0660
directory mask = 2770
[<PRIMARY_USER>]
path = /srv/freigabe/<PRIMARY_USER>
browseable = yes
read only = no
guest ok = no
valid users = <PRIMARY_USER>
create mask = 0600
directory mask = 0700
[<SECONDARY_USER>]
path = /srv/freigabe/<SECONDARY_USER>
browseable = yes
read only = no
guest ok = no
valid users = <SECONDARY_USER>
create mask = 0600
directory mask = 0700
⚠️ [freigabe] (das alte, flache Share) bewusst NICHT entfernt. Die bereits vorhandenen Inhalte liegen weiterhin flach unter /srv/freigabe und wurden noch nicht in die neuen Bereiche einsortiert — das erfordert eine bewusste Entscheidung je Ordner, kein automatisches Verschieben. Bis diese Migration gemacht ist, bleibt [freigabe] der einzige Weg an die alten Daten. Danach kann es entfernt werden.
Zweite Filebrowser-Instanz für Backups (read-only): analog zum Gast-Filebrowser-Muster (siehe 09-gast-zugang.md), aber für den Eigentümer bestimmt statt für Gäste — dedizierter User, eigener Port, -a 0.0.0.0 (LAN/Tailnet, nicht auf eine einzelne Tailscale-IP beschränkt wie beim Gast-Pendant).
useradd -M -s /usr/sbin/nologin filebackup
mkdir -p /var/lib/filebrowser-backup && chown filebackup:filebackup /var/lib/filebrowser-backup
su -s /bin/bash filebackup -c '/usr/local/bin/filebrowser config init -d /var/lib/filebrowser-backup/filebrowser.db'
su -s /bin/bash filebackup -c '/usr/local/bin/filebrowser config set -a 0.0.0.0 -p 8082 -r /srv/backups -d /var/lib/filebrowser-backup/filebrowser.db'
su -s /bin/bash filebackup -c "/usr/local/bin/filebrowser users add <PRIMARY_USER> '<Passwort>' -d /var/lib/filebrowser-backup/filebrowser.db"
# Danach explizit auf read-only reduzieren - der Default nach "users add" erlaubt Create/Rename/Modify/Delete!
/usr/local/bin/filebrowser users update <PRIMARY_USER> --perm.create=false --perm.delete=false --perm.modify=false --perm.rename=false --perm.share=false -d /var/lib/filebrowser-backup/filebrowser.db
⚠️ NICHT TUN: sich nach filebrowser users add auf Read-only verlassen, ohne die Permissions explizit zu setzen. Der Default gewährt Create/Rename/Modify/Delete/Share — für einen bewusst schreibgeschützten Backup-Browser müssen diese fünf Flags einzeln per users update auf false gesetzt werden.
Fülllstand-Tracking (Erweiterung des HDD-/ZFS-Tracking-Scripts, siehe 08-monitoring.md): die drei neuen Datasets zur bestehenden zfs list-Zeile ergänzt, plus ein einfacher Schwellenwert-Alert (≥80 % Belegung, Statefile verhindert Alert-Spam bei jedem 5-Minuten-Lauf) direkt im selben Script statt eines Grafana-nativen Alerts — konsistent mit dem Rest des Projekts, das Alerting immer über Skript+ntfy statt Grafana-Alerting löst.
6. Foto-Backup vom Handy: Syncthing — ABGEBAUT
Nicht mehr in Betrieb. Scheiterte praktisch an einem Android-SAF-Scan-Hänger (App friert beim ersten Voll-Scan der SD-Karte ein, ANR) — stattdessen Umstieg auf eine Nextcloud-Instanz mit External-Storage-Anbindung auf denselben Pfad, ohne diese Redundanz. Komplett zurückgebaut: beide systemd-Instanzen gestoppt/deaktiviert,
syncthing-Paket gepurged, die beiden.syncthing-Zustandsordner gelöscht (die eigentlichen Fotos blieben unangetastet), beide Kuma-Monitore entfernt.Der folgende Abschnitt bleibt als Referenz stehen, falls Syncthing je wieder gebraucht wird (z.B. für einen Anwendungsfall ohne Nextcloud-Client).
War an die Struktur aus Abschnitt 5 blockiert (Quota-Bereiche mussten zuerst stehen). Bewusst kein gemeinsamer Syncthing-Prozess für beide Konten: eine eigene Instanz je Nutzer, symmetrisch zum Drei-Ebenen-Modell aus Abschnitt 5 — Fotos landen im jeweils persönlichen, quota-geschützten Bereich (/srv/freigabe/<user>/Fotos), nicht in gemeinsam.
apt install -y syncthing # in Debian <DEBIAN_RELEASE> direkt in main, kein Drittanbieter-Repo noetig
mkdir -p /srv/freigabe/<PRIMARY_USER>/Fotos /srv/freigabe/<SECONDARY_USER>/Fotos
chown <PRIMARY_USER>:<PRIMARY_USER> /srv/freigabe/<PRIMARY_USER>/Fotos
chown <SECONDARY_USER>:<SECONDARY_USER> /srv/freigabe/<SECONDARY_USER>/Fotos
runuser -u <PRIMARY_USER> -- syncthing generate --home=/srv/freigabe/<PRIMARY_USER>/.syncthing \
--no-default-folder --gui-user=<PRIMARY_USER> --gui-password='<PW>'
runuser -u <SECONDARY_USER> -- syncthing generate --home=/srv/freigabe/<SECONDARY_USER>/.syncthing \
--no-default-folder --gui-user=<SECONDARY_USER> --gui-password='<PW>'
⚠️ NICHT TUN: --home weglassen und sich auf den Unix-Homedir-Default verlassen. Der Unix-Home des Hauptnutzers ist historisch /srv/freigabe (der alte flache Share-Root, Altlast von vor der Quota-Umstellung) — ohne explizites --home wäre die Syncthing-Config dort gelandet, mitten im gemeinsamen/unmigrierten Altbestand statt im eigenen quota-geschützten Bereich.
config.xml je Instanz manuell ergänzt (kein CLI-Flag dafür): GUI-Adresse auf die Tailscale-IP von CT101 gebunden (<TS_IP_NAS>:8384/:8385, nicht LAN — gleiches Muster wie Vaultwarden/Forgejo), plus ein Fotos-Ordner mit type="receiveonly" (eigene Folder-ID je Nutzer, Pfad /srv/freigabe/<user>/Fotos). Empfangs-only ist zentral fürs Konzept: Handy = "Nur senden", Server = "Nur empfangen" — Löschungen am Handy löschen nichts auf dem Server.
mkdir -p /etc/systemd/system/syncthing@<PRIMARY_USER>.service.d
cat > /etc/systemd/system/syncthing@<PRIMARY_USER>.service.d/override.conf <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/bin/syncthing serve --no-browser --no-restart --logflags=0 --home=/srv/freigabe/<PRIMARY_USER>/.syncthing
EOF
# analog fuer den zweiten Nutzer mit --home=/srv/freigabe/<SECONDARY_USER>/.syncthing
systemctl daemon-reload
systemctl enable --now syncthing@<PRIMARY_USER>.service syncthing@<SECONDARY_USER>.service
⚠️ NICHT TUN: beide Instanzen mit dem default-Sync-Listener laufen lassen. Beide Prozesse teilen sich denselben Container/Netzwerk-Namespace — die zweite Instanz, die startet, scheitert lautlos beim Binden von TCP/QUIC 0.0.0.0:22000 ("address already in use") und ist nur noch über Relay erreichbar (langsamer, weniger privat). Fix: der zweiten Instanz in config.xml unter <options> explizite <listenAddress>-Einträge auf Port 22001 geben (tcp://0.0.0.0:22001, quic://0.0.0.0:22001, plus den Relay-Eintrag unverändert), dann neu starten. GUI-Ports (8384/8385) waren dagegen von Anfang an über den generate-Aufruf getrennt und kollidierten nicht.
Pairing (menschlicher Schritt, nicht automatisierbar): App "Syncthing" auf jedem Handy (F-Droid/Play Store), dort den Server als Remote-Gerät per Device-ID/QR aus der Web-GUI hinzufügen, Server bestätigt eingehende Geräte-Anfrage in der GUI. Auf dem Handy den DCIM/Camera-Ordner als Folder-Typ "Nur senden" mit dem Server teilen, Folder-ID passend zur serverseitigen setzen. Mehrere Handys insgesamt, alle gegen die jeweils eigene Instanz.
⚠️ Vor dem ersten Voll-Sync die tatsächliche DCIM-Größe gegen die 200-GB-Quota des persönlichen Bereichs prüfen — die Quota schützt zwar vor einem Pool-Overrun, ein DCIM-Ordner, der die Quota sprengt, bricht den Sync trotzdem einfach ab statt sauber zu laufen.
Monitoring: zwei TCP-Port-Checks in Kuma (Port 8384/8385 über die Tailscale-IP) — Details 08-monitoring.md.
Verifikation
smbclient -L //<LAN_IP_NAS> -U <PRIMARY_USER>%'<PASSWORT>'
sftp <PRIMARY_USER>@<LAN_IP_NAS>
curl -s -o /dev/null -w "%{http_code}\n" http://<LAN_IP_NAS>:8080/
curl -s -o /dev/null -w "%{http_code}\n" http://<LAN_IP_NAS>:8082/ # Backup-Filebrowser
smbclient -L //<LAN_IP_NAS> -U <SECONDARY_USER>%'<Passwort>' # sollte gemeinsam/<SECONDARY_USER> zeigen, nicht <PRIMARY_USER>/freigabe/backups
curl -s -o /dev/null -w "%{http_code}\n" http://<TS_IP_NAS>:8384/ # Syncthing Hauptnutzer (Tailnet-only)
curl -s -o /dev/null -w "%{http_code}\n" http://<TS_IP_NAS>:8385/ # Syncthing zweiter Nutzer (Tailnet-only)
Rebuild-Runbooks
Aufbau
- Übersicht
- PVE-Host
- AdGuard · CT100
- NAS · CT101
- Management-Desktop · CT102
- Vaultwarden · CT103
- VPS · STRATO
- Backup & Restore
- Monitoring · CT104
- Gast-Zugang
Referenz