| + |
{ config, lib, pkgs, ... }:
|
| + |
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}/.radicle";
|
| + |
};
|
| + |
|
| + |
# Convenient wrapper to run `rad` in the namespaces of `radicle-node.service`
|
| + |
rad-system = pkgs.writeShellScriptBin "rad-system" ''
|
| + |
set -o allexport
|
| + |
${lib.toShellVars env}
|
| + |
# Note that --env is not used to preserve host's envvars like $TERM
|
| + |
exec ${lib.getExe' pkgs.util-linux "nsenter"} -a \
|
| + |
-t "$(${lib.getExe' config.systemd.package "systemctl"} show -P MainPID radicle-node.service)" \
|
| + |
-S "$(${lib.getExe' config.systemd.package "systemctl"} show -P UID radicle-node.service)" \
|
| + |
-G "$(${lib.getExe' config.systemd.package "systemctl"} show -P GID radicle-node.service)" \
|
| + |
${lib.getExe' cfg.package "rad"} "$@"
|
| + |
'';
|
| + |
|
| + |
commonServiceConfig = serviceName: {
|
| + |
environment = env // {
|
| + |
RUST_LOG = lib.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 = lib.mkMerge [
|
| + |
{
|
| + |
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
|
| + |
(lib.getLib pkgs.nss)
|
| + |
pkgs.tzdata
|
| + |
];
|
| + |
};
|
| + |
};
|
| + |
in
|
| + |
{
|
| + |
# Replace the existing module
|
| + |
disabledModules = [
|
| + |
"services/misc/radicle.nix"
|
| + |
];
|
| + |
|
| + |
options = {
|
| + |
services.radicle = {
|
| + |
enable = lib.mkEnableOption "Radicle Seed Node";
|
| + |
package = lib.mkPackageOption pkgs "radicle-node" { };
|
| + |
privateKeyFile = lib.mkOption {
|
| + |
# Note that a key encrypted by systemd-creds is not a path but a str.
|
| + |
type = with lib.types; either path str;
|
| + |
description = ''
|
| + |
Absolute file path to an SSH private key,
|
| + |
usually 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`.
|
| + |
'';
|
| + |
};
|
| + |
publicKey = lib.mkOption {
|
| + |
type = with lib.types; either path str;
|
| + |
description = ''
|
| + |
An SSH public key (as an absolute file path or directly as a string),
|
| + |
usually generated by `rad auth`.
|
| + |
'';
|
| + |
};
|
| + |
node = {
|
| + |
listenAddress = lib.mkOption {
|
| + |
type = lib.types.str;
|
| + |
default = "[::]";
|
| + |
example = "127.0.0.1";
|
| + |
description = "The IP address on which `radicle-node` listens.";
|
| + |
};
|
| + |
listenPort = lib.mkOption {
|
| + |
type = lib.types.port;
|
| + |
default = 8776;
|
| + |
description = "The port on which `radicle-node` listens.";
|
| + |
};
|
| + |
openFirewall = lib.mkEnableOption "opening the firewall for `radicle-node`";
|
| + |
extraArgs = lib.mkOption {
|
| + |
type = with lib.types; listOf str;
|
| + |
default = [ ];
|
| + |
description = "Extra arguments for `radicle-node`";
|
| + |
};
|
| + |
};
|
| + |
configFile = lib.mkOption {
|
| + |
type = lib.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 + lib.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
|
| + |
${lib.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 = lib.mkEnableOption "checking the {file}`config.json` file resulting from {option}`services.radicle.settings`" // { default = true; };
|
| + |
settings = lib.mkOption {
|
| + |
description = ''
|
| + |
See https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/tree/radicle/src/node/config.rs#L275
|
| + |
'';
|
| + |
default = { };
|
| + |
example = lib.literalExpression ''
|
| + |
{
|
| + |
web.pinned.repositories = [
|
| + |
"rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5" # heartwood
|
| + |
"rad:z3trNYnLWS11cJWC6BbxDs5niGo82" # rips
|
| + |
];
|
| + |
}
|
| + |
'';
|
| + |
type = lib.types.submodule {
|
| + |
freeformType = json.type;
|
| + |
};
|
| + |
};
|
| + |
httpd = {
|
| + |
enable = lib.mkEnableOption "Radicle HTTP gateway to radicle-node";
|
| + |
package = lib.mkPackageOption pkgs "radicle-httpd" { };
|
| + |
listenAddress = lib.mkOption {
|
| + |
type = lib.types.str;
|
| + |
default = "127.0.0.1";
|
| + |
description = "The IP address on which `radicle-httpd` listens.";
|
| + |
};
|
| + |
listenPort = lib.mkOption {
|
| + |
type = lib.types.port;
|
| + |
default = 8080;
|
| + |
description = "The port on which `radicle-httpd` listens.";
|
| + |
};
|
| + |
nginx = lib.mkOption {
|
| + |
# Type of a single virtual host, or null.
|
| + |
type = lib.types.nullOr (lib.types.submodule (
|
| + |
lib.recursiveUpdate (import "${toString <nixos>}/nixos/modules/services/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 = lib.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 = lib.mkOption {
|
| + |
type = with lib.types; listOf str;
|
| + |
default = [ ];
|
| + |
description = "Extra arguments for `radicle-httpd`";
|
| + |
};
|
| + |
};
|
| + |
};
|
| + |
};
|
| + |
|
| + |
config = lib.mkIf cfg.enable (lib.mkMerge [
|
| + |
{
|
| + |
systemd.mounts = [
|
| + |
{
|
| + |
description = "Radicle node configuration";
|
| + |
what = "${cfg.configFile}";
|
| + |
where = "${env.RAD_HOME}/config.json";
|
| + |
type = "none";
|
| + |
options = "bind";
|
| + |
wantedBy = [ "radicle-node.service" ] ++ lib.optional cfg.httpd.enable "radicle-httpd.service";
|
| + |
}
|
| + |
{
|
| + |
description = "Radicle node public key";
|
| + |
what = "${if lib.types.path.check cfg.publicKey then cfg.publicKey else pkgs.writeText "radicle.pub" cfg.publicKey}";
|
| + |
where = "${env.RAD_HOME}/keys/radicle.pub";
|
| + |
type = "none";
|
| + |
options = "bind";
|
| + |
wantedBy = [ "radicle-node.service" ] ++ lib.optional cfg.httpd.enable "radicle-httpd.service";
|
| + |
}
|
| + |
];
|
| + |
systemd.services.radicle-node = lib.mkMerge [
|
| + |
(commonServiceConfig "radicle-node")
|
| + |
{
|
| + |
description = "Radicle Node";
|
| + |
documentation = [ "man:radicle-node(1)" ];
|
| + |
serviceConfig = {
|
| + |
ExecStart = "${lib.getExe' cfg.package "radicle-node"} --force --listen ${cfg.node.listenAddress}:${toString cfg.node.listenPort} ${lib.escapeShellArgs cfg.node.extraArgs}";
|
| + |
Restart = lib.mkDefault "on-failure";
|
| + |
RestartSec = "30";
|
| + |
SocketBindAllow = [ "tcp:${toString cfg.node.listenPort}" ];
|
| + |
SystemCallFilter = lib.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 lib.length keyCred > 1
|
| + |
then {
|
| + |
LoadCredentialEncrypted = [ cfg.privateKeyFile ];
|
| + |
# Note that neither %d nor ${CREDENTIALS_DIRECTORY} works in BindReadOnlyPaths=
|
| + |
BindReadOnlyPaths = [ "/run/credentials/radicle-node.service/${lib.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 = lib.mkIf cfg.node.openFirewall {
|
| + |
allowedTCPPorts = [ cfg.node.listenPort ];
|
| + |
};
|
| + |
|
| + |
users = {
|
| + |
users.radicle = {
|
| + |
description = "Radicle";
|
| + |
group = "radicle";
|
| + |
home = env.HOME;
|
| + |
isSystemUser = true;
|
| + |
};
|
| + |
groups.radicle = {
|
| + |
};
|
| + |
};
|
| + |
}
|
| + |
|
| + |
(lib.mkIf cfg.httpd.enable (lib.mkMerge [
|
| + |
{
|
| + |
systemd.services.radicle-httpd = lib.mkMerge [
|
| + |
(commonServiceConfig "radicle-httpd")
|
| + |
{
|
| + |
description = "Radicle HTTP gateway to radicle-node";
|
| + |
documentation = [ "man:radicle-httpd(1)" ];
|
| + |
serviceConfig = {
|
| + |
ExecStart = "${lib.getExe' cfg.httpd.package "radicle-httpd"} --listen ${cfg.httpd.listenAddress}:${toString cfg.httpd.listenPort} ${lib.escapeShellArgs cfg.httpd.extraArgs}";
|
| + |
Restart = lib.mkDefault "on-failure";
|
| + |
RestartSec = "10";
|
| + |
SocketBindAllow = [ "tcp:${toString cfg.httpd.listenPort}" ];
|
| + |
SystemCallFilter = lib.mkAfter [
|
| + |
# Needed by git upload-pack which calls alarm() and setitimer() when providing a git clone
|
| + |
"@timer"
|
| + |
];
|
| + |
};
|
| + |
confinement.packages = [
|
| + |
cfg.httpd.package
|
| + |
];
|
| + |
}
|
| + |
];
|
| + |
}
|
| + |
|
| + |
(lib.mkIf (cfg.httpd.nginx != null) {
|
| + |
services.nginx.virtualHosts.${cfg.httpd.nginx.serverName} = lib.mkMerge [
|
| + |
cfg.httpd.nginx
|
| + |
{
|
| + |
forceSSL = lib.mkDefault true;
|
| + |
enableACME = lib.mkDefault true;
|
| + |
locations."/" = {
|
| + |
proxyPass = "http://${cfg.httpd.listenAddress}:${toString cfg.httpd.listenPort}";
|
| + |
recommendedProxySettings = true;
|
| + |
};
|
| + |
}
|
| + |
];
|
| + |
|
| + |
services.radicle.settings = {
|
| + |
node.alias = lib.mkDefault cfg.httpd.nginx.serverName;
|
| + |
node.externalAddresses = lib.mkDefault [
|
| + |
"${cfg.httpd.nginx.serverName}:${toString cfg.node.listenPort}"
|
| + |
];
|
| + |
};
|
| + |
})
|
| + |
]))
|
| + |
]);
|
| + |
|
| + |
meta.maintainers = with lib.maintainers; [
|
| + |
julm
|
| + |
lorenzleutgeb
|
| + |
];
|
| + |
}
|