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

Conversation

dotlambda
Copy link
Member

@dotlambda dotlambda commented Apr 23, 2021

Motivation for this change

NixOS/rfcs#42

Things done
  • Tested using sandboxing (nix.useSandbox on NixOS, or option sandbox in nix.conf on non-NixOS linux)
  • Built on platform(s)
    • NixOS
    • macOS
    • other Linux distributions
  • Tested via one or more NixOS test(s) if existing and applicable for the change (look inside nixos/tests)
  • Tested compilation of all pkgs that depend on this change using nix-shell -p nixpkgs-review --run "nixpkgs-review wip"
  • Tested execution of all binary files (usually in ./result/bin/)
  • Determined the impact on package closure size (by running nix path-info -S before and after)
  • Ensured that relevant documentation is up to date
  • Fits CONTRIBUTING.md.

I'm looking for comments on my approach.
The following points need to be solved:

  • nixosTests.radicale is broken cause it uses /tmp
  • This is a breaking change for people who use a non-default filesystem_folder. Is documenting that change sufficient? Should we add an option to add paths to ReadWritePaths?
  • document breaking changes
  • Should we merge the two test into one, remove the old one, or keep both files?

Copy link
Member

@aanderse aanderse left a comment

Choose a reason for hiding this comment

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

Just a few comments to think on. Also, are the old versions of radicale still supported upstream?

@@ -43,7 +54,7 @@ in
'';
};

services.radicale.config = mkOption {
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

@@ -84,9 +150,25 @@ in
));
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.

nixos/modules/services/networking/radicale.nix Outdated Show resolved Hide resolved
cfg = config.services.radicale;

confFile = pkgs.writeText "radicale.conf" cfg.config;
format = let
Copy link
Member

Choose a reason for hiding this comment

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

Doesn't the ini format work here? That would be much easier to read...

Copy link
Member Author

Choose a reason for hiding this comment

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

No, that's why I had to create my own. The IP addresses Radicale listens on have to be given as a comma-separated list: https://radicale.org/3.0.html#tutorials/basic-configuration/addresses. I thought requiring users to specify e.g. hosts = "0.0.0.0:5232, [::]:5232" is a little unnatural.

Copy link
Member

Choose a reason for hiding this comment

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

Do we have a (unit) test to ensure our formatter is doing what we expect it to do?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, there is nixos/tests/radicale3.nix.

Copy link
Member

Choose a reason for hiding this comment

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

No no I mean: can we actually format the IPs as we claim? The nixos (integration) test just covers one case as far as I can see. I would love to have a few tests. One case for each of the permissible input values.

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 essentially, I'm not doing anything except for replacing lists of strings with comma-separated strings and then I use lib.generators.toINI, which is tested in lib/tests/misc.nix.

Copy link
Member Author

Choose a reason for hiding this comment

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

No no I mean: can we actually format the IPs as we claim? The nixos (integration) test just covers one case as far as I can see. I would love to have a few tests. One case for each of the permissible input values.

Wouldn't we have to put the generator in lib/ for that? I honestly don't think unit tests are needed for such a simple thing as concatStringsSep ", ".

Copy link
Member

@infinisil infinisil May 3, 2021

Choose a reason for hiding this comment

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

I extended formats.ini to support this with #121613

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks a lot!

@dotlambda
Copy link
Member Author

@SuperSandro2000 I did not ping @andir without a reason: I know he uses Radicale: https://github.com/andir/infra/tree/master/config/servers/mail/radicale

@dotlambda dotlambda requested a review from andir April 24, 2021 06:17
@SuperSandro2000
Copy link
Member

SuperSandro2000 commented Apr 24, 2021

@SuperSandro2000 I did not ping @andir without a reason: I know he uses Radicale: https://github.com/andir/infra/tree/master/config/servers/mail/radicale

Thats not my fault. He blocked me and when I edit the reviewer list he gets removed.

@dotlambda
Copy link
Member Author

dotlambda commented Apr 24, 2021

Also, are the old versions of radicale still supported upstream?

