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