8 cfg = config.services.ncps;
20 globalFlags = lib.concatStringsSep " " (
21 [ "--log-level='${cfg.logLevel}'" ]
22 ++ (lib.optionals cfg.openTelemetry.enable (
27 cfg.openTelemetry.grpcURL != null
28 ) "--otel-grpc-url='${cfg.openTelemetry.grpcURL}'")
32 serveFlags = lib.concatStringsSep " " (
34 "--cache-hostname='${cfg.cache.hostName}'"
35 "--cache-data-path='${cfg.cache.dataPath}'"
36 "--cache-database-url='${cfg.cache.databaseURL}'"
37 "--server-addr='${cfg.server.addr}'"
39 ++ (lib.optional cfg.cache.allowDeleteVerb "--cache-allow-delete-verb")
40 ++ (lib.optional cfg.cache.allowPutVerb "--cache-allow-put-verb")
41 ++ (lib.optional (cfg.cache.maxSize != null) "--cache-max-size='${cfg.cache.maxSize}'")
42 ++ (lib.optionals (cfg.cache.lru.schedule != null) [
43 "--cache-lru-schedule='${cfg.cache.lru.schedule}'"
44 "--cache-lru-schedule-timezone='${cfg.cache.lru.scheduleTimeZone}'"
46 ++ (lib.optional (cfg.cache.secretKeyPath != null) "--cache-secret-key-path='%d/secretKey'")
47 ++ (lib.forEach cfg.upstream.caches (url: "--upstream-cache='${url}'"))
48 ++ (lib.forEach cfg.upstream.publicKeys (pk: "--upstream-public-key='${pk}'"))
51 isSqlite = lib.strings.hasPrefix "sqlite:" cfg.cache.databaseURL;
53 dbPath = lib.removePrefix "sqlite:" cfg.cache.databaseURL;
59 enable = lib.mkEnableOption "ncps: Nix binary cache proxy service implemented in Go";
61 package = lib.mkPackageOption pkgs "ncps" { };
63 dbmatePackage = lib.mkPackageOption pkgs "dbmate" { };
66 enable = lib.mkEnableOption "Enable OpenTelemetry logs, metrics, and tracing";
68 grpcURL = lib.mkOption {
69 type = lib.types.nullOr lib.types.str;
72 Configure OpenTelemetry gRPC URL. Missing or "https" scheme enables
73 secure gRPC, "insecure" otherwise. Omit to emit telemetry to
79 logLevel = lib.mkOption {
80 type = lib.types.enum logLevels;
83 Set the level for logging. Refer to
84 <https://pkg.go.dev/github.com/rs/zerolog#readme-leveled-logging> for
90 allowDeleteVerb = lib.mkEnableOption ''
91 Whether to allow the DELETE verb to delete narinfo and nar files from
95 allowPutVerb = lib.mkEnableOption ''
96 Whether to allow the PUT verb to push narinfo and nar files directly
100 hostName = lib.mkOption {
101 type = lib.types.str;
103 The hostname of the cache server. **This is used to generate the
104 private key used for signing store paths (.narinfo)**
108 dataPath = lib.mkOption {
109 type = lib.types.str;
110 default = "/var/lib/ncps";
112 The local directory for storing configuration and cached store paths
116 databaseURL = lib.mkOption {
117 type = lib.types.str;
118 default = "sqlite:${cfg.cache.dataPath}/db/db.sqlite";
119 defaultText = "sqlite:/var/lib/ncps/db/db.sqlite";
121 The URL of the database (currently only SQLite is supported)
126 schedule = lib.mkOption {
127 type = lib.types.nullOr lib.types.str;
129 example = "0 2 * * *";
131 The cron spec for cleaning the store to keep it under
132 config.ncps.cache.maxSize. Refer to
133 https://pkg.go.dev/github.com/robfig/cron/v3#hdr-Usage for
138 scheduleTimeZone = lib.mkOption {
139 type = lib.types.str;
141 example = "America/Los_Angeles";
143 The name of the timezone to use for the cron schedule. See
144 <https://en.wikipedia.org/wiki/List_of_tz_database_time_zones>
145 for a comprehensive list of possible values for this setting.
150 maxSize = lib.mkOption {
151 type = lib.types.nullOr lib.types.str;
155 The maximum size of the store. It can be given with units such as
156 5K, 10G etc. Supported units: B, K, M, G, T.
160 secretKeyPath = lib.mkOption {
161 type = lib.types.nullOr lib.types.str;
164 The path to load the secretKey for signing narinfos. Leave this
165 empty to automatically generate a private/public key.
171 addr = lib.mkOption {
172 type = lib.types.str;
175 The address and port the server listens on.
181 caches = lib.mkOption {
182 type = lib.types.listOf lib.types.str;
183 example = [ "https://cache.nixos.org" ];
185 A list of URLs of upstream binary caches.
189 publicKeys = lib.mkOption {
190 type = lib.types.listOf lib.types.str;
192 example = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ];
194 A list of public keys of upstream caches in the format
195 `host[-[0-9]*]:public-key`. This flag is used to verify the
196 signatures of store paths downloaded from upstream caches.
203 config = lib.mkIf cfg.enable {
206 assertion = cfg.cache.lru.schedule == null || cfg.cache.maxSize != null;
207 message = "You must specify config.ncps.cache.lru.schedule when config.ncps.cache.maxSize is set";
211 assertion = cfg.cache.secretKeyPath == null || (builtins.pathExists cfg.cache.secretKeyPath);
212 message = "config.ncps.cache.secresecretKeyPath=${cfg.cache.secretKeyPath} must exist but does not";
220 users.groups.ncps = { };
222 systemd.services.ncps-create-datadirs = {
223 description = "Created required directories by ncps";
229 (lib.optionalString (cfg.cache.dataPath != "/var/lib/ncps") ''
230 if ! test -d ${cfg.cache.dataPath}; then
231 mkdir -p ${cfg.cache.dataPath}
232 chown ncps:ncps ${cfg.cache.dataPath}
235 + (lib.optionalString isSqlite ''
236 if ! test -d ${dbDir}; then
238 chown ncps:ncps ${dbDir}
241 wantedBy = [ "ncps.service" ];
242 before = [ "ncps.service" ];
245 systemd.services.ncps = {
246 description = "ncps binary cache proxy service";
248 after = [ "network.target" ];
249 wantedBy = [ "multi-user.target" ];
252 ${lib.getExe cfg.dbmatePackage} --migrations-dir=${cfg.package}/share/ncps/db/migrations --url=${cfg.cache.databaseURL} up
255 serviceConfig = lib.mkMerge [
257 ExecStart = "${lib.getExe cfg.package} ${globalFlags} serve ${serveFlags}";
260 Restart = "on-failure";
261 RuntimeDirectory = "ncps";
265 (lib.mkIf (cfg.cache.secretKeyPath != null) {
266 LoadCredential = "secretKey:${cfg.cache.secretKeyPath}";
269 # ensure permissions on required directories
270 (lib.mkIf (cfg.cache.dataPath != "/var/lib/ncps") {
271 ReadWritePaths = [ cfg.cache.dataPath ];
273 (lib.mkIf (cfg.cache.dataPath == "/var/lib/ncps") {
274 StateDirectory = "ncps";
275 StateDirectoryMode = "0700";
277 (lib.mkIf (isSqlite && !lib.strings.hasPrefix "/var/lib/ncps" dbDir) {
278 ReadWritePaths = [ dbDir ];
288 CapabilityBoundingSet = "";
290 DevicePolicy = "closed";
291 DeviceAllow = [ "" ];
292 ProtectKernelModules = true;
293 ProtectKernelTunables = true;
294 ProtectControlGroups = true;
295 ProtectKernelLogs = true;
296 ProtectHostname = true;
298 ProtectProc = "invisible";
299 ProtectSystem = "strict";
301 RestrictSUIDSGID = true;
302 RestrictRealtime = true;
303 MemoryDenyWriteExecute = true;
305 RestrictNamespaces = true;
306 SystemCallArchitectures = "native";
307 PrivateNetwork = false;
309 PrivateDevices = true;
310 PrivateMounts = true;
311 NoNewPrivileges = true;
312 LockPersonality = true;
313 RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
319 unitConfig.RequiresMountsFor = lib.concatStringsSep " " (
320 [ "${cfg.cache.dataPath}" ] ++ lib.optional (isSqlite) dbDir
325 meta.maintainers = with lib.maintainers; [ kalbasit ];