Skip to content

NextPDF Gotenberg security and operations

This bridge sends documents from your application to an external service over the network. That makes it both a server-side request surface and a transport-security surface. The package implements specific, verifiable controls for both. It does not secure the whole system by itself. Those controls work only when you deploy and operate Gotenberg with the matching safeguards. This page explains the controls the package implements and the operational duties that complete them.

Nothing here is a guarantee. Each control is a defined, test-covered behavior with stated limits.

The bridge applies two distinct security policies at different layers:

  • Transport policy (GotenbergSecurityPolicy) — URL scheme enforcement, server-side request forgery (SSRF) screening, Domain Name System (DNS) rebinding defense, input-size limits, and filename screening. This is the layer documented in depth below.
  • HTML parse policy — a parse-layer content policy, defaulting to the NextPDF core default policy, which runs before content reaches a renderer. It complements the transport policy but remains independent of it. This page covers the transport policy.

Server-side request forgery (SSRF) screening

Section titled “Server-side request forgery (SSRF) screening”

The bridge screens the configured API URL before any byte leaves the process. The control has three parts.

Scheme enforcement. Only https is accepted (case-insensitive). A plain http:// URL is rejected. Transport Layer Security (TLS) is therefore mandatory for every conversion and for the health probe.

Address screening. If the host is an IP literal, the bridge rejects it when it falls in a private or reserved range. If the host is a name, the bridge resolves it to all of its A and AAAA records, and rejects the request if any resolved address is private or reserved. Resolving the full record set, rather than a single address, is the control that defeats an attacker who hides a private address behind a name that also returns a public one. This follows the OWASP SSRF prevention guidance approach: retrieve every IP address behind the domain name (A and AAAA records, for IPv4 and IPv6), and validate each one against an allowlist (OWASP Cheat Sheet Series, SSRF prevention, application-layer defense; pinned in the page’s retrieval-augmented generation (RAG) sidecar).

Time-of-check/time-of-use (TOCTOU) re-check. The bridge re-resolves and compares the address set captured during validation immediately before the request. If a new address appears, the request is aborted with a DNS-rebinding error. This closes the window between validation and connection that a rebinding attack would otherwise exploit.

When the bridge uses its cURL-pinned transport, the validated address set is bound to the connection with CURLOPT_RESOLVE, so the kernel connects to the vetted address rather than to whatever a fresh connect-time DNS lookup might return. Redirect following is disabled on that transport (CURLOPT_FOLLOWLOCATION off, CURLOPT_MAXREDIRS zero), so a 3xx cannot silently send the request to an unvetted host. The policy layer receives the response instead.

Operational consequence. The SSRF guard rejects private and reserved addresses by design. If your Gotenberg runs on a private network, you cannot point the bridge at its private address. Expose it through an address the guard accepts and protect that path with network segmentation and authentication, as described under deployment below.

TLS peer and host verification are always on in the cURL-pinned transport (CURLOPT_SSL_VERIFYPEER true, CURLOPT_SSL_VERIFYHOST 2). On top of standard chain validation, the bridge supports SubjectPublicKeyInfo (SPKI) pinning.

Each pin is a SHA-256 hash of the server’s SubjectPublicKeyInfo, expressed as sha256/<base64>. The bridge accepts a certificate when its SPKI hash matches any pin in the combined primary-plus-backup set. This backup-pin model follows RFC 7469 §4.3, which identifies a backup pin — a fingerprint of a secondary, not-yet-deployed key pair — as the primary recovery path for an inadvertent pin-validation failure, and §2.5, which requires the pinned set to include at least one pin not present in the current certificate chain (RFC 7469 §4.3 and §2.5; pinned in the page’s RAG sidecar). The bridge’s code declares RFC 7469 §2.1 and §2.6 for the at-least-one-backup-pin and combined-set intersection semantics. Pinning is opt-in: with no pins configured, standard chain validation applies and pinning is not enforced.

A pin that does not parse raises a configuration error before any request. A live certificate whose SPKI matches no configured pin causes the transport to fail the request — by design.

An incorrect rotation locks the bridge out of the service. Rotate without an outage:

  1. Before changing the server key, generate the SPKI pin for the new key and add it to the backup pin list. Deploy that configuration. The bridge now accepts both the current and the future key.
  2. Roll the server certificate or key to use the new key.
  3. Confirm conversions still succeed (the new key now matches the backup pin).
  4. Move the new pin from the backup list to the primary list and remove the retired key’s pin. Deploy.
  5. Generate and stage the pin for the next rotation as the new backup so the set always carries a usable spare.

Keeping the backup list separate from the primary list lets you stage and validate the next pin without touching the active one.

