nix/modules/webdav.nix
2025-09-11 19:47:49 +02:00

159 lines
No EOL
4.7 KiB
Nix

{ 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 ];
};
}