at 25.11-pre 10 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 ); 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; 55in 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 users.users.ncps = { 212 isSystemUser = true; 213 group = "ncps"; 214 }; 215 users.groups.ncps = { }; 216 217 systemd.services.ncps-create-datadirs = { 218 description = "Created required directories by ncps"; 219 serviceConfig = { 220 Type = "oneshot"; 221 UMask = "0066"; 222 }; 223 script = 224 (lib.optionalString (cfg.cache.dataPath != "/var/lib/ncps") '' 225 if ! test -d ${cfg.cache.dataPath}; then 226 mkdir -p ${cfg.cache.dataPath} 227 chown ncps:ncps ${cfg.cache.dataPath} 228 fi 229 '') 230 + (lib.optionalString isSqlite '' 231 if ! test -d ${dbDir}; then 232 mkdir -p ${dbDir} 233 chown ncps:ncps ${dbDir} 234 fi 235 ''); 236 wantedBy = [ "ncps.service" ]; 237 before = [ "ncps.service" ]; 238 }; 239 240 systemd.services.ncps = { 241 description = "ncps binary cache proxy service"; 242 243 after = [ "network-online.target" ]; 244 wants = [ "network-online.target" ]; 245 wantedBy = [ "multi-user.target" ]; 246 247 preStart = '' 248 ${lib.getExe cfg.dbmatePackage} --migrations-dir=${cfg.package}/share/ncps/db/migrations --url=${cfg.cache.databaseURL} up 249 ''; 250 251 serviceConfig = lib.mkMerge [ 252 { 253 ExecStart = "${lib.getExe cfg.package} ${globalFlags} serve ${serveFlags}"; 254 User = "ncps"; 255 Group = "ncps"; 256 Restart = "on-failure"; 257 RuntimeDirectory = "ncps"; 258 } 259 260 # credentials 261 (lib.mkIf (cfg.cache.secretKeyPath != null) { 262 LoadCredential = "secretKey:${cfg.cache.secretKeyPath}"; 263 }) 264 265 # ensure permissions on required directories 266 (lib.mkIf (cfg.cache.dataPath != "/var/lib/ncps") { 267 ReadWritePaths = [ cfg.cache.dataPath ]; 268 }) 269 (lib.mkIf (cfg.cache.dataPath == "/var/lib/ncps") { 270 StateDirectory = "ncps"; 271 StateDirectoryMode = "0700"; 272 }) 273 (lib.mkIf (isSqlite && !lib.strings.hasPrefix "/var/lib/ncps" dbDir) { 274 ReadWritePaths = [ dbDir ]; 275 }) 276 277 # Hardening 278 { 279 SystemCallFilter = [ 280 "@system-service" 281 "~@privileged" 282 "~@resources" 283 ]; 284 CapabilityBoundingSet = ""; 285 PrivateUsers = true; 286 DevicePolicy = "closed"; 287 DeviceAllow = [ "" ]; 288 ProtectKernelModules = true; 289 ProtectKernelTunables = true; 290 ProtectControlGroups = true; 291 ProtectKernelLogs = true; 292 ProtectHostname = true; 293 ProtectClock = true; 294 ProtectProc = "invisible"; 295 ProtectSystem = "strict"; 296 ProtectHome = true; 297 RestrictSUIDSGID = true; 298 RestrictRealtime = true; 299 MemoryDenyWriteExecute = true; 300 ProcSubset = "pid"; 301 RestrictNamespaces = true; 302 SystemCallArchitectures = "native"; 303 PrivateNetwork = false; 304 PrivateTmp = true; 305 PrivateDevices = true; 306 PrivateMounts = true; 307 NoNewPrivileges = true; 308 LockPersonality = true; 309 RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6"; 310 LimitNOFILE = 65536; 311 UMask = "0066"; 312 } 313 ]; 314 315 unitConfig.RequiresMountsFor = lib.concatStringsSep " " ( 316 [ "${cfg.cache.dataPath}" ] ++ lib.optional (isSqlite) dbDir 317 ); 318 }; 319 }; 320 321 meta.maintainers = with lib.maintainers; [ kalbasit ]; 322}