When apiKey is non-empty, the bridge sends it as an Authorization: Bearer header on the conversion request. The field is marked #[\SensitiveParameter] so the value is redacted from stack traces. The bridge does not load the secret for you; supply it from a secret manager at process start and never commit it. The token is not written to the request log — the logged debug entry contains the URL, file name, format, and content length only.

A response is accepted only when the status is 200, the Content-Type contains application/pdf, and the body begins with the %PDF signature. The byte-signature check matters because a declared content type alone does not prove what the bytes are. The WHATWG MIME Sniffing standard formalizes the same reasoning in its MIME-type sniffing algorithm, which derives a computed type from leading byte-pattern matching rather than from the supplied type. The OWASP file upload guidance states the corresponding application principle: validate the file type and do not trust the declared Content-Type header, because it can be spoofed (WHATWG MIME Sniffing §6.2.3; OWASP Cheat Sheet Series, file-upload validation; both pinned in the page’s RAG sidecar). The bridge applies the equivalent check defensively on the inbound side: a mismatch raises a typed exception, and the bytes are never returned as a result.

This boundary is also why the PSR-18 contract matters here. A PSR-18 client throws only when it cannot send the request or cannot parse the response into a PSR-7 object — it does not throw on an error status code. A well-formed 4xx/5xx response is returned to the caller as normal (PSR-18, “Exceptions”; pinned in the page’s RAG sidecar). The bridge therefore inspects the status, type, and signature itself rather than assuming a returned response is successful. The HTTP semantics for a content-type constraint violation — a 415 (Unsupported Media Type) rejection, where a server refuses content in an unsupported format — are the model the inbound check mirrors (RFC 9110 §15.5.16; pinned in the page’s RAG sidecar).

The bridge has one in-process resource bound: maxFileSize (default 52,428,800 bytes = 50 MiB). It is enforced before the request, so an oversized input never reaches the service. There is no built-in concurrency limit, rate limit, or output-size cap in the bridge. Those are deployment and caller responsibilities (see /integrations/gotenberg/production-usage/). Set maxFileSize to the smallest value your real documents require — a tighter cap is a cheaper denial-of-service control.

Deploying and securing the Gotenberg service

Section titled “Deploying and securing the Gotenberg service”

The bridge is only as safe as the service it calls. You operate that service; the duties below complete the controls above.

  • Terminate TLS in front of Gotenberg. Gotenberg’s container speaks plain HTTP by default. The bridge requires HTTPS, so place Gotenberg behind a TLS-terminating reverse proxy, ingress, or service mesh and point the bridge at the HTTPS endpoint. Pin the proxy’s SPKI if you enable pinning.
  • Do not expose Gotenberg publicly. It performs document conversion with LibreOffice and Chromium-class engines, and it is not an internet-facing service. Restrict ingress to the application hosts that call it, using network policy or firewall rules.
  • Require authentication on the path. The bridge sends a bearer token when configured; enforce it (or mutual TLS) at the proxy so an unauthenticated request cannot reach the conversion engine.
  • Pin a specific service version. The bridge assumes exactly two service paths — /forms/libreoffice/convert and /health. Pin the Gotenberg image to a specific patch tag, verify those two paths against the version you deploy, and re-verify on every upgrade.
  • Size conversion capacity deliberately. Each conversion holds a worker for the request duration. Size the Gotenberg deployment for your peak concurrent conversion rate and bound in-flight conversions on the caller side to match. Capacity is a property of your deployment, not of this package.
  • Treat conversion inputs as untrusted. Documents pushed through conversion are processed by complex engines. Constrain maxFileSize, isolate the Gotenberg deployment (its own network segment, minimal egress, no access to internal services), and keep the engine patched.
  • It is not “secure by default”: the controls are real but they depend on correct deployment and configuration.
  • It does not make conversion “tamper-proof” or the output “certified”. It validates the transport and the response shape; it does not attest document content.
  • It does not provide signing, timestamping, or long-term validation. Those are post-processing concerns. The Pro edition’s PAdES support is the B-B baseline only and does not include B-T, B-LT, or B-LTA; nothing in this bridge implies a timestamp or LTV capability.
  • It does not support “all Office files”. It supports the six enumerated formats and rejects everything else before any request.
  • /integrations/gotenberg/configuration/ — transport-selection rules and the full pin model.
  • /integrations/gotenberg/production-usage/ — retries, timeouts, concurrency, and observability.
  • /integrations/gotenberg/troubleshooting/ — every security exception and its trigger.
  • /integrations/gotenberg/overview/ — the conversion flow and dependency model.