Skip to main content

Hardening checklist

Work through this before exposing PDF Server. Items are grouped by what they defend against; see Proved issues for the underlying findings.

Network & access

  • Put an authenticating reverse proxy in front of the service (no route has built-in auth).
  • Review the in-app request body-size limit (--server-max-body-size, default 1 MiB) and tighten at the proxy if needed.
  • Review the in-app read-header/read/idle timeouts (--server-*-timeout) and add write/total-request limits at the proxy.
  • Add rate limiting at the proxy.
  • Do not expose the port directly to untrusted networks.

Egress (defends SSRF)

  • Apply an egress network policy that blocks:

    • link-local 169.254.0.0/16 (cloud metadata 169.254.169.254),
    • private ranges 10/8, 172.16/12, 192.168/16,
    • anything the container does not legitimately need.

    This is the control for the browser's own fetches (injected JS can load arbitrary URLs); the in-app guard below does not cover them.

  • Remote-template fetches are guarded in-app by fixed secure defaults: a fetch timeout, a response-size cap, and a block on loopback/private/ link-local targets that cannot be disabled process-wide. This is defense in depth — still build their requests only from trusted data.

Browser sandbox (compensates for --no-sandbox)

  • Run the container with a restrictive seccomp profile.
  • Prefer user-namespace isolation or a stronger sandbox (gVisor).
  • Run as non-root (the image already does).
  • Set CPU and memory limits.

Template & data hygiene (defends XSS-into-PDF)

  • Never pass untrusted data to raw-HTML sinks: {{{triple}}}, the markdown helper, or embed_text.
  • Sanitize untrusted Markdown before rendering it.
  • Validate untrusted data with a schema.json; for compositions, validate at the composition boundary (child schemas are not re-validated).
  • Avoid placing untrusted data directly into src/href/URL positions; validate/allowlist if you must.

Availability

  • Raise RENDER_POOL_MAX_TOTAL above the default of 1 for real load.
  • Bound total per-request time at the proxy.
  • Monitor /_metadata for liveness (cheap) and /_status for pool health.

Data exposure

  • Treat document/asset contents on the internal render server as visible to the rendering page; do not ship secrets in assets/.