at 25.11-pre 14 kB view raw
1{ 2 lib, 3 config, 4 pkgs, 5 ... 6}: 7let 8 cfg = config.services.reposilite; 9 format = pkgs.formats.cdn { }; 10 configFile = format.generate "reposilite.cdn" cfg.settings; 11 12 useEmbeddedDb = cfg.database.type == "sqlite" || cfg.database.type == "h2"; 13 useMySQL = cfg.database.type == "mariadb" || cfg.database.type == "mysql"; 14 usePostgres = cfg.database.type == "postgresql"; 15 16 # db password is appended at runtime by the service script (if needed) 17 dbString = 18 if useEmbeddedDb then 19 "${cfg.database.type} ${cfg.database.path}" 20 else 21 "${cfg.database.type} ${cfg.database.host}:${builtins.toString cfg.database.port} ${cfg.database.dbname} ${cfg.database.user} $(<${cfg.database.passwordFile})"; 22 23 certDir = config.security.acme.certs.${cfg.useACMEHost}.directory; 24 25 databaseModule = { 26 options = { 27 type = lib.mkOption { 28 type = lib.types.enum [ 29 "h2" 30 "mariadb" 31 "mysql" 32 "postgresql" 33 "sqlite" 34 ]; 35 description = '' 36 Database engine to use. 37 ''; 38 default = "sqlite"; 39 }; 40 41 path = lib.mkOption { 42 type = lib.types.str; 43 description = '' 44 Path to the embedded database file. Set to `--temporary` to use an in-memory database. 45 ''; 46 default = "reposilite.db"; 47 }; 48 49 host = lib.mkOption { 50 type = lib.types.str; 51 description = '' 52 Database host address. 53 ''; 54 default = "127.0.0.1"; 55 }; 56 57 port = lib.mkOption { 58 type = lib.types.port; 59 description = '' 60 Database TCP port. 61 ''; 62 defaultText = lib.literalExpression '' 63 if type == "postgresql" then 5432 else 3306 64 ''; 65 default = if usePostgres then config.services.postgresql.settings.port else 3306; 66 }; 67 68 dbname = lib.mkOption { 69 type = lib.types.str; 70 description = '' 71 Database name. 72 ''; 73 default = "reposilite"; 74 }; 75 76 user = lib.mkOption { 77 type = lib.types.str; 78 description = '' 79 Database user. 80 ''; 81 default = "reposilite"; 82 }; 83 84 passwordFile = lib.mkOption { 85 type = lib.types.nullOr lib.types.path; 86 description = '' 87 Path to the file containing the password for the database connection. 88 This file must be readable by {option}`services.reposilite.user`. 89 ''; 90 default = null; 91 }; 92 }; 93 }; 94 95 settingsModule = { 96 freeformType = format.type; 97 options = { 98 hostname = lib.mkOption { 99 type = lib.types.str; 100 description = '' 101 The hostname to bind to. Set to `0.0.0.0` to accept connections from everywhere, or `127.0.0.1` to restrict to localhost." 102 ''; 103 default = "0.0.0.0"; 104 example = "127.0.0.1"; 105 }; 106 107 port = lib.mkOption { 108 type = lib.types.port; 109 description = '' 110 The TCP port to bind to. 111 ''; 112 default = 3000; 113 }; 114 115 database = lib.mkOption { 116 type = lib.types.nullOr lib.types.str; 117 description = '' 118 Database connection string. Please use {option}`services.reposilite.database` instead. 119 See https://reposilite.com/guide/general#local-configuration for valid values. 120 ''; 121 default = null; 122 }; 123 124 sslEnabled = lib.mkOption { 125 type = lib.types.bool; 126 description = '' 127 Whether to listen for encrypted connections on {option}`settings.sslPort`. 128 ''; 129 default = false; 130 }; 131 132 sslPort = lib.mkOption { 133 type = lib.types.port; # cant be null 134 description = "SSL port to bind to. SSL needs to be enabled explicitly via {option}`settings.enableSsl`."; 135 default = 443; 136 }; 137 138 keyPath = lib.mkOption { 139 type = lib.types.nullOr lib.types.str; 140 description = '' 141 Path to the .jsk KeyStore or paths to the PKCS#8 certificate and private key, separated by a space (see example). 142 You can use `''${WORKING_DIRECTORY}` to refer to paths relative to Reposilite's working directory. 143 If you are using a Java KeyStore, don't forget to specify the password via the {var}`REPOSILITE_LOCAL_KEYPASSWORD` environment variable. 144 See https://reposilite.com/guide/ssl for more information on how to set SSL up. 145 ''; 146 default = null; 147 example = "\${WORKING_DIRECTORY}/cert.pem \${WORKING_DIRECTORY}/key.pem"; 148 }; 149 150 keyPassword = lib.mkOption { 151 type = lib.types.nullOr lib.types.str; 152 description = '' 153 Plaintext password used to unlock the Java KeyStore set in {option}`services.reposilite.settings.keyPath`. 154 WARNING: this option is insecure and should not be used to store the password. 155 Consider using {option}`services.reposilite.keyPasswordFile` instead. 156 ''; 157 default = null; 158 }; 159 160 enforceSsl = lib.mkOption { 161 type = lib.types.bool; 162 description = '' 163 Whether to redirect all traffic to SSL. 164 ''; 165 default = false; 166 }; 167 168 webThreadPool = lib.mkOption { 169 type = lib.types.ints.between 5 65535; 170 description = '' 171 Maximum amount of threads used by the core thread pool. (min: 5) 172 The web thread pool handles the first few steps of incoming HTTP connections, tasks are redirected as soon as possible to the IO thread pool. 173 ''; 174 default = 16; 175 }; 176 177 ioThreadPool = lib.mkOption { 178 type = lib.types.ints.between 2 65535; 179 description = '' 180 The IO thread pool handles all tasks that may benefit from non-blocking IO. (min: 2) 181 Because most tasks are redirected to IO thread pool, it might be a good idea to keep it at least equal to web thread pool. 182 ''; 183 default = 8; 184 }; 185 186 databaseThreadPool = lib.mkOption { 187 type = lib.types.ints.positive; 188 description = '' 189 Maximum amount of concurrent connections to the database. (one per thread) 190 Embedded databases (sqlite, h2) do not support truly concurrent connections, so the value will always be `1` if they are used. 191 ''; 192 default = 1; 193 }; 194 195 compressionStrategy = lib.mkOption { 196 type = lib.types.enum [ 197 "none" 198 "gzip" 199 ]; 200 description = '' 201 Compression algorithm used by this instance of Reposilite. 202 `none` reduces usage of CPU & memory, but requires transfering more data. 203 ''; 204 default = "none"; 205 }; 206 207 idleTimeout = lib.mkOption { 208 type = lib.types.ints.unsigned; 209 description = '' 210 Default idle timeout used by Jetty. 211 ''; 212 default = 30000; 213 }; 214 215 bypassExternalCache = lib.mkOption { 216 type = lib.types.bool; 217 description = '' 218 Add cache bypass headers to responses from /api/* to avoid issues with proxies such as Cloudflare. 219 ''; 220 default = true; 221 }; 222 223 cachedLogSize = lib.mkOption { 224 type = lib.types.ints.unsigned; 225 description = '' 226 Amount of messages stored in the cache logger. 227 ''; 228 default = 50; 229 }; 230 231 defaultFrontend = lib.mkOption { 232 type = lib.types.bool; 233 description = '' 234 Whether to enable the default included frontend with a dashboard. 235 ''; 236 default = true; 237 }; 238 239 basePath = lib.mkOption { 240 type = lib.types.str; 241 description = '' 242 Custom base path for this Reposilite instance. 243 It is not recommended changing this, you should instead prioritize using a different subdomain. 244 ''; 245 default = "/"; 246 }; 247 248 debugEnabled = lib.mkOption { 249 type = lib.types.bool; 250 description = '' 251 Whether to enable debug mode. 252 ''; 253 default = false; 254 }; 255 }; 256 }; 257in 258{ 259 options.services.reposilite = { 260 enable = lib.mkEnableOption "Reposilite"; 261 package = lib.mkPackageOption pkgs "reposilite" { } // { 262 apply = 263 pkg: 264 pkg.override (old: { 265 plugins = (old.plugins or [ ]) ++ cfg.plugins; 266 }); 267 }; 268 269 plugins = lib.mkOption { 270 type = lib.types.listOf lib.types.package; 271 description = '' 272 List of plugins to add to Reposilite. 273 ''; 274 default = [ ]; 275 example = "with reposilitePlugins; [ checksum groovy ]"; 276 }; 277 278 database = lib.mkOption { 279 description = "Database options."; 280 default = { }; 281 type = lib.types.submodule databaseModule; 282 }; 283 284 keyPasswordFile = lib.mkOption { 285 type = lib.types.nullOr lib.types.path; 286 description = '' 287 Path the the file containing the password used to unlock the Java KeyStore file specified in {option}`services.reposilite.settings.keyPath`. 288 This file must be readable my {option}`services.reposilite.user`. 289 ''; 290 default = null; 291 }; 292 293 useACMEHost = lib.mkOption { 294 type = lib.types.nullOr lib.types.str; 295 description = '' 296 Host of an existing Let's Encrypt certificate to use for SSL. 297 Make sure that the certificate directory is readable by the `reposilite` user or group, for example via {option}`security.acme.certs.<cert>.group`. 298 *Note that this option does not create any certificates, nor it does add subdomains to existing ones you will need to create them manually using {option}`security.acme.certs`* 299 ''; 300 default = null; 301 }; 302 303 settings = lib.mkOption { 304 description = "Configuration written to the reposilite.cdn file"; 305 default = { }; 306 type = lib.types.submodule settingsModule; 307 }; 308 309 workingDirectory = lib.mkOption { 310 type = lib.types.path; 311 description = '' 312 Working directory for Reposilite. 313 ''; 314 default = "/var/lib/reposilite"; 315 }; 316 317 extraArgs = lib.mkOption { 318 type = lib.types.listOf lib.types.str; 319 description = '' 320 Extra arguments/parameters passed to the Reposilite. Can be used for first token generation. 321 ''; 322 default = [ ]; 323 example = lib.literalExpression ''[ "--token" "name:tempsecrettoken" ]''; 324 }; 325 326 user = lib.mkOption { 327 type = lib.types.str; 328 description = '' 329 The user to run Reposilite under. 330 ''; 331 default = "reposilite"; 332 }; 333 334 group = lib.mkOption { 335 type = lib.types.str; 336 description = '' 337 The group to run Reposilite under. 338 ''; 339 default = "reposilite"; 340 }; 341 342 openFirewall = lib.mkOption { 343 type = lib.types.bool; 344 description = '' 345 Whether to open the firewall ports for Reposilite. If SSL is enabled, its port will be opened too. 346 ''; 347 default = false; 348 }; 349 }; 350 351 config = lib.mkIf cfg.enable { 352 assertions = [ 353 { 354 assertion = cfg.settings.sslEnabled -> cfg.settings.keyPath != null; 355 message = '' 356 Reposilite was configured to enable SSL, but no valid paths to certificate files were provided via `settings.keyPath`. 357 Read more about SSL certificates here: https://reposilite.com/guide/ssl 358 ''; 359 } 360 { 361 assertion = cfg.settings.enforceSsl -> cfg.settings.sslEnabled; 362 message = "You cannot enforce SSL if SSL is not enabled."; 363 } 364 { 365 assertion = !useEmbeddedDb -> cfg.database.passwordFile != null; 366 message = "You need to set `services.reposilite.database.passwordFile` when using MySQL or Postgres."; 367 } 368 ]; 369 370 services.reposilite.settings.keyPath = lib.mkIf ( 371 cfg.useACMEHost != null 372 ) "${certDir}/fullchain.pem ${certDir}/key.pem"; 373 374 environment.systemPackages = [ cfg.package ]; 375 376 users = { 377 groups.${cfg.group} = lib.mkIf (cfg.group == "reposilite") { }; 378 users.${cfg.user} = lib.mkIf (cfg.user == "reposilite") { 379 isSystemUser = true; 380 group = cfg.group; 381 }; 382 }; 383 384 networking.firewall = lib.mkIf cfg.openFirewall ( 385 lib.mkMerge [ 386 { 387 allowedTCPPorts = [ cfg.settings.port ]; 388 } 389 (lib.mkIf cfg.settings.sslEnabled { 390 allowedTCPPorts = [ cfg.settings.sslPort ]; 391 }) 392 ] 393 ); 394 395 systemd.services.reposilite = { 396 enable = true; 397 wantedBy = [ "multi-user.target" ]; 398 after = 399 [ "network.target" ] 400 ++ (lib.optional useMySQL "mysql.service") 401 ++ (lib.optional usePostgres "postgresql.service"); 402 403 script = 404 lib.optionalString (cfg.keyPasswordFile != null && cfg.settings.keyPassword == null) '' 405 export REPOSILITE_LOCAL_KEYPASSWORD="$(<${cfg.keyPasswordFile})" 406 '' 407 + '' 408 export REPOSILITE_LOCAL_DATABASE="${dbString}" 409 410 ${lib.getExe cfg.package} --local-configuration ${configFile} --local-configuration-mode none --working-directory ${cfg.workingDirectory} ${lib.escapeShellArgs cfg.extraArgs} 411 ''; 412 413 serviceConfig = lib.mkMerge [ 414 (lib.mkIf (builtins.dirOf cfg.workingDirectory == "/var/lib") { 415 StateDirectory = builtins.baseNameOf cfg.workingDirectory; 416 StateDirectoryMode = "700"; 417 }) 418 { 419 Type = "exec"; 420 Restart = "on-failure"; 421 422 User = cfg.user; 423 Group = cfg.group; 424 WorkingDirectory = cfg.workingDirectory; 425 426 # TODO better hardening 427 LimitNOFILE = "1048576"; 428 PrivateTmp = true; 429 PrivateDevices = true; 430 ProtectHome = true; 431 ProtectSystem = "strict"; 432 AmbientCapabilities = "CAP_NET_BIND_SERVICE"; 433 } 434 ]; 435 }; 436 }; 437 438 meta.maintainers = [ lib.maintainers.uku3lig ]; 439}