Files
infra-nix/modules/fileserver.nix
T
danlin c4938a6c05 Remove plaintext secrets, update SSH key, and upgrade to NixOS 25.11
Replace initialPassword with hashedPassword for danlin user, move
FileBrowser admin password to external environmentFile with restricted
secrets directory, update SSH authorized key, and bump nixpkgs to 25.11.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 17:44:03 +01:00

348 lines
11 KiB
Nix

{ config, pkgs, ... }:
{
########################################
# Samba File Sharing
########################################
services.samba = {
enable = true;
openFirewall = true;
# Enable nmbd for NetBIOS name resolution and browsing (legacy Windows/macOS)
nmbd.enable = true;
# Enable WSD discovery (Windows) and mDNS/NetBIOS advertisement improvements via wsdd + Avahi.
# "smb encrypt" set to desired to avoid discovery issues with some clients while still allowing encryption.
settings = {
global = {
workgroup = "WORKGROUP";
"server string" = "NixFiles";
"netbios name" = "fileserver";
security = "user";
"map to guest" = "bad user";
"unix extensions" = "no";
# macOS optimizations
"vfs objects" = "catia fruit streams_xattr";
"fruit:aapl" = "yes";
"fruit:metadata" = "stream";
"fruit:resource" = "stream";
"fruit:model" = "MacSamba";
"fruit:advertise_fullsync" = "yes";
"ea support" = "yes";
# Time Machine support
"fruit:time machine" = "yes";
"fruit:time machine max size" = "500G";
# Prefer encryption but do not require it for basic browsing/guest visibility
"smb encrypt" = "desired";
# Better macOS discovery
"min protocol" = "SMB2";
"max protocol" = "SMB3";
"server role" = "standalone server";
# Allow Apple extended attributes & full sync advertisement already configured above.
};
daten = {
path = "/data/daten";
browseable = "yes";
"read only" = "no";
"valid users" = "danlin";
"fruit:time machine" = "no";
};
daten-share = {
path = "/data/daten/share";
browseable = "yes";
"read only" = "no";
"guest ok" = "yes"; # Gastzugriff erlaubt
public = "yes"; # Alias für guest ok
# Removed "valid users" to allow true guest access; danlin can still write via group permissions
"fruit:time machine" = "no";
};
timemachine = {
path = "/data/backup/timemachine";
browseable = "yes";
"read only" = "no";
"valid users" = "danlin";
"fruit:time machine" = "yes";
"fruit:time machine max size" = "500G";
};
};
};
########################################
# Network Discovery Services (Avahi + WSD)
########################################
# Avahi for macOS Finder discovery (mDNS) and general _smb._tcp advertising
services.avahi = {
enable = true;
nssmdns4 = true; # Provide .local resolution
openFirewall = true;
publish = {
enable = true;
userServices = true;
};
# Explicit SMB service advertisement for macOS Finder
extraServiceFiles = {
smb = ''
<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name replace-wildcards="yes">%h</name>
<service>
<type>_smb._tcp</type>
<port>445</port>
</service>
<service>
<type>_device-info._tcp</type>
<port>0</port>
<txt-record>model=TimeCapsule8,119</txt-record>
</service>
<service>
<type>_adisk._tcp</type>
<port>9</port>
<txt-record>sys=waMa=0,adVF=0x100</txt-record>
<txt-record>dk0=adVN=timemachine,adVF=0x82</txt-record>
</service>
</service-group>
'';
};
};
# Windows network neighborhood discovery via Web Services for Devices (WSD)
# NixOS does not provide a `services.wsdd` option in this release, so define a systemd unit.
systemd.services.wsdd = {
description = "Web Services Discovery daemon for Samba (wsdd)";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" "samba.service" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.wsdd}/bin/wsdd --hostname fileserver --workgroup WORKGROUP --interface ens18";
Restart = "on-failure";
RestartSec = 5;
};
};
########################################
# Directory Structure
########################################
systemd.tmpfiles.rules = [
"d /data 0775 root root -"
"d /data/daten 0775 danlin users -"
"d /data/daten/share 0775 danlin users -"
# Gitea (runs as git user inside container, uid 1000)
"d /data/gitea 0755 root root -"
# DJ-Struktur
"d /data/daten/DJing 0775 danlin users -"
"d /data/daten/DJing/Music 0775 danlin users -"
"d /data/daten/DJing/Data 0775 danlin users -"
"d /data/daten/DJing/Inbox 0775 danlin users -"
# Time Machine backup
"d /data/backup 0775 root root -"
"d /data/backup/timemachine 0775 danlin users -"
# Secrets
"d /data/secrets 0700 root root -"
# dj-beets project
"d /opt/dj-beets 0755 root root -"
];
########################################
# Virtualization & Containers
########################################
# Virtualization
virtualisation.podman.enable = true;
virtualisation.oci-containers.backend = "podman";
virtualisation.oci-containers.containers.filebrowser = {
image = "gtstef/filebrowser:stable";
autoStart = true;
ports = [ "8080:80" ];
volumes = [ "/data:/srv" ];
environment = {
TZ = "Europe/Berlin";
};
environmentFiles = [ "/data/secrets/filebrowser.env" ];
};
virtualisation.oci-containers.containers.gitea = {
image = "gitea/gitea:1.25";
autoStart = true;
ports = [ "3000:3000" "2222:2222" ];
volumes = [
"/data/gitea:/data"
];
environment = {
USER_UID = "1000";
USER_GID = "1000";
TZ = "Europe/Berlin";
GITEA__server__DOMAIN = "git.home.lindenfelser.de";
GITEA__server__ROOT_URL = "https://git.home.lindenfelser.de/";
GITEA__server__SSH_DOMAIN = "git.home.lindenfelser.de";
GITEA__server__SSH_PORT = "2222";
GITEA__server__SSH_LISTEN_PORT = "2222";
GITEA__server__START_SSH_SERVER = "true";
};
};
virtualisation.oci-containers.containers.dj-beets-cli = {
image = "dj-beets:latest";
autoStart = false; # Run manually or via systemd service
volumes = [
"/data/daten/DJing:/data/daten/DJing"
"/opt/dj-beets/config.yaml:/etc/beets/config.yaml:ro"
"/opt/dj-beets/beatport_token.json:/opt/dj-beets/beatport_token.json"
];
environment = {
BEETS_CONFIG = "/etc/beets/config.yaml";
};
cmd = [ "tail" "-f" "/dev/null" ]; # Keep container alive
};
virtualisation.oci-containers.containers.dj-beets-web = {
image = "dj-beets:latest";
autoStart = true;
ports = [ "8337:8337" ];
volumes = [
"/data/daten/DJing:/data/daten/DJing"
"/opt/dj-beets/config.yaml:/etc/beets/config.yaml:ro"
"/opt/dj-beets/beatport_token.json:/opt/dj-beets/beatport_token.json"
];
environment = {
BEETS_CONFIG = "/etc/beets/config.yaml";
};
cmd = [ "beet" "web" ];
};
########################################
# Backup Services
########################################
systemd.services.rclone-backup = {
description = "Backup /data to Google Drive via rclone";
serviceConfig = {
Type = "oneshot";
ExecStart = ''
${pkgs.rclone}/bin/rclone sync /data gdrive:backup-daten \
--fast-list \
--drive-stop-on-upload-limit \
--log-file=/var/log/rclone-backup.log \
--log-level=INFO
'';
User = "root";
};
};
systemd.timers.rclone-backup = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "03:00";
Persistent = true;
};
};
########################################
# System Packages
########################################
environment.systemPackages = [
pkgs.git
pkgs.tmux
pkgs.htop
pkgs.podman-compose
# Beets CLI wrapper
(pkgs.writeShellScriptBin "beet" ''
exec ${pkgs.podman}/bin/podman run --rm -it \
-v /data/daten/DJing:/data/daten/DJing \
-v /opt/dj-beets/config.yaml:/etc/beets/config.yaml:ro \
-v /opt/dj-beets/beatport_token.json:/opt/dj-beets/beatport_token.json \
-e BEETS_CONFIG=/etc/beets/config.yaml \
-u $(id -u):$(id -g) \
dj-beets:latest \
beet "$@"
'')
];
########################################
# DJ-Beets Services
########################################
# Systemd service to build dj-beets image on deploy
systemd.services.dj-beets-build = {
description = "Build dj-beets Docker image";
wantedBy = [ "multi-user.target" ];
before = [ "podman-dj-beets-web.service" ];
after = [ "podman.service" ];
wants = [ "podman.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = pkgs.writeShellScript "build-dj-beets" ''
set -euo pipefail
if [ ! -f /opt/dj-beets/Dockerfile ]; then
echo "ERROR: Dockerfile not found at /opt/dj-beets" >&2
echo "Deploy with 'make deploy-fileserver' first" >&2
exit 1
fi
cd /opt/dj-beets
${pkgs.podman}/bin/podman build -t dj-beets:latest .
echo "dj-beets image built successfully"
'';
User = "root";
};
};
# Auto-import service using Docker container
systemd.services.dj-beets-autoimport = {
description = "Auto-import music into beets library via Docker";
after = [ "dj-beets-build.service" ];
wants = [ "dj-beets-build.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = pkgs.writeShellScript "beets-autoimport" ''
set -euo pipefail
INBOX="/data/daten/DJing/Inbox"
LOG="/data/daten/DJing/Data/BeetsAutoImport.log"
# Check if inbox has files
if ! find "$INBOX" -mindepth 1 -maxdepth 1 -type f -print -quit | grep -q .; then
echo "[$(date)] No files in Inbox, skipping" >> "$LOG"
exit 0
fi
echo "[$(date)] Starting beets import" >> "$LOG"
${pkgs.podman}/bin/podman run --rm \
-v /data/daten/DJing:/data/daten/DJing \
-v /opt/dj-beets/config.yaml:/etc/beets/config.yaml:ro \
-v /opt/dj-beets/beatport_token.json:/opt/dj-beets/beatport_token.json \
-e BEETS_CONFIG=/etc/beets/config.yaml \
localhost/dj-beets:latest \
beet import -q /data/daten/DJing/Inbox >> "$LOG" 2>&1
'';
# Use root so the service can access the root Podman image storage
User = "root";
Group = "root";
};
};
systemd.timers.dj-beets-autoimport = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*:0/10"; # Every 10 minutes
Persistent = true;
};
};
########################################
# Firewall
########################################
networking.firewall.enable = true;
networking.firewall.allowedTCPPorts = [ 22 139 445 8080 8337 5357 3000 2222 ]; # 5357 WSD
networking.firewall.allowedUDPPorts = [ 137 138 3702 5353 ]; # NetBIOS + WSD (3702) + mDNS (5353)
}