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