Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/radicale: add settings option #120440

Merged
merged 3 commits into from
May 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions nixos/doc/manual/release-notes/rl-2105.xml
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,20 @@ environment.systemPackages = [
The <package>yadm</package> dotfile manager has been updated from 2.x to 3.x, which has new (XDG) default locations for some data/state files. Most yadm commands will fail and print a legacy path warning (which describes how to upgrade/migrate your repository). If you have scripts, daemons, scheduled jobs, shell profiles, etc. that invoke yadm, expect them to fail or misbehave until you perform this migration and prepare accordingly.
</para>
</listitem>
<listitem>
<para>
Instead of determining <option>services.radicale.package</option>
automatically based on <option>system.stateVersion</option>, the latest
version is always used because old versions are not officially supported.
</para>
<para>
Furthermore, Radicale's systemd unit was hardened which might break some
deployments. In particular, a non-default
<literal>filesystem_folder</literal> has to be added to
<option>systemd.services.radicale.serviceConfig.ReadWritePaths</option> if
the deprecated <option>services.radicale.config</option> is used.
</para>
</listitem>
</itemizedlist>
</section>

Expand Down
196 changes: 152 additions & 44 deletions nixos/modules/services/networking/radicale.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,90 +3,198 @@
with lib;

let

cfg = config.services.radicale;

confFile = pkgs.writeText "radicale.conf" cfg.config;

defaultPackage = if versionAtLeast config.system.stateVersion "20.09" then {
pkg = pkgs.radicale3;
text = "pkgs.radicale3";
} else if versionAtLeast config.system.stateVersion "17.09" then {
pkg = pkgs.radicale2;
text = "pkgs.radicale2";
} else {
pkg = pkgs.radicale1;
text = "pkgs.radicale1";
format = pkgs.formats.ini {
listToValue = concatMapStringsSep ", " (generators.mkValueStringDefault { });
};
in

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

options = {
services.radicale.enable = mkOption {
type = types.bool;
default = false;
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 = "pkgs.radicale";
};

config = mkOption {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still needed? Presumably for the older versions of radicale? If so, can set add a comment about when you would use which option and how they are mutually exclusive. Maybe an assertion.

Copy link
Member Author

@dotlambda dotlambda Apr 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

settings should also work with older versions, but I kept config for backwards compatibility with existing configuration.nixs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with this is that third-party modules can't rely on settings. Every module that wants to read or write settings options has the chance to not work in case the user doesn't use settings. And it's even worse: A third-party module writing to settings makes the settings == {} check fail, meaning the users config option gets ignored! Because of this I think it might be better to actually remove this option.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And it's even worse: A third-party module writing to settings makes the settings == {} check fail, meaning the users config option gets ignored! Because of this I think it might be better to actually remove this option.

That's not true: If both config and settings are set an assertion is triggerred.
Nevertheless, I'm all in favor of removing config but it might make sense to first mark it as deprecated using lib.warn.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

type = types.str;
default = "";
description = ''
Enable Radicale CalDAV and CardDAV server.
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.
'';
};

services.radicale.package = mkOption {
type = types.package;
default = defaultPackage.pkg;
defaultText = defaultPackage.text;
settings = mkOption {
type = format.type;
default = { };
description = ''
Radicale package to use. This defaults to version 1.x if
<literal>system.stateVersion &lt; 17.09</literal>, version 2.x if
<literal>17.09 ≤ system.stateVersion &lt; 20.09</literal>, and
version 3.x otherwise.
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 = literalExample ''
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";
};
'';
};

services.radicale.config = mkOption {
type = types.str;
default = "";
rights = mkOption {
type = format.type;
description = ''
Radicale configuration, this will set the service
configuration file.
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 = literalExample ''
root = {
user = ".+";
collection = "";
permissions = "R";
};
principal = {
user = ".+";
collection = "{user}";
permissions = "RW";
};
calendars = {
user = ".+";
collection = "{user}/[^/]+";
permissions = "rw";
};
'';
};

services.radicale.extraArgs = mkOption {
extraArgs = mkOption {
type = types.listOf types.str;
default = [];
description = "Extra arguments passed to the Radicale daemon.";
};
};

config = mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
assertions = [
{
assertion = cfg.settings == { } || cfg.config == "";
message = ''
The options services.radicale.config and services.radicale.settings
are mutually exclusive.
'';
}
];

users.users.radicale =
{ uid = config.ids.uids.radicale;
description = "radicale user";
home = "/var/lib/radicale";
createHome = true;
};
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.uid = config.ids.uids.radicale;

users.groups.radicale =
{ gid = config.ids.gids.radicale; };
users.groups.radicale.gid = config.ids.gids.radicale;

systemd.services.radicale = {
description = "A Simple Calendar and Contact Server";
after = [ "network.target" ];
requires = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = concatStringsSep " " ([
"${cfg.package}/bin/radicale" "-C" confFile
"${pkg}/bin/radicale" "-C" confFile
] ++ (
map escapeShellArg cfg.extraArgs
));
User = "radicale";
Group = "radicale";
StateDirectory = "radicale/collections";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Link to upstream documentation with comment so we can track changes would be useful. Additionally, will these hardening options break older versions of radicale?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They will only break configurations where data is stored at a non-standard location.

StateDirectoryMode = "0750";
# Hardening
CapabilityBoundingSet = [ "" ];
dotlambda marked this conversation as resolved.
Show resolved Hide resolved
DeviceAllow = [ "/dev/stdin" ];
DevicePolicy = "strict";
IPAddressAllow = mkIf bindLocalhost "localhost";
IPAddressDeny = mkIf bindLocalhost "any";
Comment on lines +166 to +167
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this problematic? One scenario I can think of is a hook accessing the (non-local) network. Does anyone do that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't feel comfortable putting a hostname in there, but maybe that's just the networker in me speaking. But making it contingent on bindLocalhost sounds like a sensible approach.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not actually a hostname, it's a special name interpreted as 127.0.0.0/8 ::1/128 by systemd: https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html#IPAddressAllow=ADDRESS%5B/PREFIXLENGTH%5D%E2%80%A6

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";
};
};
};

meta.maintainers = with lib.maintainers; [ aneeshusa infinisil ];
meta.maintainers = with lib.maintainers; [ aneeshusa infinisil dotlambda ];
}