Unverified Commit 1dd16a79 authored by Wael Nasreddine's avatar Wael Nasreddine Committed by GitHub
Browse files

nixos/ncps: init service (#370153)

parents aff999e6 07144733
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -51,6 +51,8 @@

- [networking.modemmanager](options.html#opt-networking.modemmanager) has been split out of [networking.networkmanager](options.html#opt-networking.networkmanager). NetworkManager still enables ModemManager by default, but options exist now to run NetworkManager without ModemManager.

- [ncps](https://github.com/kalbasit/ncps), a Nix binary cache proxy service implemented in Go using [go-nix](https://github.com/nix-community/go-nix). Available as [services.ncps](options.html#opt-services.ncps.enable).

- [Conduwuit](https://conduwuit.puppyirl.gay/), a federated chat server implementing the Matrix protocol, forked from Conduit. Available as [services.conduwuit](#opt-services.conduwuit.enable).

- [Traccar](https://www.traccar.org/), a modern GPS Tracking Platform. Available as [services.traccar](#opt-services.traccar.enable).
+1 −0
Original line number Diff line number Diff line
@@ -1160,6 +1160,7 @@
  ./services/networking/nats.nix
  ./services/networking/nbd.nix
  ./services/networking/ncdns.nix
  ./services/networking/ncps.nix
  ./services/networking/ndppd.nix
  ./services/networking/nebula.nix
  ./services/networking/netbird.nix
+326 −0
Original line number Diff line number Diff line
{
  config,
  pkgs,
  lib,
  ...
}:
let
  cfg = config.services.ncps;

  logLevels = [
    "trace"
    "debug"
    "info"
    "warn"
    "error"
    "fatal"
    "panic"
  ];

  globalFlags = lib.concatStringsSep " " (
    [ "--log-level='${cfg.logLevel}'" ]
    ++ (lib.optionals cfg.openTelemetry.enable (
      [
        "--otel-enabled"
      ]
      ++ (lib.optional (
        cfg.openTelemetry.grpcURL != null
      ) "--otel-grpc-url='${cfg.openTelemetry.grpcURL}'")
    ))
  );

  serveFlags = lib.concatStringsSep " " (
    [
      "--cache-hostname='${cfg.cache.hostName}'"
      "--cache-data-path='${cfg.cache.dataPath}'"
      "--cache-database-url='${cfg.cache.databaseURL}'"
      "--server-addr='${cfg.server.addr}'"
    ]
    ++ (lib.optional cfg.cache.allowDeleteVerb "--cache-allow-delete-verb")
    ++ (lib.optional cfg.cache.allowPutVerb "--cache-allow-put-verb")
    ++ (lib.optional (cfg.cache.maxSize != null) "--cache-max-size='${cfg.cache.maxSize}'")
    ++ (lib.optionals (cfg.cache.lru.schedule != null) [
      "--cache-lru-schedule='${cfg.cache.lru.schedule}'"
      "--cache-lru-schedule-timezone='${cfg.cache.lru.scheduleTimeZone}'"
    ])
    ++ (lib.optional (cfg.cache.secretKeyPath != null) "--cache-secret-key-path='%d/secretKey'")
    ++ (lib.forEach cfg.upstream.caches (url: "--upstream-cache='${url}'"))
    ++ (lib.forEach cfg.upstream.publicKeys (pk: "--upstream-public-key='${pk}'"))
  );

  isSqlite = lib.strings.hasPrefix "sqlite:" cfg.cache.databaseURL;

  dbPath = lib.removePrefix "sqlite:" cfg.cache.databaseURL;
  dbDir = dirOf dbPath;
in
{
  options = {
    services.ncps = {
      enable = lib.mkEnableOption "ncps: Nix binary cache proxy service implemented in Go";

      package = lib.mkPackageOption pkgs "ncps" { };

      dbmatePackage = lib.mkPackageOption pkgs "dbmate" { };

      openTelemetry = {
        enable = lib.mkEnableOption "Enable OpenTelemetry logs, metrics, and tracing";

        grpcURL = lib.mkOption {
          type = lib.types.nullOr lib.types.str;
          default = null;
          description = ''
            Configure OpenTelemetry gRPC URL. Missing or "https" scheme enables
            secure gRPC, "insecure" otherwise. Omit to emit telemetry to
            stdout.
          '';
        };
      };

      logLevel = lib.mkOption {
        type = lib.types.enum logLevels;
        default = "info";
        description = ''
          Set the level for logging. Refer to
          <https://pkg.go.dev/github.com/rs/zerolog#readme-leveled-logging> for
          more information.
        '';
      };

      cache = {
        allowDeleteVerb = lib.mkEnableOption ''
          Whether to allow the DELETE verb to delete narinfo and nar files from
          the cache.
        '';

        allowPutVerb = lib.mkEnableOption ''
          Whether to allow the PUT verb to push narinfo and nar files directly
          to the cache.
        '';

        hostName = lib.mkOption {
          type = lib.types.str;
          description = ''
            The hostname of the cache server. **This is used to generate the
            private key used for signing store paths (.narinfo)**
          '';
        };

        dataPath = lib.mkOption {
          type = lib.types.str;
          default = "/var/lib/ncps";
          description = ''
            The local directory for storing configuration and cached store paths
          '';
        };

        databaseURL = lib.mkOption {
          type = lib.types.str;
          default = "sqlite:${cfg.cache.dataPath}/db/db.sqlite";
          defaultText = "sqlite:/var/lib/ncps/db/db.sqlite";
          description = ''
            The URL of the database (currently only SQLite is supported)
          '';
        };

        lru = {
          schedule = lib.mkOption {
            type = lib.types.nullOr lib.types.str;
            default = null;
            example = "0 2 * * *";
            description = ''
              The cron spec for cleaning the store to keep it under
              config.ncps.cache.maxSize. Refer to
              https://pkg.go.dev/github.com/robfig/cron/v3#hdr-Usage for
              documentation.
            '';
          };

          scheduleTimeZone = lib.mkOption {
            type = lib.types.str;
            default = "Local";
            example = "America/Los_Angeles";
            description = ''
              The name of the timezone to use for the cron schedule. See
              <https://en.wikipedia.org/wiki/List_of_tz_database_time_zones>
              for a comprehensive list of possible values for this setting.
            '';
          };
        };

        maxSize = lib.mkOption {
          type = lib.types.nullOr lib.types.str;
          default = null;
          example = "100G";
          description = ''
            The maximum size of the store. It can be given with units such as
            5K, 10G etc. Supported units: B, K, M, G, T.
          '';
        };

        secretKeyPath = lib.mkOption {
          type = lib.types.nullOr lib.types.str;
          default = null;
          description = ''
            The path to load the secretKey for signing narinfos. Leave this
            empty to automatically generate a private/public key.
          '';
        };
      };

      server = {
        addr = lib.mkOption {
          type = lib.types.str;
          default = ":8501";
          description = ''
            The address and port the server listens on.
          '';
        };
      };

      upstream = {
        caches = lib.mkOption {
          type = lib.types.listOf lib.types.str;
          example = [ "https://cache.nixos.org" ];
          description = ''
            A list of URLs of upstream binary caches.
          '';
        };

        publicKeys = lib.mkOption {
          type = lib.types.listOf lib.types.str;
          default = [ ];
          example = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ];
          description = ''
            A list of public keys of upstream caches in the format
            `host[-[0-9]*]:public-key`. This flag is used to verify the
            signatures of store paths downloaded from upstream caches.
          '';
        };
      };
    };
  };

  config = lib.mkIf cfg.enable {
    assertions = [
      {
        assertion = cfg.cache.lru.schedule == null || cfg.cache.maxSize != null;
        message = "You must specify config.ncps.cache.lru.schedule when config.ncps.cache.maxSize is set";
      }

      {
        assertion = cfg.cache.secretKeyPath == null || (builtins.pathExists cfg.cache.secretKeyPath);
        message = "config.ncps.cache.secresecretKeyPath=${cfg.cache.secretKeyPath} must exist but does not";
      }
    ];

    users.users.ncps = {
      isSystemUser = true;
      group = "ncps";
    };
    users.groups.ncps = { };

    systemd.services.ncps-create-datadirs = {
      description = "Created required directories by ncps";
      serviceConfig = {
        Type = "oneshot";
        UMask = "0066";
      };
      script =
        (lib.optionalString (cfg.cache.dataPath != "/var/lib/ncps") ''
          if ! test -d ${cfg.cache.dataPath}; then
            mkdir -p ${cfg.cache.dataPath}
            chown ncps:ncps ${cfg.cache.dataPath}
          fi
        '')
        + (lib.optionalString isSqlite ''
          if ! test -d ${dbDir}; then
            mkdir -p ${dbDir}
            chown ncps:ncps ${dbDir}
          fi
        '');
      wantedBy = [ "ncps.service" ];
      before = [ "ncps.service" ];
    };

    systemd.services.ncps = {
      description = "ncps binary cache proxy service";

      after = [ "network.target" ];
      wantedBy = [ "multi-user.target" ];

      preStart = ''
        ${lib.getExe cfg.dbmatePackage} --migrations-dir=${cfg.package}/share/ncps/db/migrations --url=${cfg.cache.databaseURL} up
      '';

      serviceConfig = lib.mkMerge [
        {
          ExecStart = "${lib.getExe cfg.package} ${globalFlags} serve ${serveFlags}";
          User = "ncps";
          Group = "ncps";
          Restart = "on-failure";
          RuntimeDirectory = "ncps";
        }

        # credentials
        (lib.mkIf (cfg.cache.secretKeyPath != null) {
          LoadCredential = "secretKey:${cfg.cache.secretKeyPath}";
        })

        # ensure permissions on required directories
        (lib.mkIf (cfg.cache.dataPath != "/var/lib/ncps") {
          ReadWritePaths = [ cfg.cache.dataPath ];
        })
        (lib.mkIf (cfg.cache.dataPath == "/var/lib/ncps") {
          StateDirectory = "ncps";
          StateDirectoryMode = "0700";
        })
        (lib.mkIf (isSqlite && !lib.strings.hasPrefix "/var/lib/ncps" dbDir) {
          ReadWritePaths = [ dbDir ];
        })

        # Hardening
        {
          SystemCallFilter = [
            "@system-service"
            "~@privileged"
            "~@resources"
          ];
          CapabilityBoundingSet = "";
          PrivateUsers = true;
          DevicePolicy = "closed";
          DeviceAllow = [ "" ];
          ProtectKernelModules = true;
          ProtectKernelTunables = true;
          ProtectControlGroups = true;
          ProtectKernelLogs = true;
          ProtectHostname = true;
          ProtectClock = true;
          ProtectProc = "invisible";
          ProtectSystem = "strict";
          ProtectHome = true;
          RestrictSUIDSGID = true;
          RestrictRealtime = true;
          MemoryDenyWriteExecute = true;
          ProcSubset = "pid";
          RestrictNamespaces = true;
          SystemCallArchitectures = "native";
          PrivateNetwork = false;
          PrivateTmp = true;
          PrivateDevices = true;
          PrivateMounts = true;
          NoNewPrivileges = true;
          LockPersonality = true;
          RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
          LimitNOFILE = 65536;
          UMask = "0066";
        }
      ];

      unitConfig.RequiresMountsFor = lib.concatStringsSep " " (
        [ "${cfg.cache.dataPath}" ] ++ lib.optional (isSqlite) dbDir
      );
    };
  };

  meta.maintainers = with lib.maintainers; [ kalbasit ];
}
+5 −0
Original line number Diff line number Diff line
@@ -660,6 +660,11 @@ in {
  navidrome = handleTest ./navidrome.nix {};
  nbd = handleTest ./nbd.nix {};
  ncdns = handleTest ./ncdns.nix {};
  ncps = runTest ./ncps.nix;
  ncps-custom-cache-datapath = runTest {
    imports = [ ./ncps.nix ];
    defaults.services.ncps.cache.dataPath =  "/path/to/ncps";
  };
  ndppd = handleTest ./ndppd.nix {};
  nix-channel = pkgs.callPackage ../modules/config/nix-channel/test.nix { };
  nebula = handleTest ./nebula.nix {};

nixos/tests/ncps.nix

0 → 100644
+89 −0
Original line number Diff line number Diff line
{
  lib,
  pkgs,
  ...
}:

{
  name = "ncps";

  nodes = {
    harmonia = {
      services.harmonia = {
        enable = true;
        signKeyPaths = [
          (pkgs.writeText "cache-key" "cache.example.com-1:9FhO0w+7HjZrhvmzT1VlAZw4OSAlFGTgC24Seg3tmPl4gZBdwZClzTTHr9cVzJpwsRSYLTu7hEAQe3ljy92CWg==")
        ];
        settings.priority = 35;
      };

      networking.firewall.allowedTCPPorts = [ 5000 ];
      system.extraDependencies = [ pkgs.emptyFile ];
    };

    ncps = {
      services.ncps = {
        enable = true;

        cache = {
          hostName = "ncps";
          secretKeyPath = builtins.toString (
            pkgs.writeText "ncps-cache-key" "ncps:dcrGsrku0KvltFhrR5lVIMqyloAdo0y8vYZOeIFUSLJS2IToL7dPHSSCk/fi+PJf8EorpBn8PU7MNhfvZoI8mA=="
          );
        };

        upstream = {
          caches = [ "http://harmonia:5000" ];
          publicKeys = [
            "cache.example.com-1:eIGQXcGQpc00x6/XFcyacLEUmC07u4RAEHt5Y8vdglo="
          ];
        };
      };

      networking.firewall.allowedTCPPorts = [ 8501 ];
    };

    client01 = {
      nix.settings = {
        substituters = lib.mkForce [ "http://ncps:8501" ];
        trusted-public-keys = lib.mkForce [
          "ncps:UtiE6C+3Tx0kgpP34vjyX/BKK6QZ/D1OzDYX72aCPJg="
        ];
      };
    };
  };

  testScript =
    { nodes, ... }:
    let
      narinfoName =
        (lib.strings.removePrefix "/nix/store/" (
          lib.strings.removeSuffix "-empty-file" pkgs.emptyFile.outPath
        ))
        + ".narinfo";

      narinfoNameChars = lib.strings.stringToCharacters narinfoName;

      narinfoPath = lib.concatStringsSep "/" [
        nodes.ncps.services.ncps.cache.dataPath
        "store/narinfo"
        (lib.lists.elemAt narinfoNameChars 0)
        ((lib.lists.elemAt narinfoNameChars 0) + (lib.lists.elemAt narinfoNameChars 1))
        narinfoName
      ];
    in
    ''
      start_all()

      harmonia.wait_for_unit("harmonia.service")

      ncps.wait_for_unit("ncps.service")

      client01.wait_until_succeeds("curl -f http://ncps:8501/ | grep '\"hostname\":\"${toString nodes.ncps.services.ncps.cache.hostName}\"' >&2")

      client01.succeed("cat /etc/nix/nix.conf >&2")
      client01.succeed("nix-store --realise ${pkgs.emptyFile}")

      ncps.succeed("cat ${narinfoPath} >&2")
    '';
}