at master 11 kB view raw
1{ 2 config, 3 pkgs, 4 lib, 5 ... 6}: 7let 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 ++ (lib.optionals cfg.prometheus.enable [ 31 "--prometheus-enabled" 32 ]) 33 ); 34 35 serveFlags = lib.concatStringsSep " " ( 36 [ 37 "--cache-hostname='${cfg.cache.hostName}'" 38 "--cache-data-path='${cfg.cache.dataPath}'" 39 "--cache-database-url='${cfg.cache.databaseURL}'" 40 "--cache-temp-path='${cfg.cache.tempPath}'" 41 "--server-addr='${cfg.server.addr}'" 42 ] 43 ++ (lib.optional cfg.cache.allowDeleteVerb "--cache-allow-delete-verb") 44 ++ (lib.optional cfg.cache.allowPutVerb "--cache-allow-put-verb") 45 ++ (lib.optional (cfg.cache.maxSize != null) "--cache-max-size='${cfg.cache.maxSize}'") 46 ++ (lib.optionals (cfg.cache.lru.schedule != null) [ 47 "--cache-lru-schedule='${cfg.cache.lru.schedule}'" 48 "--cache-lru-schedule-timezone='${cfg.cache.lru.scheduleTimeZone}'" 49 ]) 50 ++ (lib.optional (cfg.cache.secretKeyPath != null) "--cache-secret-key-path='%d/secretKey'") 51 ++ (lib.forEach cfg.upstream.caches (url: "--upstream-cache='${url}'")) 52 ++ (lib.forEach cfg.upstream.publicKeys (pk: "--upstream-public-key='${pk}'")) 53 ); 54 55 isSqlite = lib.strings.hasPrefix "sqlite:" cfg.cache.databaseURL; 56 57 dbPath = lib.removePrefix "sqlite:" cfg.cache.databaseURL; 58 dbDir = dirOf dbPath; 59in 60{ 61 options = { 62 services.ncps = { 63 enable = lib.mkEnableOption "ncps: Nix binary cache proxy service implemented in Go"; 64 65 package = lib.mkPackageOption pkgs "ncps" { }; 66 67 dbmatePackage = lib.mkPackageOption pkgs "dbmate" { }; 68 69 openTelemetry = { 70 enable = lib.mkEnableOption "Enable OpenTelemetry logs, metrics, and tracing"; 71 72 grpcURL = lib.mkOption { 73 type = lib.types.nullOr lib.types.str; 74 default = null; 75 description = '' 76 Configure OpenTelemetry gRPC URL. Missing or "https" scheme enables 77 secure gRPC, "insecure" otherwise. Omit to emit telemetry to 78 stdout. 79 ''; 80 }; 81 }; 82 83 prometheus.enable = lib.mkEnableOption "Enable Prometheus metrics endpoint at /metrics"; 84 85 logLevel = lib.mkOption { 86 type = lib.types.enum logLevels; 87 default = "info"; 88 description = '' 89 Set the level for logging. Refer to 90 <https://pkg.go.dev/github.com/rs/zerolog#readme-leveled-logging> for 91 more information. 92 ''; 93 }; 94 95 cache = { 96 allowDeleteVerb = lib.mkEnableOption '' 97 Whether to allow the DELETE verb to delete narinfo and nar files from 98 the cache. 99 ''; 100 101 allowPutVerb = lib.mkEnableOption '' 102 Whether to allow the PUT verb to push narinfo and nar files directly 103 to the cache. 104 ''; 105 106 hostName = lib.mkOption { 107 type = lib.types.str; 108 description = '' 109 The hostname of the cache server. **This is used to generate the 110 private key used for signing store paths (.narinfo)** 111 ''; 112 }; 113 114 dataPath = lib.mkOption { 115 type = lib.types.str; 116 default = "/var/lib/ncps"; 117 description = '' 118 The local directory for storing configuration and cached store paths 119 ''; 120 }; 121 122 databaseURL = lib.mkOption { 123 type = lib.types.str; 124 default = "sqlite:${cfg.cache.dataPath}/db/db.sqlite"; 125 defaultText = "sqlite:/var/lib/ncps/db/db.sqlite"; 126 description = '' 127 The URL of the database (currently only SQLite is supported) 128 ''; 129 }; 130 131 lru = { 132 schedule = lib.mkOption { 133 type = lib.types.nullOr lib.types.str; 134 default = null; 135 example = "0 2 * * *"; 136 description = '' 137 The cron spec for cleaning the store to keep it under 138 config.ncps.cache.maxSize. Refer to 139 https://pkg.go.dev/github.com/robfig/cron/v3#hdr-Usage for 140 documentation. 141 ''; 142 }; 143 144 scheduleTimeZone = lib.mkOption { 145 type = lib.types.str; 146 default = "Local"; 147 example = "America/Los_Angeles"; 148 description = '' 149 The name of the timezone to use for the cron schedule. See 150 <https://en.wikipedia.org/wiki/List_of_tz_database_time_zones> 151 for a comprehensive list of possible values for this setting. 152 ''; 153 }; 154 }; 155 156 maxSize = lib.mkOption { 157 type = lib.types.nullOr lib.types.str; 158 default = null; 159 example = "100G"; 160 description = '' 161 The maximum size of the store. It can be given with units such as 162 5K, 10G etc. Supported units: B, K, M, G, T. 163 ''; 164 }; 165 166 secretKeyPath = lib.mkOption { 167 type = lib.types.nullOr lib.types.str; 168 default = null; 169 description = '' 170 The path to load the secretKey for signing narinfos. Leave this 171 empty to automatically generate a private/public key. 172 ''; 173 }; 174 175 tempPath = lib.mkOption { 176 type = lib.types.str; 177 default = "/tmp"; 178 description = '' 179 The path to the temporary directory that is used by the cache to download NAR files 180 ''; 181 }; 182 }; 183 184 server = { 185 addr = lib.mkOption { 186 type = lib.types.str; 187 default = ":8501"; 188 description = '' 189 The address and port the server listens on. 190 ''; 191 }; 192 }; 193 194 upstream = { 195 caches = lib.mkOption { 196 type = lib.types.listOf lib.types.str; 197 example = [ "https://cache.nixos.org" ]; 198 description = '' 199 A list of URLs of upstream binary caches. 200 ''; 201 }; 202 203 publicKeys = lib.mkOption { 204 type = lib.types.listOf lib.types.str; 205 default = [ ]; 206 example = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ]; 207 description = '' 208 A list of public keys of upstream caches in the format 209 `host[-[0-9]*]:public-key`. This flag is used to verify the 210 signatures of store paths downloaded from upstream caches. 211 ''; 212 }; 213 }; 214 }; 215 }; 216 217 config = lib.mkIf cfg.enable { 218 assertions = [ 219 { 220 assertion = cfg.cache.lru.schedule == null || cfg.cache.maxSize != null; 221 message = "You must specify config.ncps.cache.lru.schedule when config.ncps.cache.maxSize is set"; 222 } 223 ]; 224 225 users.users.ncps = { 226 isSystemUser = true; 227 group = "ncps"; 228 }; 229 users.groups.ncps = { }; 230 231 systemd.services.ncps-create-directories = { 232 description = "Created required directories by ncps"; 233 serviceConfig = { 234 Type = "oneshot"; 235 UMask = "0066"; 236 }; 237 script = 238 (lib.optionalString (cfg.cache.dataPath != "/var/lib/ncps") '' 239 if ! test -d ${cfg.cache.dataPath}; then 240 mkdir -p ${cfg.cache.dataPath} 241 chown ncps:ncps ${cfg.cache.dataPath} 242 fi 243 '') 244 + (lib.optionalString isSqlite '' 245 if ! test -d ${dbDir}; then 246 mkdir -p ${dbDir} 247 chown ncps:ncps ${dbDir} 248 fi 249 '') 250 + (lib.optionalString (cfg.cache.tempPath != "/tmp") '' 251 if ! test -d ${cfg.cache.tempPath}; then 252 mkdir -p ${cfg.cache.tempPath} 253 chown ncps:ncps ${cfg.cache.tempPath} 254 fi 255 ''); 256 wantedBy = [ "ncps.service" ]; 257 before = [ "ncps.service" ]; 258 }; 259 260 systemd.services.ncps = { 261 description = "ncps binary cache proxy service"; 262 263 after = [ "network-online.target" ]; 264 wants = [ "network-online.target" ]; 265 wantedBy = [ "multi-user.target" ]; 266 267 preStart = '' 268 ${lib.getExe cfg.dbmatePackage} --migrations-dir=${cfg.package}/share/ncps/db/migrations --url=${cfg.cache.databaseURL} up 269 ''; 270 271 serviceConfig = lib.mkMerge [ 272 { 273 ExecStart = "${lib.getExe cfg.package} ${globalFlags} serve ${serveFlags}"; 274 User = "ncps"; 275 Group = "ncps"; 276 Restart = "on-failure"; 277 RuntimeDirectory = "ncps"; 278 } 279 280 # credentials 281 (lib.mkIf (cfg.cache.secretKeyPath != null) { 282 LoadCredential = "secretKey:${cfg.cache.secretKeyPath}"; 283 }) 284 285 # ensure permissions on required directories 286 (lib.mkIf (cfg.cache.dataPath != "/var/lib/ncps") { 287 ReadWritePaths = [ cfg.cache.dataPath ]; 288 }) 289 (lib.mkIf (cfg.cache.dataPath == "/var/lib/ncps") { 290 StateDirectory = "ncps"; 291 StateDirectoryMode = "0700"; 292 }) 293 (lib.mkIf (isSqlite && !lib.strings.hasPrefix "/var/lib/ncps" dbDir) { 294 ReadWritePaths = [ dbDir ]; 295 }) 296 (lib.mkIf (cfg.cache.tempPath != "/tmp") { 297 ReadWritePaths = [ cfg.cache.tempPath ]; 298 }) 299 300 # Hardening 301 { 302 SystemCallFilter = [ 303 "@system-service" 304 "~@privileged" 305 "~@resources" 306 ]; 307 CapabilityBoundingSet = ""; 308 PrivateUsers = true; 309 DevicePolicy = "closed"; 310 DeviceAllow = [ "" ]; 311 ProtectKernelModules = true; 312 ProtectKernelTunables = true; 313 ProtectControlGroups = true; 314 ProtectKernelLogs = true; 315 ProtectHostname = true; 316 ProtectClock = true; 317 ProtectProc = "invisible"; 318 ProtectSystem = "strict"; 319 ProtectHome = true; 320 RestrictSUIDSGID = true; 321 RestrictRealtime = true; 322 MemoryDenyWriteExecute = true; 323 ProcSubset = "pid"; 324 RestrictNamespaces = true; 325 SystemCallArchitectures = "native"; 326 PrivateNetwork = false; 327 PrivateTmp = true; 328 PrivateDevices = true; 329 PrivateMounts = true; 330 NoNewPrivileges = true; 331 LockPersonality = true; 332 RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6"; 333 LimitNOFILE = 65536; 334 UMask = "0066"; 335 } 336 ]; 337 338 unitConfig.RequiresMountsFor = lib.concatStringsSep " " ( 339 [ "${cfg.cache.dataPath}" ] ++ lib.optional (isSqlite) dbDir 340 ); 341 }; 342 }; 343 344 meta.maintainers = with lib.maintainers; [ kalbasit ]; 345}