diff --git a/hosts/nixos/hs/system.nix b/hosts/nixos/hs/system.nix index c3376d9..56f1638 100644 --- a/hosts/nixos/hs/system.nix +++ b/hosts/nixos/hs/system.nix @@ -10,6 +10,7 @@ ../../../modules/traefik.nix ../../../modules/samba.nix ../../../modules/borg.nix + ../../../modules/webdav.nix ]; # GRUB bootloader with ZFS support @@ -225,6 +226,23 @@ # Samba file sharing configuration services.samba-custom = { enable = false; }; + # WebDAV file server configuration + services.webdav-server = { + enable = true; + port = 5009; + servePath = "/mnt/storage/Media/NSFW"; + auth = { + username = "yanlin"; + passwordFile = "/etc/webdav-password"; + }; + readOnly = false; + allowUpload = true; + allowDelete = true; + allowSearch = true; + allowSymlink = false; + hideDotFiles = true; + }; + # Borg backup configuration services.borgbackup-custom = { enable = true; diff --git a/hosts/nixos/vps/proxy.nix b/hosts/nixos/vps/proxy.nix index 5990a25..9bd7814 100644 --- a/hosts/nixos/vps/proxy.nix +++ b/hosts/nixos/vps/proxy.nix @@ -149,6 +149,19 @@ }]; }; }; + + # WebDAV file server + files = { + rule = "Host(`files.yanlincs.com`)"; + entrypoints = "websecure"; + service = "files"; + tls = { + certResolver = "cloudflare"; + domains = [{ + main = "*.yanlincs.com"; + }]; + }; + }; }; services = { # Redirect service @@ -249,6 +262,15 @@ }]; }; }; + + # WebDAV file server backend (via WireGuard) + files = { + loadBalancer = { + servers = [{ + url = "http://10.2.2.20:5009"; + }]; + }; + }; }; middlewares = { # Redirect middleware diff --git a/modules/webdav.nix b/modules/webdav.nix new file mode 100644 index 0000000..9ccb757 --- /dev/null +++ b/modules/webdav.nix @@ -0,0 +1,159 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.webdav-server; +in +{ + options.services.webdav-server = { + enable = mkEnableOption "WebDAV file server using dufs"; + + port = mkOption { + type = types.port; + default = 5009; + description = "Port to listen on"; + }; + + address = mkOption { + type = types.str; + default = "0.0.0.0"; + description = "Address to bind to"; + }; + + servePath = mkOption { + type = types.str; + description = "Path to serve via WebDAV"; + }; + + auth = mkOption { + type = types.nullOr (types.submodule { + options = { + username = mkOption { + type = types.str; + description = "Username for authentication"; + }; + passwordFile = mkOption { + type = types.str; + description = "Path to file containing password"; + }; + }; + }); + default = null; + description = "Authentication configuration"; + }; + + readOnly = mkOption { + type = types.bool; + default = false; + description = "Make the WebDAV share read-only"; + }; + + allowUpload = mkOption { + type = types.bool; + default = true; + description = "Allow file uploads"; + }; + + allowDelete = mkOption { + type = types.bool; + default = true; + description = "Allow file deletion"; + }; + + allowSearch = mkOption { + type = types.bool; + default = true; + description = "Enable search functionality"; + }; + + allowSymlink = mkOption { + type = types.bool; + default = false; + description = "Allow serving symbolic links"; + }; + + hideDotFiles = mkOption { + type = types.bool; + default = true; + description = "Hide dot files from listing"; + }; + }; + + config = mkIf cfg.enable { + # Install dufs package + environment.systemPackages = [ pkgs.dufs ]; + + # Set password file permissions if auth is enabled + systemd.tmpfiles.rules = mkIf (cfg.auth != null) [ + "z ${cfg.auth.passwordFile} 0640 yanlin users - -" + ]; + + # Create systemd service for dufs + systemd.services.webdav-server = { + description = "WebDAV server using dufs"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = let + permissionArgs = concatStringsSep " " ( + (optional cfg.readOnly "--render-index") ++ + (optional (!cfg.allowUpload) "--no-upload") ++ + (optional (!cfg.allowDelete) "--no-delete") ++ + (optional (!cfg.allowSearch) "--no-search") ++ + (optional cfg.allowSymlink "--allow-symlink") ++ + (optional cfg.hideDotFiles "--hidden '.*, .*/'") + ); + in { + Type = "simple"; + ExecStart = let + startScript = pkgs.writeShellScript "dufs-start" '' + ${if cfg.auth != null then '' + if [ ! -f "${cfg.auth.passwordFile}" ]; then + echo "Error: Password file ${cfg.auth.passwordFile} does not exist" + exit 1 + fi + AUTH_PASSWORD=$(cat ${cfg.auth.passwordFile} | tr -d '\n') + exec ${pkgs.dufs}/bin/dufs \ + --bind ${cfg.address} \ + --port ${toString cfg.port} \ + --auth "${cfg.auth.username}:$AUTH_PASSWORD@/:rw" \ + ${permissionArgs} \ + "${cfg.servePath}" + '' else '' + exec ${pkgs.dufs}/bin/dufs \ + --bind ${cfg.address} \ + --port ${toString cfg.port} \ + ${permissionArgs} \ + "${cfg.servePath}" + ''} + ''; + in "${startScript}"; + Restart = "always"; + RestartSec = "10"; + User = "yanlin"; + Group = "users"; + UMask = "0022"; # Creates dirs as 755, files as 644 + + # Security hardening + PrivateTmp = true; + ProtectSystem = "strict"; + ReadWritePaths = if cfg.readOnly then [] else [ cfg.servePath ]; + ReadOnlyPaths = if cfg.readOnly then [ cfg.servePath ] else []; + NoNewPrivileges = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" "AF_NETLINK" ]; # Added AF_UNIX and AF_NETLINK for interface enumeration + RestrictNamespaces = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + }; + }; + + # Open firewall port if needed (usually not needed as it goes through Traefik) + # networking.firewall.allowedTCPPorts = [ cfg.port ]; + }; +} \ No newline at end of file