Skip to content

Commit

Permalink
nixos/radicle: init services
Browse files Browse the repository at this point in the history
  • Loading branch information
ju1m committed Jul 5, 2024
1 parent e27c510 commit 88fb6d3
Show file tree
Hide file tree
Showing 6 changed files with 573 additions and 0 deletions.
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2411.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

- [Eintopf](https://eintopf.info), community event and calendar web application. Available as [services.eintopf](options.html#opt-services.eintopf).

- [Radicle](https://radicle.xyz), an open source, peer-to-peer code collaboration stack built on Git. Available as [services.radicle](#opt-services.radicle.enable).

- [Renovate](https://github.com/renovatebot/renovate), a dependency updating tool for various git forges and language ecosystems. Available as [services.renovate](#opt-services.renovate.enable).

- [wg-access-server](https://github.com/freifunkMUC/wg-access-server/), an all-in-one WireGuard VPN solution with a web ui for connecting devices. Available at [services.wg-access-server](#opt-services.wg-access-server.enable).
Expand Down
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,7 @@
./services/misc/pufferpanel.nix
./services/misc/pykms.nix
./services/misc/radarr.nix
./services/misc/radicle.nix
./services/misc/readarr.nix
./services/misc/redmine.nix
./services/misc/renovate.nix
Expand Down
347 changes: 347 additions & 0 deletions nixos/modules/services/misc/radicle.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,347 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.radicle;

json = pkgs.formats.json { };

env = rec {
# rad fails if it cannot stat $HOME/.gitconfig
HOME = "/var/lib/radicle";
RAD_HOME = HOME;
};

# Convenient wrapper to run `rad` in the namespaces of `radicle-node.service`
rad-system = pkgs.writeShellScriptBin "rad-system" ''
set -o allexport
${toShellVars env}
# Note that --env is not used to preserve host's envvars like $TERM
exec ${getExe' pkgs.util-linux "nsenter"} -a \
-t "$(${getExe' config.systemd.package "systemctl"} show -P MainPID radicle-node.service)" \
-S "$(${getExe' config.systemd.package "systemctl"} show -P UID radicle-node.service)" \
-G "$(${getExe' config.systemd.package "systemctl"} show -P GID radicle-node.service)" \
${getExe' cfg.package "rad"} "$@"
'';

commonServiceConfig = serviceName: {
environment = env // {
RUST_LOG = mkDefault "info";
};
path = [
pkgs.gitMinimal
];
documentation = [
"https://docs.radicle.xyz/guides/seeder"
];
after = [
"network.target"
"network-online.target"
];
requires = [
"network-online.target"
];
wantedBy = [ "multi-user.target" ];
serviceConfig = mkMerge [
{
BindReadOnlyPaths = [
"${cfg.configFile}:${env.RAD_HOME}/config.json"
"${if isPath cfg.publicKeyFile then cfg.publicKeyFile else pkgs.writeText "radicle.pub" cfg.publicKeyFile}:${env.RAD_HOME}/keys/radicle.pub"
];
KillMode = "process";
StateDirectory = [ "radicle" ];
User = config.users.users.radicle.name;
Group = config.users.groups.radicle.name;
WorkingDirectory = env.HOME;
}
# The following options are only for optimizing:
# systemd-analyze security ${serviceName}
{
BindReadOnlyPaths = [
"-/etc/resolv.conf"
"/etc/ssl/certs/ca-certificates.crt"
"/run/systemd"
];
AmbientCapabilities = "";
CapabilityBoundingSet = "";
DeviceAllow = ""; # ProtectClock= adds DeviceAllow=char-rtc r
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateTmp = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RuntimeDirectoryMode = "700";
SocketBindDeny = [ "any" ];
StateDirectoryMode = "0750";
SystemCallFilter = [
"@system-service"
"~@aio"
"~@chown"
"~@keyring"
"~@memlock"
"~@privileged"
"~@resources"
"~@setuid"
"~@timer"
];
SystemCallArchitectures = "native";
# This is for BindPaths= and BindReadOnlyPaths=
# to allow traversal of directories they create inside RootDirectory=
UMask = "0066";
}
];
confinement = {
enable = true;
mode = "full-apivfs";
packages = [
pkgs.gitMinimal
cfg.package
pkgs.iana-etc
(getLib pkgs.nss)
pkgs.tzdata
];
};
};
in
{
options = {
services.radicle = {
enable = mkEnableOption "Radicle Seed Node";
package = mkPackageOption pkgs "radicle-node" { };
privateKeyFile = mkOption {
type = with types; either path str;
description = ''
SSH private key generated by `rad auth`.
If it contains a colon (`:`) the string before the colon
is taken as the credential name
and the string after as a path encrypted with `systemd-creds`.
'';
};
publicKeyFile = mkOption {
type = with types; either path str;
description = ''
SSH public key generated by `rad auth`.
'';
};
node = {
listenAddress = mkOption {
type = types.str;
default = "0.0.0.0";
example = "127.0.0.1";
description = "The IP address on which `radicle-node` listens.";
};
listenPort = mkOption {
type = types.port;
default = 8776;
description = "The port on which `radicle-node` listens.";
};
openFirewall = mkEnableOption "opening the firewall for `radicle-node`";
extraArgs = mkOption {
type = with types; listOf str;
default = [ ];
description = "Extra arguments for `radicle-node`";
};
};
configFile = mkOption {
type = types.package;
internal = true;
default = (json.generate "config.json" cfg.settings).overrideAttrs (previousAttrs: {
preferLocalBuild = true;
# None of the usual phases are run here because runCommandWith uses buildCommand,
# so just append to buildCommand what would usually be a checkPhase.
buildCommand = previousAttrs.buildCommand + optionalString cfg.checkConfig ''
ln -s $out config.json
install -D -m 644 /dev/stdin keys/radicle.pub <<<"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBgFMhajUng+Rjj/sCFXI9PzG8BQjru2n7JgUVF1Kbv5 snakeoil"
export RAD_HOME=$PWD
${getExe' pkgs.buildPackages.radicle-node "rad"} config >/dev/null || {
cat -n config.json
echo "Invalid config.json according to rad."
echo "Please double-check your services.radicle.settings (producing the config.json above),"
echo "some settings may be missing or have the wrong type."
exit 1
} >&2
'';
});
};
checkConfig = mkEnableOption "checking the {file}`config.json` file resulting from {option}`services.radicle.settings`" // { default = true; };
settings = mkOption {
description = ''
See https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/radicle/src/node/config.rs#L275
'';
default = { };
type = types.submodule {
freeformType = json.type;
};
};
httpd = {
enable = mkEnableOption "Radicle HTTP gateway to radicle-node";
package = mkPackageOption pkgs "radicle-httpd" { };
listenAddress = mkOption {
type = types.str;
default = "127.0.0.1";
description = "The IP address on which `radicle-httpd` listens.";
};
listenPort = mkOption {
type = types.port;
default = 8080;
description = "The port on which `radicle-httpd` listens.";
};
nginx = mkOption {
# Type of a single virtual host, or null.
type = types.nullOr (types.submodule (
recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {
options.serverName = {
default = "radicle-${config.networking.hostName}.${config.networking.domain}";
defaultText = "radicle-\${config.networking.hostName}.\${config.networking.domain}";
};
}
));
default = null;
example = literalExpression ''
{
serverAliases = [
"seed.''${config.networking.domain}"
];
enableACME = false;
useACMEHost = config.networking.domain;
}
'';
description = ''
With this option, you can customize an nginx virtual host which already has sensible defaults for `radicle-httpd`.
Set to `{}` if you do not need any customization to the virtual host.
If enabled, then by default, the {option}`serverName` is
`radicle-''${config.networking.hostName}.''${config.networking.domain}`,
TLS is active, and certificates are acquired via ACME.
If this is set to null (the default), no nginx virtual host will be configured.
'';
};
extraArgs = mkOption {
type = with types; listOf str;
default = [ ];
description = "Extra arguments for `radicle-httpd`";
};
};
};
};

config = mkIf cfg.enable (mkMerge [
{
systemd.services.radicle-node = mkMerge [
(commonServiceConfig "radicle-node")
{
description = "Radicle Node";
documentation = [ "man:radicle-node(1)" ];
serviceConfig = {
ExecStart = "${getExe' cfg.package "radicle-node"} --force --listen ${cfg.node.listenAddress}:${toString cfg.node.listenPort} ${escapeShellArgs cfg.node.extraArgs}";
Restart = mkDefault "on-failure";
RestartSec = "30";
SocketBindAllow = [ "tcp:${toString cfg.node.listenPort}" ];
SystemCallFilter = mkAfter [
# Needed by git upload-pack which calls alarm() and setitimer() when providing a rad clone
"@timer"
];
};
confinement.packages = [
cfg.package
];
}
# Give only access to the private key to radicle-node.
{
serviceConfig =
let keyCred = builtins.split ":" "${cfg.privateKeyFile}"; in
if length keyCred > 1
then {
LoadCredentialEncrypted = [ cfg.privateKeyFile ];
# Note that neither %d nor ${CREDENTIALS_DIRECTORY} works in BindReadOnlyPaths=
BindReadOnlyPaths = [ "/run/credentials/radicle-node.service/${head keyCred}:${env.RAD_HOME}/keys/radicle" ];
}
else {
LoadCredential = [ "radicle:${cfg.privateKeyFile}" ];
BindReadOnlyPaths = [ "/run/credentials/radicle-node.service/radicle:${env.RAD_HOME}/keys/radicle" ];
};
}
];

environment.systemPackages = [
rad-system
];

networking.firewall = mkIf cfg.node.openFirewall {
allowedTCPPorts = [ cfg.node.listenPort ];
};

users = {
users.radicle = {
description = "Radicle";
group = "radicle";
home = env.HOME;
isSystemUser = true;
};
groups.radicle = {
};
};
}

(mkIf cfg.httpd.enable (mkMerge [
{
systemd.services.radicle-httpd = mkMerge [
(commonServiceConfig "radicle-httpd")
{
description = "Radicle HTTP gateway to radicle-node";
documentation = [ "man:radicle-httpd(1)" ];
serviceConfig = {
ExecStart = "${getExe' cfg.httpd.package "radicle-httpd"} --listen ${cfg.httpd.listenAddress}:${toString cfg.httpd.listenPort} ${escapeShellArgs cfg.httpd.extraArgs}";
Restart = mkDefault "on-failure";
RestartSec = "10";
SocketBindAllow = [ "tcp:${toString cfg.httpd.listenPort}" ];
SystemCallFilter = mkAfter [
# Needed by git upload-pack which calls alarm() and setitimer() when providing a git clone
"@timer"
];
};
confinement.packages = [
cfg.httpd.package
];
}
];
}

(mkIf (cfg.httpd.nginx != null) {
services.nginx.virtualHosts.${cfg.httpd.nginx.serverName} = lib.mkMerge [
cfg.httpd.nginx
{
forceSSL = mkDefault true;
enableACME = mkDefault true;
locations."/" = {
proxyPass = "http://${cfg.httpd.listenAddress}:${toString cfg.httpd.listenPort}";
recommendedProxySettings = true;
};
}
];

services.radicle.settings = {
node.alias = mkDefault cfg.httpd.nginx.serverName;
node.externalAddresses = mkDefault [
"${cfg.httpd.nginx.serverName}:${toString cfg.node.listenPort}"
];
};
})
]))
]);

meta.maintainers = with lib.maintainers; [
julm
lorenzleutgeb
];
}
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,7 @@ in {
rabbitmq = handleTest ./rabbitmq.nix {};
radarr = handleTest ./radarr.nix {};
radicale = handleTest ./radicale.nix {};
radicle = runTest ./radicle.nix;
ragnarwm = handleTest ./ragnarwm.nix {};
rasdaemon = handleTest ./rasdaemon.nix {};
readarr = handleTest ./readarr.nix {};
Expand Down
Loading

0 comments on commit 88fb6d3

Please sign in to comment.