Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
chore: add radicle service from nixpkgs
null committed 1 year ago
commit d92262e060830f1c55ec6b9ac95df764c9fbe3fe
parent 3b5fac178eaf9bca639fbd0c1df0c68619a7f51f
1 file changed +358 -0
added resources/nixos/service.nix
@@ -0,0 +1,358 @@
+
{ 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;
+
  };
+

+
  # 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 [
+
      {
+
        BindReadOnlyPaths = [
+
          "${cfg.configFile}:${env.RAD_HOME}/config.json"
+
          "${if lib.types.path.check cfg.publicKey then cfg.publicKey else pkgs.writeText "radicle.pub" cfg.publicKey}:${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
+
        (lib.getLib pkgs.nss)
+
        pkgs.tzdata
+
      ];
+
    };
+
  };
+
in
+
{
+
  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 ../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.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
+
  ];
+
}
+