|
| 1 | +{ |
| 2 | + config, |
| 3 | + pkgs, |
| 4 | + lib, |
| 5 | + ... |
| 6 | +}: |
| 7 | +let |
| 8 | + cfg = config.services.ncps; |
| 9 | + |
| 10 | + logLevels = [ |
| 11 | + "trace" |
| 12 | + "debug" |
| 13 | + "info" |
| 14 | + "warn" |
| 15 | + "error" |
| 16 | + "fatal" |
| 17 | + "panic" |
| 18 | + ]; |
| 19 | + |
| 20 | + globalFlags = lib.concatStringsSep " " ( |
| 21 | + [ "--log-level='${cfg.logLevel}'" ] |
| 22 | + ++ (lib.optionals cfg.openTelemetry.enable ( |
| 23 | + [ |
| 24 | + "--otel-enabled" |
| 25 | + ] |
| 26 | + ++ (lib.optional ( |
| 27 | + cfg.openTelemetry.grpcURL != null |
| 28 | + ) "--otel-grpc-url='${cfg.openTelemetry.grpcURL}'") |
| 29 | + )) |
| 30 | + ); |
| 31 | + |
| 32 | + serveFlags = lib.concatStringsSep " " ( |
| 33 | + [ |
| 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}'" |
| 38 | + ] |
| 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}'" |
| 45 | + ]) |
| 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}'")) |
| 49 | + ); |
| 50 | + |
| 51 | + isSqlite = lib.strings.hasPrefix "sqlite:" cfg.cache.databaseURL; |
| 52 | + |
| 53 | + dbPath = lib.removePrefix "sqlite:" cfg.cache.databaseURL; |
| 54 | + dbDir = dirOf dbPath; |
| 55 | +in |
| 56 | +{ |
| 57 | + options = { |
| 58 | + services.ncps = { |
| 59 | + enable = lib.mkEnableOption "ncps: Nix binary cache proxy service implemented in Go"; |
| 60 | + |
| 61 | + package = lib.mkPackageOption pkgs "ncps" { }; |
| 62 | + |
| 63 | + dbmatePackage = lib.mkPackageOption pkgs "dbmate" { }; |
| 64 | + |
| 65 | + openTelemetry = { |
| 66 | + enable = lib.mkEnableOption "Enable OpenTelemetry logs, metrics, and tracing"; |
| 67 | + |
| 68 | + grpcURL = lib.mkOption { |
| 69 | + type = lib.types.nullOr lib.types.str; |
| 70 | + default = null; |
| 71 | + description = '' |
| 72 | + Configure OpenTelemetry gRPC URL. Missing or "https" scheme enables |
| 73 | + secure gRPC, "insecure" otherwise. Omit to emit telemetry to |
| 74 | + stdout. |
| 75 | + ''; |
| 76 | + }; |
| 77 | + }; |
| 78 | + |
| 79 | + logLevel = lib.mkOption { |
| 80 | + type = lib.types.enum logLevels; |
| 81 | + default = "info"; |
| 82 | + description = '' |
| 83 | + Set the level for logging. Refer to |
| 84 | + <https://pkg.go.dev/github.com/rs/zerolog#readme-leveled-logging> for |
| 85 | + more information. |
| 86 | + ''; |
| 87 | + }; |
| 88 | + |
| 89 | + cache = { |
| 90 | + allowDeleteVerb = lib.mkEnableOption '' |
| 91 | + Whether to allow the DELETE verb to delete narinfo and nar files from |
| 92 | + the cache. |
| 93 | + ''; |
| 94 | + |
| 95 | + allowPutVerb = lib.mkEnableOption '' |
| 96 | + Whether to allow the PUT verb to push narinfo and nar files directly |
| 97 | + to the cache. |
| 98 | + ''; |
| 99 | + |
| 100 | + hostName = lib.mkOption { |
| 101 | + type = lib.types.str; |
| 102 | + description = '' |
| 103 | + The hostname of the cache server. **This is used to generate the |
| 104 | + private key used for signing store paths (.narinfo)** |
| 105 | + ''; |
| 106 | + }; |
| 107 | + |
| 108 | + dataPath = lib.mkOption { |
| 109 | + type = lib.types.str; |
| 110 | + default = "/var/lib/ncps"; |
| 111 | + description = '' |
| 112 | + The local directory for storing configuration and cached store paths |
| 113 | + ''; |
| 114 | + }; |
| 115 | + |
| 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"; |
| 120 | + description = '' |
| 121 | + The URL of the database (currently only SQLite is supported) |
| 122 | + ''; |
| 123 | + }; |
| 124 | + |
| 125 | + lru = { |
| 126 | + schedule = lib.mkOption { |
| 127 | + type = lib.types.nullOr lib.types.str; |
| 128 | + default = null; |
| 129 | + example = "0 2 * * *"; |
| 130 | + description = '' |
| 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 |
| 134 | + documentation. |
| 135 | + ''; |
| 136 | + }; |
| 137 | + |
| 138 | + scheduleTimeZone = lib.mkOption { |
| 139 | + type = lib.types.str; |
| 140 | + default = "Local"; |
| 141 | + example = "America/Los_Angeles"; |
| 142 | + description = '' |
| 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. |
| 146 | + ''; |
| 147 | + }; |
| 148 | + }; |
| 149 | + |
| 150 | + maxSize = lib.mkOption { |
| 151 | + type = lib.types.nullOr lib.types.str; |
| 152 | + default = null; |
| 153 | + example = "100G"; |
| 154 | + description = '' |
| 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. |
| 157 | + ''; |
| 158 | + }; |
| 159 | + |
| 160 | + secretKeyPath = lib.mkOption { |
| 161 | + type = lib.types.nullOr lib.types.str; |
| 162 | + default = null; |
| 163 | + description = '' |
| 164 | + The path to load the secretKey for signing narinfos. Leave this |
| 165 | + empty to automatically generate a private/public key. |
| 166 | + ''; |
| 167 | + }; |
| 168 | + }; |
| 169 | + |
| 170 | + server = { |
| 171 | + addr = lib.mkOption { |
| 172 | + type = lib.types.str; |
| 173 | + default = ":8501"; |
| 174 | + description = '' |
| 175 | + The address and port the server listens on. |
| 176 | + ''; |
| 177 | + }; |
| 178 | + }; |
| 179 | + |
| 180 | + upstream = { |
| 181 | + caches = lib.mkOption { |
| 182 | + type = lib.types.listOf lib.types.str; |
| 183 | + example = [ "https://cache.nixos.org" ]; |
| 184 | + description = '' |
| 185 | + A list of URLs of upstream binary caches. |
| 186 | + ''; |
| 187 | + }; |
| 188 | + |
| 189 | + publicKeys = lib.mkOption { |
| 190 | + type = lib.types.listOf lib.types.str; |
| 191 | + default = [ ]; |
| 192 | + example = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ]; |
| 193 | + description = '' |
| 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. |
| 197 | + ''; |
| 198 | + }; |
| 199 | + }; |
| 200 | + }; |
| 201 | + }; |
| 202 | + |
| 203 | + config = lib.mkIf cfg.enable { |
| 204 | + assertions = [ |
| 205 | + { |
| 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"; |
| 208 | + } |
| 209 | + |
| 210 | + { |
| 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"; |
| 213 | + } |
| 214 | + ]; |
| 215 | + |
| 216 | + users.users.ncps = { |
| 217 | + isSystemUser = true; |
| 218 | + group = "ncps"; |
| 219 | + }; |
| 220 | + users.groups.ncps = { }; |
| 221 | + |
| 222 | + systemd.services.ncps-create-datadirs = { |
| 223 | + description = "Created required directories by ncps"; |
| 224 | + serviceConfig = { |
| 225 | + Type = "oneshot"; |
| 226 | + UMask = "0066"; |
| 227 | + }; |
| 228 | + script = |
| 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} |
| 233 | + fi |
| 234 | + '') |
| 235 | + + (lib.optionalString isSqlite '' |
| 236 | + if ! test -d ${dbDir}; then |
| 237 | + mkdir -p ${dbDir} |
| 238 | + chown ncps:ncps ${dbDir} |
| 239 | + fi |
| 240 | + ''); |
| 241 | + wantedBy = [ "ncps.service" ]; |
| 242 | + before = [ "ncps.service" ]; |
| 243 | + }; |
| 244 | + |
| 245 | + systemd.services.ncps = { |
| 246 | + description = "ncps binary cache proxy service"; |
| 247 | + |
| 248 | + after = [ "network.target" ]; |
| 249 | + wantedBy = [ "multi-user.target" ]; |
| 250 | + |
| 251 | + preStart = '' |
| 252 | + ${lib.getExe cfg.dbmatePackage} --migrations-dir=${cfg.package}/share/ncps/db/migrations --url=${cfg.cache.databaseURL} up |
| 253 | + ''; |
| 254 | + |
| 255 | + serviceConfig = lib.mkMerge [ |
| 256 | + { |
| 257 | + ExecStart = "${lib.getExe cfg.package} ${globalFlags} serve ${serveFlags}"; |
| 258 | + User = "ncps"; |
| 259 | + Group = "ncps"; |
| 260 | + Restart = "on-failure"; |
| 261 | + RuntimeDirectory = "ncps"; |
| 262 | + } |
| 263 | + |
| 264 | + # credentials |
| 265 | + (lib.mkIf (cfg.cache.secretKeyPath != null) { |
| 266 | + LoadCredential = "secretKey:${cfg.cache.secretKeyPath}"; |
| 267 | + }) |
| 268 | + |
| 269 | + # ensure permissions on required directories |
| 270 | + (lib.mkIf (cfg.cache.dataPath != "/var/lib/ncps") { |
| 271 | + ReadWritePaths = [ cfg.cache.dataPath ]; |
| 272 | + }) |
| 273 | + (lib.mkIf (cfg.cache.dataPath == "/var/lib/ncps") { |
| 274 | + StateDirectory = "ncps"; |
| 275 | + StateDirectoryMode = "0700"; |
| 276 | + }) |
| 277 | + (lib.mkIf (isSqlite && !lib.strings.hasPrefix "/var/lib/ncps" dbDir) { |
| 278 | + ReadWritePaths = [ dbDir ]; |
| 279 | + }) |
| 280 | + |
| 281 | + # Hardening |
| 282 | + { |
| 283 | + SystemCallFilter = [ |
| 284 | + "@system-service" |
| 285 | + "~@privileged" |
| 286 | + "~@resources" |
| 287 | + ]; |
| 288 | + CapabilityBoundingSet = ""; |
| 289 | + PrivateUsers = true; |
| 290 | + DevicePolicy = "closed"; |
| 291 | + DeviceAllow = [ "" ]; |
| 292 | + ProtectKernelModules = true; |
| 293 | + ProtectKernelTunables = true; |
| 294 | + ProtectControlGroups = true; |
| 295 | + ProtectKernelLogs = true; |
| 296 | + ProtectHostname = true; |
| 297 | + ProtectClock = true; |
| 298 | + ProtectProc = "invisible"; |
| 299 | + ProtectSystem = "strict"; |
| 300 | + ProtectHome = true; |
| 301 | + RestrictSUIDSGID = true; |
| 302 | + RestrictRealtime = true; |
| 303 | + MemoryDenyWriteExecute = true; |
| 304 | + ProcSubset = "pid"; |
| 305 | + RestrictNamespaces = true; |
| 306 | + SystemCallArchitectures = "native"; |
| 307 | + PrivateNetwork = false; |
| 308 | + PrivateTmp = true; |
| 309 | + PrivateDevices = true; |
| 310 | + PrivateMounts = true; |
| 311 | + NoNewPrivileges = true; |
| 312 | + LockPersonality = true; |
| 313 | + RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6"; |
| 314 | + LimitNOFILE = 65536; |
| 315 | + UMask = "0066"; |
| 316 | + } |
| 317 | + ]; |
| 318 | + |
| 319 | + unitConfig.RequiresMountsFor = lib.concatStringsSep " " ( |
| 320 | + [ "${cfg.cache.dataPath}" ] ++ lib.optional (isSqlite) dbDir |
| 321 | + ); |
| 322 | + }; |
| 323 | + }; |
| 324 | + |
| 325 | + meta.maintainers = with lib.maintainers; [ kalbasit ]; |
| 326 | +} |
0 commit comments