summary refs log tree commit diff
path: root/nixos/modules/services/networking/radicale.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/networking/radicale.nix')
-rw-r--r--nixos/modules/services/networking/radicale.nix204
1 files changed, 204 insertions, 0 deletions
diff --git a/nixos/modules/services/networking/radicale.nix b/nixos/modules/services/networking/radicale.nix
new file mode 100644
index 00000000000..c6c40777ed7
--- /dev/null
+++ b/nixos/modules/services/networking/radicale.nix
@@ -0,0 +1,204 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.radicale;
+
+  format = pkgs.formats.ini {
+    listToValue = concatMapStringsSep ", " (generators.mkValueStringDefault { });
+  };
+
+  pkg = if isNull cfg.package then
+    pkgs.radicale
+  else
+    cfg.package;
+
+  confFile = if cfg.settings == { } then
+    pkgs.writeText "radicale.conf" cfg.config
+  else
+    format.generate "radicale.conf" cfg.settings;
+
+  rightsFile = format.generate "radicale.rights" cfg.rights;
+
+  bindLocalhost = cfg.settings != { } && !hasAttrByPath [ "server" "hosts" ] cfg.settings;
+
+in {
+  options.services.radicale = {
+    enable = mkEnableOption "Radicale CalDAV and CardDAV server";
+
+    package = mkOption {
+      description = "Radicale package to use.";
+      # Default cannot be pkgs.radicale because non-null values suppress
+      # warnings about incompatible configuration and storage formats.
+      type = with types; nullOr package // { inherit (package) description; };
+      default = null;
+      defaultText = literalExpression "pkgs.radicale";
+    };
+
+    config = mkOption {
+      type = types.str;
+      default = "";
+      description = ''
+        Radicale configuration, this will set the service
+        configuration file.
+        This option is mutually exclusive with <option>settings</option>.
+        This option is deprecated.  Use <option>settings</option> instead.
+      '';
+    };
+
+    settings = mkOption {
+      type = format.type;
+      default = { };
+      description = ''
+        Configuration for Radicale. See
+        <link xlink:href="https://radicale.org/3.0.html#documentation/configuration" />.
+        This option is mutually exclusive with <option>config</option>.
+      '';
+      example = literalExpression ''
+        server = {
+          hosts = [ "0.0.0.0:5232" "[::]:5232" ];
+        };
+        auth = {
+          type = "htpasswd";
+          htpasswd_filename = "/etc/radicale/users";
+          htpasswd_encryption = "bcrypt";
+        };
+        storage = {
+          filesystem_folder = "/var/lib/radicale/collections";
+        };
+      '';
+    };
+
+    rights = mkOption {
+      type = format.type;
+      description = ''
+        Configuration for Radicale's rights file. See
+        <link xlink:href="https://radicale.org/3.0.html#documentation/authentication-and-rights" />.
+        This option only works in conjunction with <option>settings</option>.
+        Setting this will also set <option>settings.rights.type</option> and
+        <option>settings.rights.file</option> to approriate values.
+      '';
+      default = { };
+      example = literalExpression ''
+        root = {
+          user = ".+";
+          collection = "";
+          permissions = "R";
+        };
+        principal = {
+          user = ".+";
+          collection = "{user}";
+          permissions = "RW";
+        };
+        calendars = {
+          user = ".+";
+          collection = "{user}/[^/]+";
+          permissions = "rw";
+        };
+      '';
+    };
+
+    extraArgs = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = "Extra arguments passed to the Radicale daemon.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.settings == { } || cfg.config == "";
+        message = ''
+          The options services.radicale.config and services.radicale.settings
+          are mutually exclusive.
+        '';
+      }
+    ];
+
+    warnings = optional (isNull cfg.package && versionOlder config.system.stateVersion "17.09") ''
+      The configuration and storage formats of your existing Radicale
+      installation might be incompatible with the newest version.
+      For upgrade instructions see
+      https://radicale.org/2.1.html#documentation/migration-from-1xx-to-2xx.
+      Set services.radicale.package to suppress this warning.
+    '' ++ optional (isNull cfg.package && versionOlder config.system.stateVersion "20.09") ''
+      The configuration format of your existing Radicale installation might be
+      incompatible with the newest version.  For upgrade instructions see
+      https://github.com/Kozea/Radicale/blob/3.0.6/NEWS.md#upgrade-checklist.
+      Set services.radicale.package to suppress this warning.
+    '' ++ optional (cfg.config != "") ''
+      The option services.radicale.config is deprecated.
+      Use services.radicale.settings instead.
+    '';
+
+    services.radicale.settings.rights = mkIf (cfg.rights != { }) {
+      type = "from_file";
+      file = toString rightsFile;
+    };
+
+    environment.systemPackages = [ pkg ];
+
+    users.users.radicale = {
+      isSystemUser = true;
+      group = "radicale";
+    };
+
+    users.groups.radicale = {};
+
+    systemd.services.radicale = {
+      description = "A Simple Calendar and Contact Server";
+      after = [ "network.target" ];
+      requires = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = concatStringsSep " " ([
+          "${pkg}/bin/radicale" "-C" confFile
+        ] ++ (
+          map escapeShellArg cfg.extraArgs
+        ));
+        User = "radicale";
+        Group = "radicale";
+        StateDirectory = "radicale/collections";
+        StateDirectoryMode = "0750";
+        # Hardening
+        CapabilityBoundingSet = [ "" ];
+        DeviceAllow = [ "/dev/stdin" ];
+        DevicePolicy = "strict";
+        IPAddressAllow = mkIf bindLocalhost "localhost";
+        IPAddressDeny = mkIf bindLocalhost "any";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        ReadWritePaths = lib.optional
+          (hasAttrByPath [ "storage" "filesystem_folder" ] cfg.settings)
+          cfg.settings.storage.filesystem_folder;
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+        UMask = "0027";
+        WorkingDirectory = "/var/lib/radicale";
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ aneeshusa infinisil dotlambda ];
+}