No, they are not. But using a newer version requires changes to the configuration (and the data format is different between version 1 and the others), so we keep them for old system.stateVersions. I don't know of any vulnerabilities in the old versions.
If we want to get rid of this behaviour, we could require that people set their own package unless the system.stateVersion is new enough.

@dotlambda dotlambda force-pushed the radicale-settings branch 2 times, most recently from 80cebea to 8d3ed4a Compare April 24, 2021 07:42
nixos/modules/services/networking/radicale.nix Outdated Show resolved Hide resolved
cfg = config.services.radicale;

confFile = pkgs.writeText "radicale.conf" cfg.config;
format = let
Copy link
Member

Choose a reason for hiding this comment

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

Do we have a (unit) test to ensure our formatter is doing what we expect it to do?

type = "from_file";
file = toString rightsFile;
};

environment.systemPackages = [ cfg.package ];

users.users.radicale =
{ uid = config.ids.uids.radicale;
description = "radicale user";
home = "/var/lib/radicale";
Copy link
Member

Choose a reason for hiding this comment

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

I think this requires a changelog entry so an admin can put special attention towards this change. It is subtile as the service might start but the data might no longer be "there" if we forgot some case.

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.

Yes, see the list of todos at the very top. I will add a changelog entry once we decided on what the changes will be.

@dotlambda dotlambda force-pushed the radicale-settings branch 3 times, most recently from e4798a9 to aba0248 Compare May 1, 2021 10:52
@dotlambda
Copy link
Member Author

I applied some more hardening options to the systemd unit. Maybe @mweinelt who's been doing that a lot lately wants to have a look?

@dotlambda
Copy link
Member Author

Also, are the old versions of radicale still supported upstream?

No, they are not. But using a newer version requires changes to the configuration (and the data format is different between version 1 and the others), so we keep them for old system.stateVersions. I don't know of any vulnerabilities in the old versions.
If we want to get rid of this behaviour, we could require that people set their own package unless the system.stateVersion is new enough.

I suggest the following: We remove the old package versions from Nixpkgs since keeping them gives a sense of them still being supported. In the NixOS service, we add an assertion that checks whether package is set by hand in case system.stateVersion is not recent enough.

@mweinelt
Copy link
Member

mweinelt commented May 1, 2021

I applied some more hardening options to the systemd unit. Maybe @mweinelt who's been doing that a lot lately wants to have a look?

I usually add systemd-analyze to the nixos test, so I can revisit the results quickly.

diff --git a/nixos/tests/radicale3.nix b/nixos/tests/radicale3.nix
index cda43b6c06c..0fedaeea705 100644
--- a/nixos/tests/radicale3.nix
+++ b/nixos/tests/radicale3.nix
@@ -74,5 +74,7 @@ in {
 
     with subtest("Test web interface"):
         machine.succeed("curl --fail http://${user}:${password}@localhost:${port}/.web/")
+
+    machine.log(machine.succeed("systemd-analyze security radicale.service"))
   '';
 })

Other than that hardening means you better know what interfaces radicale needs, which I do not.

✗ PrivateNetwork=                    Service has access to the host's …      0.5
✗ RestrictAddressFamilies=~AF_(INET… Service may allocate Internet soc…      0.3
✗ DeviceAllow=                       Service has a device ACL with som…      0.1
✗ IPAddressDeny=                     Service does not define an IP add…      0.2
✗ ProtectProc=                       Service has full access to proces…      0.2
✗ SystemCallFilter=~@privileged      System call allow list defined fo…      0.2
✗ RootDirectory=/RootImage=          Service runs within the host's ro…      0.1
✗ UMask=                             Files created by service are grou…      0.1
✗ ProcSubset=                        Service has full access to non-pr…      0.1

→ Overall exposure level for radicale.service: 1.4 OK :-)

Can't lock down network things further, but DevicePolicy=strict might make sense. Other than that UMask and the procfs stuff looks easy.

@dotlambda
Copy link
Member Author

DevicePolicy=strict might make sense

Doesn't work. Radicale definitely needs /dev/random or something.

@mweinelt
Copy link
Member

mweinelt commented May 1, 2021

