diff --git a/hosts/brontes/configuration.nix b/hosts/brontes/configuration.nix index a83739e..b269fd8 100644 --- a/hosts/brontes/configuration.nix +++ b/hosts/brontes/configuration.nix @@ -46,7 +46,11 @@ "/home/ny/Downloads" "/home/ny/notes" ]; - target = "/home/ny/test"; + target = { + location = "backup"; + type = "rsyncd"; + host = "shan"; + }; incremental.enable = true; }; }; diff --git a/hosts/raptus/headscale.nix b/hosts/raptus/headscale.nix index 0fc20a3..ec3bae1 100644 --- a/hosts/raptus/headscale.nix +++ b/hosts/raptus/headscale.nix @@ -121,6 +121,15 @@ mkIf config.services.headscale.enable { acls = [ (mkAcl [ "tag:client" ] [ "tag:client:*" ]) # client -> client (mkAcl [ "tag:client" ] [ "tag:server:*" ]) # client -> server + { + action = "accept"; + src = [ + "tag:client" + "tag:server" + ]; + proto = "rsync"; # optional + dst = [ "tag:backup" ]; + } ]; ssh = [ @@ -133,6 +142,7 @@ mkIf config.services.headscale.enable { tags = [ "tag:client" "tag:server" + "tag:backup" ]; tagOwners = diff --git a/hosts/shan/configuration.nix b/hosts/shan/configuration.nix index 10740a0..a9a6d9e 100644 --- a/hosts/shan/configuration.nix +++ b/hosts/shan/configuration.nix @@ -68,6 +68,24 @@ isExitNode = true; tags = [ "server" ]; }; + + services.rsyncd = { + enable = true; + port = 9523; + settings = { + globalSection = { + address = "100.64.0.4"; + }; + sections = { + backup = { + path = "/var/backups/test"; + comment = "ftp export area"; + "write only" = true; + "read only" = false; + }; + }; + }; + }; }; services = { @@ -78,8 +96,11 @@ openFirewall = true; host = "0.0.0.0"; }; + }; + networking.firewall.allowedTCPPorts = [ 9523 ]; + environment.systemPackages = map lib.lowPrio [ pkgs.curl ]; diff --git a/options/default.nix b/options/default.nix index 4e9403b..b445ed7 100644 --- a/options/default.nix +++ b/options/default.nix @@ -6,5 +6,7 @@ ./system ./container ./server + + ./fixes ]; } diff --git a/options/fixes/default.nix b/options/fixes/default.nix new file mode 100644 index 0000000..ae8fe3d --- /dev/null +++ b/options/fixes/default.nix @@ -0,0 +1 @@ +{ imports = [ ./rsyncd.nix ]; } diff --git a/options/fixes/rsyncd.nix b/options/fixes/rsyncd.nix new file mode 100644 index 0000000..75074cb --- /dev/null +++ b/options/fixes/rsyncd.nix @@ -0,0 +1,134 @@ +{ + config, + pkgs, + lib, + ... +}: +let + cfg = config.modules.services.rsyncd; + settingsFormat = pkgs.formats.iniWithGlobalSection { }; + configFile = settingsFormat.generate "rsyncd.conf" cfg.settings; +in +{ + options.modules.services.rsyncd = { + + enable = lib.mkEnableOption "the rsync daemon"; + + port = lib.mkOption { + default = 873; + type = lib.types.port; + description = "TCP port the daemon will listen on."; + }; + + settings = lib.mkOption { + type = lib.types.oneOf [ + settingsFormat.type + (pkgs.formats.ini { }).type # Retrocompatibility + ]; + default = { }; + example = { + globalSection = { + uid = "nobody"; + gid = "nobody"; + "use chroot" = true; + "max connections" = 4; + }; + sections = { + ftp = { + path = "/var/ftp/./pub"; + comment = "whole ftp area"; + }; + cvs = { + path = "/data/cvs"; + comment = "CVS repository (requires authentication)"; + "auth users" = [ + "tridge" + "susan" + ]; + "secrets file" = "/etc/rsyncd.secrets"; + }; + }; + }; + description = '' + Configuration for rsyncd. See + {manpage}`rsyncd.conf(5)`. + ''; + apply = + val: + if (lib.typeOf val == "ini") then + { + sections = lib.removeAttrs val [ "global" ]; + globalSection = lib.mkIf (lib.hasAttrs "global") val.global; + + } + else + val; + }; + + socketActivated = lib.mkOption { + default = false; + type = lib.types.bool; + description = "If enabled Rsync will be socket-activated rather than run persistently."; + }; + + }; + + config = lib.mkIf cfg.enable { + + modules.services.rsyncd.settings.globalSection.port = toString cfg.port; + + systemd = + let + serviceConfigSecurity = { + ProtectSystem = "full"; + PrivateDevices = "on"; + NoNewPrivileges = "on"; + }; + in + { + services.rsync = lib.mkForce { + enable = !cfg.socketActivated; + aliases = [ "rsyncd.service" ]; + + description = "fast remote file copy program daemon"; + after = [ "network.target" ]; + documentation = [ + "man:rsync(1)" + "man:rsyncd.conf(5)" + ]; + + serviceConfig = serviceConfigSecurity // { + ExecStart = "${pkgs.rsync}/bin/rsync --daemon --no-detach --config=${configFile}"; + RestartSec = 1; + }; + + wantedBy = [ "multi-user.target" ]; + }; + + services."rsync@" = lib.mkForce { + description = "fast remote file copy program daemon"; + after = [ "network.target" ]; + + serviceConfig = serviceConfigSecurity // { + ExecStart = "${pkgs.rsync}/bin/rsync --daemon --config=${configFile}"; + StandardInput = "socket"; + StandardOutput = "inherit"; + StandardError = "journal"; + }; + }; + + sockets.rsync = lib.mkForce { + enable = cfg.socketActivated; + + description = "socket for fast remote file copy program daemon"; + conflicts = [ "rsync.service" ]; + + listenStreams = [ (toString cfg.port) ]; + socketConfig.Accept = true; + + wantedBy = [ "sockets.target" ]; + }; + }; + + }; +} diff --git a/options/services/rsync/default.nix b/options/services/rsync/default.nix index 749bcec..a9bef6a 100644 --- a/options/services/rsync/default.nix +++ b/options/services/rsync/default.nix @@ -9,8 +9,6 @@ let mkIf mkEnableOption mkOption - stringToCharacters - head concatStringsSep ; inherit (lib.types) @@ -19,6 +17,7 @@ let package enum str + port ; cfg = config.modules.services.rsync-backup; in @@ -32,9 +31,10 @@ in # - [ ] support # - [ ] nfs # - [ ] sftp - # - [x] ssh + # - [ ] ssh # - [x] local - # - retention limit for incremental backups + # - [x] rsyncd + # - [ ] retention limit for incremental backups options.modules.services.rsync-backup = { enable = mkEnableOption "rsync backups"; @@ -42,7 +42,6 @@ in sources = mkOption { type = listOf nonEmptyStr; default = [ ]; # TODO: validate if at least 1 element - apply = map (s: "\"${s}\""); description = '' A list of absolute paths to the files or folders that should get synchronized. It isn't necessary to wrap them in escaped double quotation marks. @@ -54,24 +53,42 @@ in default = ""; }; - target = mkOption { - type = nonEmptyStr; - description = '' - The target to where the data should be backed up. - If it is a directory, it will be created automatically by rsync. - Append a : to the string if it is an ssh target and ensure that passwordless access is available. - ''; - }; + target = { + location = mkOption { + type = nonEmptyStr; + description = '' + The target to where the data should be backed up. + If it is a directory, it will be created automatically by rsync. + Append a : to the string if it is an ssh target and ensure that passwordless access is available. + ''; + }; - targetType = mkOption { - type = enum [ - "local" - "ssh" - ]; - default = if head (stringToCharacters cfg.target) == "/" then "local" else "ssh"; - description = '' - Guesses if the given target is local or ssh, very rudimentary therefore it might be necessary to specify manually (or rewrite the logic). - ''; + finalLocation = mkOption { + type = nonEmptyStr; + default = + if cfg.target.type == "local" then + cfg.target.location + else if cfg.target.type == "rsyncd" then + "rsync://${cfg.target.host}/${cfg.target.location}" + else + throw "Unable to create location."; + }; + + type = mkOption { + type = enum [ + "local" + "rsyncd" + ]; + default = "local"; + description = '' + Guesses if the given target is local or ssh, very rudimentary therefore it might be necessary to specify manually (or rewrite the logic). + ''; + }; + + host = mkOption { + type = str; + default = ""; + }; }; package = mkOption { @@ -87,6 +104,7 @@ in default = [ "--archive" "--verbose" + "--mkpath" ]; }; @@ -96,13 +114,14 @@ in (mkIf cfg.incremental.enable "--delete") (mkIf (cfg.exclude != "") "--exclude=\"${cfg.exclude}\"") (mkIf cfg.incremental.enable "--backup-dir=\"${cfg.incremental.finalIncrementPath}\"") + "--port=${toString cfg.port}" ]; apply = concatStringsSep " "; }; interval = mkOption { type = nonEmptyStr; - default = "1d"; + default = "daily"; }; incremental = { @@ -117,7 +136,15 @@ in }; finalIncrementPath = mkOption { type = nonEmptyStr; - default = "${cfg.target}/increment/$(date +${cfg.incremental.format})/"; + default = "../${cfg.target.location}/increment/$(date +${cfg.incremental.format})/"; + description = '' + The directory in which the changes will be stored, the .. at the start is necessary because it is relative to + the target and we append `backup` to the target. + + -- cfg.location + | - backup/ + | - increment/ + ''; }; # TODO: add retention }; @@ -126,34 +153,54 @@ in type = nonEmptyStr; default = "rsync-backup"; }; + + user = mkOption { + type = str; + default = "rsync-backup"; + description = "User account under which Rsync runs."; + }; + + group = mkOption { + type = str; + default = "rsync-backup"; + description = "Group under which Rsync runs."; + }; + + port = mkOption { + type = port; + default = 9523; + }; }; config = mkIf cfg.enable { - systemd.timers.${cfg.unitName} = { enable = true; + wantedBy = [ "timers.target" ]; unitConfig = { Description = "Triggers rsync-backup regularly."; }; timerConfig = { Unit = "${cfg.unitName}.service"; - OnBootSec = cfg.interval; + OnCalendar = cfg.interval; + Persistent = true; }; }; systemd.services.${cfg.unitName} = { script = let - destination = if cfg.incremental.enable then "${cfg.target}/backup" else cfg.target; + destination = + if cfg.incremental.enable then "${cfg.target.finalLocation}/backup" else cfg.target.finalLocation; + sources = concatStringsSep " " (map (s: "\"${s}\"") cfg.sources); in '' ${cfg.package}/bin/rsync \ ${cfg.flagsFinal} \ - ${concatStringsSep " " cfg.sources} \ + ${sources} \ "${destination}" ''; - requires = mkIf (cfg.targetType == "ssh") [ "network.target" ]; + requires = mkIf (cfg.target.type == "rsyncd") [ "network.target" ]; unitConfig = { Description = "Backs up files from a source location to a specified destination."; }; @@ -161,7 +208,24 @@ in Type = "simple"; Restart = "on-failure"; RestartSec = 300; + PrivateDevices = true; + PrivateTmp = true; + ProtectSystem = "full"; + ReadOnlyPaths = concatStringsSep " " cfg.sources; + IPAddressAllow = "any"; }; }; + + users.users = mkIf (cfg.user == "rsync-backup") { + rsync-backup = { + useDefaultShell = true; + group = cfg.group; + isSystemUser = true; + }; + }; + + users.groups = mkIf (cfg.group == "rsync-backup") { + rsync-backup = { }; + }; }; }