From 5dd7402a832000ad2d1610df9e5b974f26a0d085 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sun, 3 May 2026 02:27:56 +0200 Subject: [PATCH 1/2] nixos/rustical: init Co-Authored-By: PopeRigby --- .../manual/release-notes/rl-2605.section.md | 2 + nixos/modules/module-list.nix | 1 + nixos/modules/services/web-apps/rustical.nix | 183 ++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 nixos/modules/services/web-apps/rustical.nix diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index a652c7a3657c..e71505722a69 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -110,6 +110,8 @@ - [dsearch](https://github.com/AvengeMedia/danksearch), a fast filesystem search service with fuzzy matching. Available as [programs.dsearch](#opt-programs.dsearch.enable). +- [Rustical](https://github.com/lennart-k/rustical), a CalDav/CardDav server aiming to be simple, fast and passwordless. Available as [services.rustical](options.html#opt-services.rustical.enable). + - [Elephant](https://github.com/abenz1267/elephant), a data provider service and backend for building custom application launchers. Available as [services.elephant](#opt-services.elephant.enable). - [Dunst](https://github.com/dunst-project/dunst), a lightweight and customizable notification daemon. Available as [services.dunst](#opt-services.dunst.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 74d4753a913b..fc6991e84a3b 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1768,6 +1768,7 @@ ./services/web-apps/rimgo.nix ./services/web-apps/rss-bridge.nix ./services/web-apps/rsshub.nix + ./services/web-apps/rustical.nix ./services/web-apps/rutorrent.nix ./services/web-apps/screego.nix ./services/web-apps/selfoss.nix diff --git a/nixos/modules/services/web-apps/rustical.nix b/nixos/modules/services/web-apps/rustical.nix new file mode 100644 index 000000000000..0764f384ab0d --- /dev/null +++ b/nixos/modules/services/web-apps/rustical.nix @@ -0,0 +1,183 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) + mkIf + mkEnableOption + mkOption + mkPackageOption + types + ; + + cfg = config.services.rustical; + + format = pkgs.formats.toml { }; + configFile = format.generate "rustical.toml" cfg.settings; +in + +{ + options.services.rustical = { + enable = mkEnableOption "RustiCali CalDAV/CardDAV server"; + + package = mkPackageOption pkgs "rustical" { }; + + settings = mkOption { + description = '' + Your {file}`/etc/rustical/config.toml` as a Nix attribute set. + + Possible options can be found in the [Config struct]. A default + configuration can be viewed by running `rustical gen-config`. + + [Config struct]: https://lennart-k.github.io/rustical/_crate/rustical/config/struct.Config.html + ''; + type = types.submodule { + freeformType = format.type; + options = { + data_store.sqlite = { + db_url = mkOption { + type = types.path; + default = "/var/lib/rustical/db.sqlite3"; + description = '' + Path where the sqlite database is stored. + ''; + }; + }; + + frontend = { + enabled = mkEnableOption "the HTTP frontend" // { + default = true; + }; + }; + + http = { + host = mkOption { + type = types.str; + default = "[::1]"; + example = "[::]"; + description = '' + Host address to bind the HTTP service to. + + :::{.note} + Rustical expects to be hosted behind a reverse proxy that + provides HTTPS. Without HTTPS, the web frontend and some clients + (e.g. Apple Calendar) may not work. + ::: + ''; + }; + + port = mkOption { + type = types.port; + default = 4000; + description = '' + Port to bind the HTTP service to. + ''; + }; + }; + + dav_push.enabled = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable [WebDav Push] support. + + This allows the server to notify clients about changed data. + + [WebDav Push]: https://github.com/bitfireAT/webdav-push/ + ''; + }; + + nextcloud_login.enabled = mkOption { + type = types.bool; + default = true; + description = '' + Whether to emulate the Nextcloud login flow. + + This is supported in [DAVx5] and enables automatic app token generation. + + [DAVx5]: https://www.davx5.com/ + ''; + }; + }; + }; + }; + + environmentFiles = mkOption { + type = with types; listOf path; + default = [ ]; + example = [ "/run/keys/rustical.env" ]; + description = '' + Environment files to load into the runtime environment. + + Check the documentation for how to construct [environment variables]. + + :::{.tip} + Environment variables can substitute any config value and are useful for + hiding secrets. + ::: + + [environment variables]: https://lennart-k.github.io/rustical/installation/configuration/#environment-variables + ''; + }; + }; + + config = mkIf cfg.enable { + # install the config at a path where the cli will find it + environment.etc."rustical/config.toml".source = configFile; + + # provide the rustical cli + environment.systemPackages = [ cfg.package ]; + + systemd.services.rustical = { + description = "RustiCal CalDav/CardDav server"; + documentation = [ "https://lennart-k.github.io/rustical/" ]; + wantedBy = [ "multi-user.target" ]; + restartTriggers = [ configFile ]; + + serviceConfig = { + DynamicUser = true; + ExecStart = lib.getExe cfg.package; + EnvironmentFile = cfg.environmentFiles; + Restart = "on-failure"; + StateDirectory = "rustical"; + + CapabilityBoundingSet = ""; + DevicePolicy = "closed"; + 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"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged @resources" + ]; + SystemCallErrorNumber = "EPERM"; + UMask = "0077"; + }; + }; + }; +} From c69206e657af7ebc23fa4aa0f671b4c90618b836 Mon Sep 17 00:00:00 2001 From: PopeRigby Date: Sun, 3 May 2026 02:29:36 +0200 Subject: [PATCH 2/2] nixos/tests/rustical: init Co-Authored-By: Martin Weinelt --- nixos/tests/all-tests.nix | 1 + nixos/tests/web-apps/rustical.nix | 72 ++++++++++++++++++++++++++++ pkgs/by-name/ru/rustical/package.nix | 6 +++ 3 files changed, 79 insertions(+) create mode 100644 nixos/tests/web-apps/rustical.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index b276d07786bc..2e264857fde7 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1451,6 +1451,7 @@ in rtkit = runTest ./rtkit.nix; rtorrent = runTest ./rtorrent.nix; rush = runTest ./rush.nix; + rustical = runTest ./web-apps/rustical.nix; rustls-libssl = runTest ./rustls-libssl.nix; rxe = runTest ./rxe.nix; sabnzbd = runTest ./sabnzbd.nix; diff --git a/nixos/tests/web-apps/rustical.nix b/nixos/tests/web-apps/rustical.nix new file mode 100644 index 000000000000..86b0e95afa20 --- /dev/null +++ b/nixos/tests/web-apps/rustical.nix @@ -0,0 +1,72 @@ +{ + pkgs, + lib, + ... +}: +{ + name = "rustical"; + + meta.maintainers = pkgs.rustical.meta.maintainers; + + nodes.machine = + { + pkgs, + ... + }: + { + services.rustical.enable = true; + environment.systemPackages = with pkgs; [ calendar-cli ]; + }; + + testScript = + { + nodes, + ... + }: + let + port = toString nodes.machine.services.rustical.settings.http.port; + url = "http://localhost:${toString port}"; + + createPrincipalScript = pkgs.writeScript "rustical-create-principal" '' + #!${lib.getExe pkgs.expect} + spawn rustical principals create alice --password + expect "Enter your password:\r" + send "foobar\r" + expect eof + ''; + + calendarCliConfig = (pkgs.formats.json { }).generate "rustical-test-calendar-cli.json" { + default = { + caldav_user = "alice"; + caldav_url = "${url}/caldav/"; + calendar_url = "${url}/caldav/principal/alice"; + }; + testcal = { + inherits = "default"; + calendar_url = "${url}/caldav/principal/alice/testcal"; + }; + }; + in + # python + '' + machine.wait_for_unit("rustical.service") + machine.wait_for_open_port(${port}) + + with subtest("Smoketest"): + machine.succeed("curl --fail ${url}") + + with subtest("Create principal"): + machine.succeed("${createPrincipalScript}") + machine.succeed("rustical principals list | grep alice") + + with subtest("Generate token for principal"): + machine.succeed("curl -f -c cookies.txt -d 'username=alice&password=foobar' ${url}/frontend/login") + machine.succeed("curl -f -b cookies.txt -d 'name=mytoken' ${url}/frontend/user/alice/app_token > token.txt") + + with subtest("Interact with caldav"): + machine.succeed('calendar-cli --config-file ${calendarCliConfig} --caldav-pass "$(cat token.txt)" calendar create testcal') + machine.succeed('calendar-cli --config-file ${calendarCliConfig} --config-section testcal --caldav-pass "$(cat token.txt)" calendar add 2013-10-01 testevent') + + machine.log(machine.execute("systemd-analyze security rustical.service | grep -v ✓")[1]) + ''; +} diff --git a/pkgs/by-name/ru/rustical/package.nix b/pkgs/by-name/ru/rustical/package.nix index f478c6f6c06f..055006a3ca85 100644 --- a/pkgs/by-name/ru/rustical/package.nix +++ b/pkgs/by-name/ru/rustical/package.nix @@ -4,6 +4,7 @@ fetchFromGitHub, pkg-config, openssl, + nixosTests, }: rustPlatform.buildRustPackage (finalAttrs: { @@ -20,10 +21,15 @@ rustPlatform.buildRustPackage (finalAttrs: { cargoHash = "sha256-uP1lZarcwQhBKyASQIiNUs053EuxJy112P2e3hy2uZY="; nativeBuildInputs = [ pkg-config ]; + buildInputs = [ openssl ]; env.OPENSSL_NO_VENDOR = true; + passthru.tests = { + inherit (nixosTests) rustical; + }; + meta = { description = "Yet another calendar server aiming to be simple, fast and passwordless"; homepage = "https://github.com/lennart-k/rustical";