at 25.11-pre 8.8 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 cfg = config.services.stargazer; 10 globalSection = '' 11 listen = ${lib.concatStringsSep " " cfg.listen} 12 connection-logging = ${lib.boolToString cfg.connectionLogging} 13 log-ip = ${lib.boolToString cfg.ipLog} 14 log-ip-partial = ${lib.boolToString cfg.ipLogPartial} 15 request-timeout = ${toString cfg.requestTimeout} 16 response-timeout = ${toString cfg.responseTimeout} 17 18 [:tls] 19 store = ${toString cfg.store} 20 organization = ${cfg.certOrg} 21 gen-certs = ${lib.boolToString cfg.genCerts} 22 regen-certs = ${lib.boolToString cfg.regenCerts} 23 ${lib.optionalString (cfg.certLifetime != "") "cert-lifetime = ${cfg.certLifetime}"} 24 25 ''; 26 genINI = lib.generators.toINI { }; 27 configFile = pkgs.writeText "config.ini" ( 28 lib.strings.concatStrings ( 29 [ globalSection ] 30 ++ (lib.lists.forEach cfg.routes ( 31 section: 32 let 33 name = section.route; 34 params = builtins.removeAttrs section [ "route" ]; 35 in 36 genINI { 37 "${name}" = params; 38 } 39 + "\n" 40 )) 41 ) 42 ); 43in 44{ 45 options.services.stargazer = { 46 enable = lib.mkEnableOption "Stargazer Gemini server"; 47 48 listen = lib.mkOption { 49 type = lib.types.listOf lib.types.str; 50 default = [ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]"; 51 defaultText = lib.literalExpression ''[ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]"''; 52 example = lib.literalExpression ''[ "10.0.0.12" "[2002:a00:1::]" ]''; 53 description = '' 54 Address and port to listen on. 55 ''; 56 }; 57 58 connectionLogging = lib.mkOption { 59 type = lib.types.bool; 60 default = true; 61 description = "Whether or not to log connections to stdout."; 62 }; 63 64 ipLog = lib.mkOption { 65 type = lib.types.bool; 66 default = false; 67 description = "Log client IP addresses in the connection log."; 68 }; 69 70 ipLogPartial = lib.mkOption { 71 type = lib.types.bool; 72 default = false; 73 description = "Log partial client IP addresses in the connection log."; 74 }; 75 76 requestTimeout = lib.mkOption { 77 type = lib.types.int; 78 default = 5; 79 description = '' 80 Number of seconds to wait for the client to send a complete 81 request. Set to 0 to disable. 82 ''; 83 }; 84 85 responseTimeout = lib.mkOption { 86 type = lib.types.int; 87 default = 0; 88 description = '' 89 Number of seconds to wait for the client to send a complete 90 request and for stargazer to finish sending the response. 91 Set to 0 to disable. 92 ''; 93 }; 94 95 allowCgiUser = lib.mkOption { 96 type = lib.types.bool; 97 default = false; 98 description = '' 99 When enabled, the stargazer process will be given `CAP_SETGID` 100 and `CAP_SETUID` so that it can run cgi processes as a different 101 user. This is required if the `cgi-user` option is used for a route. 102 Note that these capabilities could allow privilege escalation so be 103 careful. For that reason, this is disabled by default. 104 105 You will need to create the user mentioned `cgi-user` if it does not 106 already exist. 107 ''; 108 }; 109 110 store = lib.mkOption { 111 type = lib.types.path; 112 default = /var/lib/gemini/certs; 113 description = '' 114 Path to the certificate store on disk. This should be a 115 persistent directory writable by Stargazer. 116 ''; 117 }; 118 119 certOrg = lib.mkOption { 120 type = lib.types.str; 121 default = "stargazer"; 122 description = '' 123 The name of the organization responsible for the X.509 124 certificate's /O name. 125 ''; 126 }; 127 128 genCerts = lib.mkOption { 129 type = lib.types.bool; 130 default = true; 131 description = '' 132 Set to false to disable automatic certificate generation. 133 Use if you want to provide your own certs. 134 ''; 135 }; 136 137 regenCerts = lib.mkOption { 138 type = lib.types.bool; 139 default = true; 140 description = '' 141 Set to false to turn off automatic regeneration of expired certificates. 142 Use if you want to provide your own certs. 143 ''; 144 }; 145 146 certLifetime = lib.mkOption { 147 type = lib.types.str; 148 default = ""; 149 description = '' 150 How long certs generated by Stargazer should live for. 151 Certs live forever by default. 152 ''; 153 example = lib.literalExpression "\"1y\""; 154 }; 155 156 debugMode = lib.mkOption { 157 type = lib.types.bool; 158 default = false; 159 description = "Run Stargazer in debug mode."; 160 }; 161 162 routes = lib.mkOption { 163 type = lib.types.listOf ( 164 lib.types.submodule { 165 freeformType = 166 with lib.types; 167 attrsOf ( 168 nullOr (oneOf [ 169 bool 170 int 171 float 172 str 173 ]) 174 // { 175 description = "INI atom (null, bool, int, float or string)"; 176 } 177 ); 178 options.route = lib.mkOption { 179 type = lib.types.str; 180 description = "Route section name"; 181 }; 182 } 183 ); 184 default = [ ]; 185 description = '' 186 Routes that Stargazer should server. 187 188 Expressed as a list of attribute sets. Each set must have a key `route` 189 that becomes the section name for that route in the stargazer ini cofig. 190 The remaining keys and values become the parameters for that route. 191 192 [Refer to upstream docs for other params](https://git.sr.ht/~zethra/stargazer/tree/main/item/doc/stargazer.ini.5.txt) 193 ''; 194 example = lib.literalExpression '' 195 [ 196 { 197 route = "example.com"; 198 root = "/srv/gemini/example.com" 199 } 200 { 201 route = "example.com:/man"; 202 root = "/cgi-bin"; 203 cgi = true; 204 } 205 { 206 route = "other.org~(.*)"; 207 redirect = "gemini://example.com"; 208 rewrite = "\1"; 209 } 210 ] 211 ''; 212 }; 213 214 user = lib.mkOption { 215 type = lib.types.str; 216 default = "stargazer"; 217 description = "User account under which stargazer runs."; 218 }; 219 220 group = lib.mkOption { 221 type = lib.types.str; 222 default = "stargazer"; 223 description = "Group account under which stargazer runs."; 224 }; 225 }; 226 227 config = lib.mkIf cfg.enable { 228 systemd.services.stargazer = { 229 description = "Stargazer gemini server"; 230 after = [ "network.target" ]; 231 wantedBy = [ "multi-user.target" ]; 232 serviceConfig = { 233 ExecStart = "${pkgs.stargazer}/bin/stargazer ${configFile} ${lib.optionalString cfg.debugMode "-D"}"; 234 Restart = "always"; 235 # User and group 236 User = cfg.user; 237 Group = cfg.group; 238 AmbientCapabilities = lib.mkIf cfg.allowCgiUser [ 239 "CAP_SETGID" 240 "CAP_SETUID" 241 ]; 242 243 # Hardening 244 UMask = "0077"; 245 PrivateTmp = true; 246 ProtectHome = true; 247 ProtectSystem = "full"; 248 ProtectClock = true; 249 ProtectHostname = true; 250 ProtectControlGroups = true; 251 ProtectKernelLogs = true; 252 ProtectKernelModules = true; 253 ProtectKernelTunables = true; 254 ProtectProc = "invisible"; 255 PrivateDevices = true; 256 NoNewPrivileges = true; 257 RestrictSUIDSGID = true; 258 PrivateMounts = true; 259 MemoryDenyWriteExecute = true; 260 LockPersonality = true; 261 RestrictRealtime = true; 262 RemoveIPC = true; 263 CapabilityBoundingSet = 264 [ 265 "~CAP_SYS_PTRACE" 266 "~CAP_SYS_ADMIN" 267 "~CAP_SETPCAP" 268 "~CAP_SYS_TIME" 269 "~CAP_SYS_PACCT" 270 "~CAP_SYS_TTY_CONFIG " 271 "~CAP_SYS_CHROOT" 272 "~CAP_SYS_BOOT" 273 "~CAP_NET_ADMIN" 274 ] 275 ++ lib.lists.optional (!cfg.allowCgiUser) [ 276 "~CAP_SETGID" 277 "~CAP_SETUID" 278 ]; 279 SystemCallArchitectures = "native"; 280 SystemCallFilter = [ 281 "~@cpu-emulation @debug @keyring @mount @obsolete" 282 ] ++ lib.lists.optional (!cfg.allowCgiUser) [ "@privileged @setuid" ]; 283 }; 284 }; 285 286 # Create default cert store 287 systemd.tmpfiles.rules = lib.mkIf (cfg.store == /var/lib/gemini/certs) [ 288 ''d /var/lib/gemini/certs - "${cfg.user}" "${cfg.group}" -'' 289 ]; 290 291 users.users = lib.optionalAttrs (cfg.user == "stargazer") { 292 stargazer = { 293 group = cfg.group; 294 isSystemUser = true; 295 }; 296 }; 297 298 users.groups = lib.optionalAttrs (cfg.group == "stargazer") { 299 stargazer = { }; 300 }; 301 }; 302 303 meta.maintainers = with lib.maintainers; [ gaykitty ]; 304}