lol
1{ config, lib, pkgs, ... }:
2
3let
4 inherit (lib) mkOption types mdDoc literalExpression;
5
6 cfg = config.services.hedgedoc;
7
8 # 21.03 will not be an official release - it was instead 21.05. This
9 # versionAtLeast statement remains set to 21.03 for backwards compatibility.
10 # See https://github.com/NixOS/nixpkgs/pull/108899 and
11 # https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md.
12 name = if lib.versionAtLeast config.system.stateVersion "21.03" then
13 "hedgedoc"
14 else
15 "codimd";
16
17 settingsFormat = pkgs.formats.json { };
18in
19{
20 meta.maintainers = with lib.maintainers; [ SuperSandro2000 h7x4 ];
21
22 imports = [
23 (lib.mkRenamedOptionModule [ "services" "codimd" ] [ "services" "hedgedoc" ])
24 (lib.mkRenamedOptionModule [ "services" "hedgedoc" "configuration" ] [ "services" "hedgedoc" "settings" ])
25 (lib.mkRenamedOptionModule [ "services" "hedgedoc" "groups" ] [ "users" "users" "hedgedoc" "extraGroups" ])
26 (lib.mkRemovedOptionModule [ "services" "hedgedoc" "workDir" ] ''
27 This option has been removed in favor of systemd managing the state directory.
28
29 If you have set this option without specifying `services.settings.uploadsDir`,
30 please move these files to `/var/lib/hedgedoc/uploads`, or set the option to point
31 at the correct location.
32 '')
33 ];
34
35 options.services.hedgedoc = {
36 package = lib.mkPackageOptionMD pkgs "hedgedoc" { };
37 enable = lib.mkEnableOption (mdDoc "the HedgeDoc Markdown Editor");
38
39 settings = mkOption {
40 type = types.submodule {
41 freeformType = settingsFormat.type;
42 options = {
43 domain = mkOption {
44 type = with types; nullOr str;
45 default = null;
46 example = "hedgedoc.org";
47 description = mdDoc ''
48 Domain to use for website.
49
50 This is useful if you are trying to run hedgedoc behind
51 a reverse proxy.
52 '';
53 };
54 urlPath = mkOption {
55 type = with types; nullOr str;
56 default = null;
57 example = "hedgedoc";
58 description = mdDoc ''
59 URL path for the website.
60
61 This is useful if you are hosting hedgedoc on a path like
62 `www.example.com/hedgedoc`
63 '';
64 };
65 host = mkOption {
66 type = with types; nullOr str;
67 default = "localhost";
68 description = mdDoc ''
69 Address to listen on.
70 '';
71 };
72 port = mkOption {
73 type = types.port;
74 default = 3000;
75 example = 80;
76 description = mdDoc ''
77 Port to listen on.
78 '';
79 };
80 path = mkOption {
81 type = with types; nullOr path;
82 default = null;
83 example = "/run/hedgedoc/hedgedoc.sock";
84 description = mdDoc ''
85 Path to UNIX domain socket to listen on
86
87 ::: {.note}
88 If specified, {option}`host` and {option}`port` will be ignored.
89 :::
90 '';
91 };
92 protocolUseSSL = mkOption {
93 type = types.bool;
94 default = false;
95 example = true;
96 description = mdDoc ''
97 Use `https://` for all links.
98
99 This is useful if you are trying to run hedgedoc behind
100 a reverse proxy.
101
102 ::: {.note}
103 Only applied if {option}`domain` is set.
104 :::
105 '';
106 };
107 allowOrigin = mkOption {
108 type = with types; listOf str;
109 default = with cfg.settings; [ host ] ++ lib.optionals (domain != null) [ domain ];
110 defaultText = literalExpression ''
111 with config.services.hedgedoc.settings; [ host ] ++ lib.optionals (domain != null) [ domain ]
112 '';
113 example = [ "localhost" "hedgedoc.org" ];
114 description = mdDoc ''
115 List of domains to whitelist.
116 '';
117 };
118 db = mkOption {
119 type = types.attrs;
120 default = {
121 dialect = "sqlite";
122 storage = "/var/lib/${name}/db.sqlite";
123 };
124 defaultText = literalExpression ''
125 {
126 dialect = "sqlite";
127 storage = "/var/lib/hedgedoc/db.sqlite";
128 }
129 '';
130 example = literalExpression ''
131 db = {
132 username = "hedgedoc";
133 database = "hedgedoc";
134 host = "localhost:5432";
135 # or via socket
136 # host = "/run/postgresql";
137 dialect = "postgresql";
138 };
139 '';
140 description = mdDoc ''
141 Specify the configuration for sequelize.
142 HedgeDoc supports `mysql`, `postgres`, `sqlite` and `mssql`.
143 See <https://sequelize.readthedocs.io/en/v3/>
144 for more information.
145
146 ::: {.note}
147 The relevant parts will be overriden if you set {option}`dbURL`.
148 :::
149 '';
150 };
151 useSSL = mkOption {
152 type = types.bool;
153 default = false;
154 description = mdDoc ''
155 Enable to use SSL server.
156
157 ::: {.note}
158 This will also enable {option}`protocolUseSSL`.
159
160 It will also require you to set the following:
161
162 - {option}`sslKeyPath`
163 - {option}`sslCertPath`
164 - {option}`sslCAPath`
165 - {option}`dhParamPath`
166 :::
167 '';
168 };
169 uploadsPath = mkOption {
170 type = types.path;
171 default = "/var/lib/${name}/uploads";
172 defaultText = "/var/lib/hedgedoc/uploads";
173 description = mdDoc ''
174 Directory for storing uploaded images.
175 '';
176 };
177
178 # Declared because we change the default to false.
179 allowGravatar = mkOption {
180 type = types.bool;
181 default = false;
182 example = true;
183 description = mdDoc ''
184 Whether to enable [Libravatar](https://wiki.libravatar.org/) as
185 profile picture source on your instance.
186
187 Despite the naming of the setting, Hedgedoc replaced Gravatar
188 with Libravatar in [CodiMD 1.4.0](https://hedgedoc.org/releases/1.4.0/)
189 '';
190 };
191 };
192 };
193
194 description = mdDoc ''
195 HedgeDoc configuration, see
196 <https://docs.hedgedoc.org/configuration/>
197 for documentation.
198 '';
199 };
200
201 environmentFile = mkOption {
202 type = with types; nullOr path;
203 default = null;
204 example = "/var/lib/hedgedoc/hedgedoc.env";
205 description = mdDoc ''
206 Environment file as defined in {manpage}`systemd.exec(5)`.
207
208 Secrets may be passed to the service without adding them to the world-readable
209 Nix store, by specifying placeholder variables as the option value in Nix and
210 setting these variables accordingly in the environment file.
211
212 ```
213 # snippet of HedgeDoc-related config
214 services.hedgedoc.settings.dbURL = "postgres://hedgedoc:\''${DB_PASSWORD}@db-host:5432/hedgedocdb";
215 services.hedgedoc.settings.minio.secretKey = "$MINIO_SECRET_KEY";
216 ```
217
218 ```
219 # content of the environment file
220 DB_PASSWORD=verysecretdbpassword
221 MINIO_SECRET_KEY=verysecretminiokey
222 ```
223
224 Note that this file needs to be available on the host on which
225 `HedgeDoc` is running.
226 '';
227 };
228 };
229
230 config = lib.mkIf cfg.enable {
231 users.groups.${name} = { };
232 users.users.${name} = {
233 description = "HedgeDoc service user";
234 group = name;
235 isSystemUser = true;
236 };
237
238 services.hedgedoc.settings = {
239 defaultNotePath = lib.mkDefault "${cfg.package}/public/default.md";
240 docsPath = lib.mkDefault "${cfg.package}/public/docs";
241 viewPath = lib.mkDefault "${cfg.package}/public/views";
242 };
243
244 systemd.services.hedgedoc = {
245 description = "HedgeDoc Service";
246 documentation = [ "https://docs.hedgedoc.org/" ];
247 wantedBy = [ "multi-user.target" ];
248 after = [ "networking.target" ];
249 preStart =
250 let
251 configFile = settingsFormat.generate "hedgedoc-config.json" {
252 production = cfg.settings;
253 };
254 in
255 ''
256 ${pkgs.envsubst}/bin/envsubst \
257 -o /run/${name}/config.json \
258 -i ${configFile}
259 ${pkgs.coreutils}/bin/mkdir -p ${cfg.settings.uploadsPath}
260 '';
261 serviceConfig = {
262 User = name;
263 Group = name;
264
265 Restart = "always";
266 ExecStart = "${cfg.package}/bin/hedgedoc";
267 RuntimeDirectory = [ name ];
268 StateDirectory = [ name ];
269 WorkingDirectory = "/run/${name}";
270 ReadWritePaths = [
271 "-${cfg.settings.uploadsPath}"
272 ] ++ lib.optionals (cfg.settings.db ? "storage") [ "-${cfg.settings.db.storage}" ];
273 EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
274 Environment = [
275 "CMD_CONFIG_FILE=/run/${name}/config.json"
276 "NODE_ENV=production"
277 ];
278
279 # Hardening
280 AmbientCapabilities = "";
281 CapabilityBoundingSet = "";
282 LockPersonality = true;
283 NoNewPrivileges = true;
284 PrivateDevices = true;
285 PrivateMounts = true;
286 PrivateTmp = true;
287 PrivateUsers = true;
288 ProcSubset = "pid";
289 ProtectClock = true;
290 ProtectControlGroups = true;
291 ProtectHome = true;
292 ProtectHostname = true;
293 ProtectKernelLogs = true;
294 ProtectKernelModules = true;
295 ProtectKernelTunables = true;
296 ProtectProc = "invisible";
297 ProtectSystem = "strict";
298 RemoveIPC = true;
299 RestrictAddressFamilies = [
300 "AF_INET"
301 "AF_INET6"
302 # Required for connecting to database sockets,
303 # and listening to unix socket at `cfg.settings.path`
304 "AF_UNIX"
305 ];
306 RestrictNamespaces = true;
307 RestrictRealtime = true;
308 RestrictSUIDSGID = true;
309 SocketBindAllow = lib.mkIf (cfg.settings.path == null) cfg.settings.port;
310 SocketBindDeny = "any";
311 SystemCallArchitectures = "native";
312 SystemCallFilter = [
313 "@system-service"
314 "~@privileged @obsolete"
315 "@pkey"
316 ];
317 UMask = "0007";
318 };
319 };
320 };
321}