DevicePolicy=strict might make sense

Doesn't work. Radicale definitely needs /dev/random or something.

Then you can add that to DeviceAllow= 😀

@dotlambda dotlambda force-pushed the radicale-settings branch 2 times, most recently from 1807f71 to f6d0cba Compare May 1, 2021 12:19
@dotlambda
Copy link
Member Author

DevicePolicy=strict might make sense

Doesn't work. Radicale definitely needs /dev/random or something.

Then you can add that to DeviceAllow=

That's what I did. Turns out is uses syscalls so no dev/urandom needed.
However, I'm surprised that /dev/null is not needed (and I even added a hook to the test): https://github.com/Kozea/Radicale/blob/3.0.6/radicale/storage/multifilesystem/lock.py#L60-L62

Copy link
Member Author

@dotlambda dotlambda left a comment

Choose a reason for hiding this comment

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

The hardening is even more restrictive now, but I think NixOS modules are not supposed to be usable for everyone, they just provide an opinionated default.

@@ -84,9 +161,43 @@ in
));
User = "radicale";
Group = "radicale";
StateDirectory = "radicale/collections";
# Hardening
CapabilityBoundingSet = "";
Copy link
Member Author

Choose a reason for hiding this comment

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

This needs CAP_NET_BIND_SERVICE if you want to use a port < 1024. How should we handle that?

Copy link
Member

Choose a reason for hiding this comment

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

Best case: Socket activation, if supported.
Second best: Only pass it in if port < 1024.

I don't think python actively requests capabilities, so you'd need to put it into AmbientCapabilities as well.

Copy link
Member Author

@dotlambda dotlambda May 1, 2021

Choose a reason for hiding this comment

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

Best case: Socket activation, if supported.

not yet: Kozea/Radicale#1141

Second best: Only pass it in if port < 1024.

Then we need a new service option because we shouldn't parse settings.server.hosts.
We can make it dependent on bindLocalhost.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, the current implementation doesn't have the permissions to bind to ports < 1024, so they should also be given by hand in the future.

Comment on lines +169 to +167
IPAddressAllow = mkIf bindLocalhost "localhost";
IPAddressDeny = mkIf bindLocalhost "any";
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

@SuperSandro2000
Copy link
Member

The hardening is even more restrictive now, but I think NixOS modules are not supposed to be usable for everyone, they just provide an opinionated default.

They should create a good basis to use the package and should be applicable to a or the most common use case(s). It is okay if they don't work for every possible use case but they should supply things like extraConfig to make it easier to adapt them to my use case.

@dotlambda
Copy link
Member Author

I suggest the following: We remove the old package versions from Nixpkgs since keeping them gives a sense of them still being supported. In the NixOS service, we add an assertion that checks whether package is set by hand in case system.stateVersion is not recent enough.

I would really appreciate some feedback on this.

@dotlambda
Copy link
Member Author

I suggest the following: We remove the old package versions from Nixpkgs since keeping them gives a sense of them still being supported. In the NixOS service, we add an assertion that checks whether package is set by hand in case system.stateVersion is not recent enough.

Instead of an assertion, we might just want to use lib.warn and link to https://github.com/Kozea/Radicale/blob/3.0.6/NEWS.md#upgrade-checklist and https://radicale.org/2.1.html#documentation/migration-from-1xx-to-2xx.

@infinisil
Copy link
Member

You can use the warnings option to define a warning when e.g. options.services.radicale.config.isDefined is set

The radicale version is no longer chosen automatically based on
system.stateVersion because that gave the impression that old versions
are still supported.
@dotlambda
Copy link
Member Author

From my point of view, this is ready now. Please test whether the hardening breaks your deployments (I documented the most common case in the release notes). Let me know if you think anything needs more documentation.

@dotlambda
Copy link
Member Author

I will merge this soon unless someone complains or prefers that we call the new option config as well and decide which one it is based on the type.

@dotlambda dotlambda merged commit e611d66 into NixOS:master May 14, 2021
@dotlambda dotlambda deleted the radicale-settings branch May 14, 2021 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants