1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.gotenberg;
9
10 args =
11 [
12 "--api-port=${toString cfg.port}"
13 "--api-timeout=${cfg.timeout}"
14 "--api-root-path=${cfg.rootPath}"
15 "--log-level=${cfg.logLevel}"
16 "--chromium-max-queue-size=${toString cfg.chromium.maxQueueSize}"
17 "--libreoffice-restart-after=${toString cfg.libreoffice.restartAfter}"
18 "--libreoffice-max-queue-size=${toString cfg.libreoffice.maxQueueSize}"
19 "--pdfengines-merge-engines=${lib.concatStringsSep "," cfg.pdfEngines.merge}"
20 "--pdfengines-convert-engines=${lib.concatStringsSep "," cfg.pdfEngines.convert}"
21 "--pdfengines-read-metadata-engines=${lib.concatStringsSep "," cfg.pdfEngines.readMetadata}"
22 "--pdfengines-write-metadata-engines=${lib.concatStringsSep "," cfg.pdfEngines.writeMetadata}"
23 "--api-download-from-allow-list=${cfg.downloadFrom.allowList}"
24 "--api-download-from-max-retry=${toString cfg.downloadFrom.maxRetries}"
25 ]
26 ++ optional cfg.enableBasicAuth "--api-enable-basic-auth"
27 ++ optional cfg.chromium.autoStart "--chromium-auto-start"
28 ++ optional cfg.chromium.disableJavascript "--chromium-disable-javascript"
29 ++ optional cfg.chromium.disableRoutes "--chromium-disable-routes"
30 ++ optional cfg.libreoffice.autoStart "--libreoffice-auto-start"
31 ++ optional cfg.libreoffice.disableRoutes "--libreoffice-disable-routes"
32 ++ optional cfg.pdfEngines.disableRoutes "--pdfengines-disable-routes"
33 ++ optional (
34 cfg.downloadFrom.denyList != null
35 ) "--api-download-from-deny-list=${cfg.downloadFrom.denyList}"
36 ++ optional cfg.downloadFrom.disable "--api-disable-download-from"
37 ++ optional (cfg.bodyLimit != null) "--api-body-limit=${cfg.bodyLimit}"
38 ++ lib.optionals (cfg.extraArgs != [ ]) cfg.extraArgs;
39
40 inherit (lib)
41 mkEnableOption
42 mkPackageOption
43 mkOption
44 types
45 mkIf
46 optional
47 optionalAttrs
48 ;
49in
50{
51 options = {
52 services.gotenberg = {
53 enable = mkEnableOption "Gotenberg, a stateless API for PDF files";
54
55 # Users can override only gotenberg, libreoffice and chromium if they want to (eg. ungoogled-chromium, different LO version, etc)
56 # Don't allow setting the qpdf, pdftk, or unoconv paths, as those are very stable
57 # and there's only one version of each.
58 package = mkPackageOption pkgs "gotenberg" { };
59
60 port = mkOption {
61 type = types.port;
62 default = 3000;
63 description = "Port on which the API should listen.";
64 };
65
66 bindIP = mkOption {
67 type = types.nullOr types.str;
68 default = "127.0.0.1";
69 description = "Port the API listener should bind to. Set to 0.0.0.0 to listen on all available IPs.";
70 };
71
72 timeout = mkOption {
73 type = types.nullOr types.str;
74 default = "30s";
75 description = "Timeout for API requests.";
76 };
77
78 rootPath = mkOption {
79 type = types.str;
80 default = "/";
81 description = "Root path for the Gotenberg API.";
82 };
83
84 enableBasicAuth = mkOption {
85 type = types.bool;
86 default = false;
87 description = ''
88 HTTP Basic Authentication.
89
90 If you set this, be sure to set `GOTENBERG_API_BASIC_AUTH_USERNAME`and `GOTENBERG_API_BASIC_AUTH_PASSWORD`
91 in your `services.gotenberg.environmentFile` file.
92 '';
93 };
94
95 bodyLimit = mkOption {
96 type = types.nullOr types.str;
97 default = null;
98 description = "Sets the max limit for `multipart/form-data` requests. Accepts values like '5M', '20G', etc.";
99 };
100
101 extraFontPackages = mkOption {
102 type = types.listOf types.package;
103 default = [ ];
104 description = "Extra fonts to make available.";
105 };
106
107 chromium = {
108 package = mkPackageOption pkgs "chromium" { };
109
110 maxQueueSize = mkOption {
111 type = types.int;
112 default = 0;
113 description = "Maximum queue size for chromium-based conversions. Setting to 0 disables the limit.";
114 };
115
116 autoStart = mkOption {
117 type = types.bool;
118 default = false;
119 description = "Automatically start chromium when Gotenberg starts. If false, Chromium will start on the first conversion request that uses it.";
120 };
121
122 disableJavascript = mkOption {
123 type = types.bool;
124 default = false;
125 description = "Disable Javascript execution.";
126 };
127
128 disableRoutes = mkOption {
129 type = types.bool;
130 default = false;
131 description = "Disable all routes allowing Chromium-based conversion.";
132 };
133 };
134
135 downloadFrom = {
136 allowList = mkOption {
137 type = types.nullOr types.str;
138 default = ".*";
139 description = "Allow these URLs to be used in the `downloadFrom` API field. Accepts a regular expression.";
140 };
141 denyList = mkOption {
142 type = types.nullOr types.str;
143 default = null;
144 description = "Deny accepting URLs from these domains in the `downloadFrom` API field. Accepts a regular expression.";
145 };
146 maxRetries = mkOption {
147 type = types.int;
148 default = 4;
149 description = "The maximum amount of times to retry downloading a file specified with `downloadFrom`.";
150 };
151 disable = mkOption {
152 type = types.bool;
153 default = false;
154 description = "Whether to disable the ability to download files for conversion from outside sources.";
155 };
156 };
157
158 libreoffice = {
159 package = mkPackageOption pkgs "libreoffice" { };
160
161 restartAfter = mkOption {
162 type = types.int;
163 default = 10;
164 description = "Restart LibreOffice after this many conversions. Setting to 0 disables this feature.";
165 };
166
167 maxQueueSize = mkOption {
168 type = types.int;
169 default = 0;
170 description = "Maximum queue size for LibreOffice-based conversions. Setting to 0 disables the limit.";
171 };
172
173 autoStart = mkOption {
174 type = types.bool;
175 default = false;
176 description = "Automatically start LibreOffice when Gotenberg starts. If false, Chromium will start on the first conversion request that uses it.";
177 };
178
179 disableRoutes = mkOption {
180 type = types.bool;
181 default = false;
182 description = "Disable all routes allowing LibreOffice-based conversion.";
183 };
184 };
185
186 pdfEngines = {
187 merge = mkOption {
188 type = types.listOf (
189 types.enum [
190 "qpdf"
191 "pdfcpu"
192 "pdftk"
193 ]
194 );
195 default = [
196 "qpdf"
197 "pdfcpu"
198 "pdftk"
199 ];
200 description = "PDF Engines to use for merging files.";
201 };
202 convert = mkOption {
203 type = types.listOf (
204 types.enum [
205 "libreoffice-pdfengine"
206 ]
207 );
208 default = [
209 "libreoffice-pdfengine"
210 ];
211 description = "PDF Engines to use for converting files.";
212 };
213 readMetadata = mkOption {
214 type = types.listOf (
215 types.enum [
216 "exiftool"
217 ]
218 );
219 default = [
220 "exiftool"
221 ];
222 description = "PDF Engines to use for reading metadata from files.";
223 };
224 writeMetadata = mkOption {
225 type = types.listOf (
226 types.enum [
227 "exiftool"
228 ]
229 );
230 default = [
231 "exiftool"
232 ];
233 description = "PDF Engines to use for writing metadata to files.";
234 };
235
236 disableRoutes = mkOption {
237 type = types.bool;
238 default = false;
239 description = "Disable routes related to PDF engines.";
240 };
241 };
242
243 logLevel = mkOption {
244 type = types.enum [
245 "error"
246 "warn"
247 "info"
248 "debug"
249 ];
250 default = "info";
251 description = "The logging level for Gotenberg.";
252 };
253
254 environmentFile = mkOption {
255 type = types.nullOr types.path;
256 default = null;
257 description = "Environment file to load extra environment variables from.";
258 };
259
260 extraArgs = mkOption {
261 type = types.listOf types.str;
262 default = [ ];
263 description = "Any extra command-line flags to pass to the Gotenberg service.";
264 };
265 };
266 };
267
268 config = mkIf cfg.enable {
269 assertions = [
270 {
271 assertion = cfg.enableBasicAuth -> cfg.environmentFile != null;
272 message = ''
273 When enabling HTTP Basic Authentication with `services.gotenberg.enableBasicAuth`,
274 you must provide an environment file via `services.gotenberg.environmentFile` with the appropriate environment variables set in it.
275
276 See `services.gotenberg.enableBasicAuth` for the names of those variables.
277 '';
278 }
279 {
280 assertion = !(lib.isList cfg.pdfEngines);
281 message = ''
282 Setting `services.gotenberg.pdfEngines` to a list is now deprecated.
283 Use the new `pdfEngines.mergeEngines`, `pdfEngines.convertEngines`, `pdfEngines.readMetadataEngines`, and `pdfEngines.writeMetadataEngines` settings instead.
284
285 The previous option was using a method that is now deprecated by upstream.
286 '';
287 }
288 ];
289
290 systemd.services.gotenberg = {
291 description = "Gotenberg API server";
292 after = [ "network.target" ];
293 wantedBy = [ "multi-user.target" ];
294 path = [ cfg.package ];
295 environment = {
296 LIBREOFFICE_BIN_PATH = "${cfg.libreoffice.package}/lib/libreoffice/program/soffice.bin";
297 CHROMIUM_BIN_PATH = lib.getExe cfg.chromium.package;
298 FONTCONFIG_FILE = pkgs.makeFontsConf {
299 fontDirectories = [ pkgs.liberation_ttf_v2 ] ++ cfg.extraFontPackages;
300 };
301 # Needed for LibreOffice to work correctly.
302 # https://github.com/NixOS/nixpkgs/issues/349123#issuecomment-2418330936
303 HOME = "/run/gotenberg";
304 };
305 serviceConfig = {
306 Type = "simple";
307 DynamicUser = true;
308 ExecStart = "${lib.getExe cfg.package} ${lib.escapeShellArgs args}";
309
310 # Needed for LibreOffice to work correctly.
311 # See above issue comment.
312 WorkingDirectory = "/run/gotenberg";
313 RuntimeDirectory = "gotenberg";
314
315 # Hardening options
316 PrivateDevices = true;
317 PrivateIPC = true;
318 PrivateUsers = true;
319
320 ProtectClock = true;
321 ProtectControlGroups = true;
322 ProtectHome = true;
323 ProtectHostname = true;
324 ProtectKernelLogs = true;
325 ProtectKernelModules = true;
326 ProtectKernelTunables = true;
327 ProtectProc = "invisible";
328
329 RestrictAddressFamilies = [
330 "AF_UNIX"
331 "AF_INET"
332 "AF_INET6"
333 "AF_NETLINK"
334 ];
335 RestrictNamespaces = true;
336 RestrictRealtime = true;
337
338 LockPersonality = true;
339
340 SystemCallFilter = [
341 "@sandbox"
342 "@system-service"
343 "@chown"
344 ];
345 SystemCallArchitectures = "native";
346
347 UMask = 77;
348 } // optionalAttrs (cfg.environmentFile != null) { EnvironmentFile = cfg.environmentFile; };
349 };
350 };
351
352 meta.maintainers = with lib.maintainers; [ pyrox0 ];
353}