1 { config, lib, pkgs, ... }:
4 cfg = config.services.stargazer;
6 listen = ${lib.concatStringsSep " " cfg.listen}
7 connection-logging = ${lib.boolToString cfg.connectionLogging}
8 log-ip = ${lib.boolToString cfg.ipLog}
9 log-ip-partial = ${lib.boolToString cfg.ipLogPartial}
10 request-timeout = ${toString cfg.requestTimeout}
11 response-timeout = ${toString cfg.responseTimeout}
14 store = ${toString cfg.store}
15 organization = ${cfg.certOrg}
16 gen-certs = ${lib.boolToString cfg.genCerts}
17 regen-certs = ${lib.boolToString cfg.regenCerts}
18 ${lib.optionalString (cfg.certLifetime != "") "cert-lifetime = ${cfg.certLifetime}"}
21 genINI = lib.generators.toINI { };
22 configFile = pkgs.writeText "config.ini" (lib.strings.concatStrings (
23 [ globalSection ] ++ (lib.lists.forEach cfg.routes (section:
26 params = builtins.removeAttrs section [ "route" ];
36 options.services.stargazer = {
37 enable = lib.mkEnableOption "Stargazer Gemini server";
39 listen = lib.mkOption {
40 type = lib.types.listOf lib.types.str;
41 default = [ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]";
42 defaultText = lib.literalExpression ''[ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]"'';
43 example = lib.literalExpression ''[ "10.0.0.12" "[2002:a00:1::]" ]'';
45 Address and port to listen on.
49 connectionLogging = lib.mkOption {
50 type = lib.types.bool;
52 description = "Whether or not to log connections to stdout.";
55 ipLog = lib.mkOption {
56 type = lib.types.bool;
58 description = "Log client IP addresses in the connection log.";
61 ipLogPartial = lib.mkOption {
62 type = lib.types.bool;
64 description = "Log partial client IP addresses in the connection log.";
67 requestTimeout = lib.mkOption {
71 Number of seconds to wait for the client to send a complete
72 request. Set to 0 to disable.
76 responseTimeout = lib.mkOption {
80 Number of seconds to wait for the client to send a complete
81 request and for stargazer to finish sending the response.
86 allowCgiUser = lib.mkOption {
87 type = lib.types.bool;
90 When enabled, the stargazer process will be given `CAP_SETGID`
91 and `CAP_SETUID` so that it can run cgi processes as a different
92 user. This is required if the `cgi-user` option is used for a route.
93 Note that these capabilities could allow privilege escalation so be
94 careful. For that reason, this is disabled by default.
96 You will need to create the user mentioned `cgi-user` if it does not
101 store = lib.mkOption {
102 type = lib.types.path;
103 default = /var/lib/gemini/certs;
105 Path to the certificate store on disk. This should be a
106 persistent directory writable by Stargazer.
110 certOrg = lib.mkOption {
111 type = lib.types.str;
112 default = "stargazer";
114 The name of the organization responsible for the X.509
115 certificate's /O name.
119 genCerts = lib.mkOption {
120 type = lib.types.bool;
123 Set to false to disable automatic certificate generation.
124 Use if you want to provide your own certs.
128 regenCerts = lib.mkOption {
129 type = lib.types.bool;
132 Set to false to turn off automatic regeneration of expired certificates.
133 Use if you want to provide your own certs.
137 certLifetime = lib.mkOption {
138 type = lib.types.str;
141 How long certs generated by Stargazer should live for.
142 Certs live forever by default.
144 example = lib.literalExpression "\"1y\"";
147 debugMode = lib.mkOption {
148 type = lib.types.bool;
150 description = "Run Stargazer in debug mode.";
153 routes = lib.mkOption {
154 type = lib.types.listOf
155 (lib.types.submodule {
156 freeformType = with lib.types; attrsOf (nullOr
163 description = "INI atom (null, bool, int, float or string)";
165 options.route = lib.mkOption {
166 type = lib.types.str;
167 description = "Route section name";
172 Routes that Stargazer should server.
174 Expressed as a list of attribute sets. Each set must have a key `route`
175 that becomes the section name for that route in the stargazer ini cofig.
176 The remaining keys and values become the parameters for that route.
178 [Refer to upstream docs for other params](https://git.sr.ht/~zethra/stargazer/tree/main/item/doc/stargazer.ini.5.txt)
180 example = lib.literalExpression ''
183 route = "example.com";
184 root = "/srv/gemini/example.com"
187 route = "example.com:/man";
192 route = "other.org~(.*)";
193 redirect = "gemini://example.com";
200 user = lib.mkOption {
201 type = lib.types.str;
202 default = "stargazer";
203 description = "User account under which stargazer runs.";
206 group = lib.mkOption {
207 type = lib.types.str;
208 default = "stargazer";
209 description = "Group account under which stargazer runs.";
213 config = lib.mkIf cfg.enable {
214 systemd.services.stargazer = {
215 description = "Stargazer gemini server";
216 after = [ "network.target" ];
217 wantedBy = [ "multi-user.target" ];
219 ExecStart = "${pkgs.stargazer}/bin/stargazer ${configFile} ${lib.optionalString cfg.debugMode "-D"}";
224 AmbientCapabilities = lib.mkIf cfg.allowCgiUser [
233 ProtectSystem = "full";
235 ProtectHostname = true;
236 ProtectControlGroups = true;
237 ProtectKernelLogs = true;
238 ProtectKernelModules = true;
239 ProtectKernelTunables = true;
240 ProtectProc = "invisible";
241 PrivateDevices = true;
242 NoNewPrivileges = true;
243 RestrictSUIDSGID = true;
244 PrivateMounts = true;
245 MemoryDenyWriteExecute = true;
246 LockPersonality = true;
247 RestrictRealtime = true;
249 CapabilityBoundingSet = [
255 "~CAP_SYS_TTY_CONFIG "
259 ] ++ lib.lists.optional (!cfg.allowCgiUser) [
263 SystemCallArchitectures = "native";
264 SystemCallFilter = [ "~@cpu-emulation @debug @keyring @mount @obsolete" ]
265 ++ lib.lists.optional (!cfg.allowCgiUser) [ "@privileged @setuid" ];
269 # Create default cert store
270 systemd.tmpfiles.rules = lib.mkIf (cfg.store == /var/lib/gemini/certs) [
271 ''d /var/lib/gemini/certs - "${cfg.user}" "${cfg.group}" -''
274 users.users = lib.optionalAttrs (cfg.user == "stargazer") {
281 users.groups = lib.optionalAttrs (cfg.group == "stargazer") {
286 meta.maintainers = with lib.maintainers; [ gaykitty ];