Skip to content

Security ​

This page is for people who run Reportforge. It says what to put in front of it, what to expose, where the secrets are, which requests reportd makes, and what bounds a render. It is a short threat model, not a certification: there has been no external penetration test yet.

What Reportforge guards against, in brief:

  • People on the network who aren't signed in. They reach only reportd's sign-in, setup (with a one-time code) and public report links. Rate limits and a sign-in throttle slow guessing (rate-limits.md).
  • Authors and API callers who try to make reportd reach what they can't. Every request reportd makes for a document, a data source, a webhook or a font goes through an address guard.
  • Documents that try to use up the server. Every render has limits on pages, sub-report depth, size and time.

What it trusts: administrators, the machine and volume it runs on, and the services an administrator configures (identity providers, the assistant's provider, databases).

In front of reportd ​

reportd serves plain HTTP. It has no TLS of its own.

  • Put a reverse proxy with TLS in front of it, and set publicURL to the address people use, such as https://reports.example.com (custom-domain.md). With an https publicURL, the session cookie is always Secure. Passkeys and sign-in providers need publicURL too.
  • Set security.trustedProxies to the proxy's address or network. Then reportd takes the client's address from the proxy's header, and believes X-Forwarded-Proto only from the proxy. Without it, every client shares the proxy's rate limits and sign-in throttle (rate-limits.md).
  • Set security.trustedOrigins only if other sites' pages must send changes with a user's session. Empty is right for most installs.
  • On a workstation, plain http://localhost is fine. Setup warns about plain http on any other host.

Ports ​

PortWhatExpose it?
43123reportdYes, to the proxy. With the proxy on the same machine, publish it on loopback only: 127.0.0.1:43123:43123 in docker-compose.yml.
43124renderdNever.
5432Postgres (postgres.yml)No. Compose doesn't publish it.

renderd runs any document it is sent and has no users. In the image, reportd starts it on loopback. As its own service (split.yml, the Helm chart) it listens on the internal network and every call needs its token. renderd refuses to listen on any address but loopback without --token-file. The image exposes only 43123.

Secrets ​

  • secret: references. A password, key or token in the config is secret:env:NAME or secret:file:NAME. File references are read from secrets.dir (/run/secrets/ in the image). The administration site takes only references, never values (settings.md).

  • The first administrator. By default there is none: reportd starts in setup mode. The one-time setup code goes to the log and to dataDir/setup-code (mode 0600), and nowhere else. Setup deletes it. If you set admin.password instead, use a secret: reference; a literal password must be changed at the first sign-in.

  • Keys reportd makes in dataDir on first start, each mode 0600, when the config doesn't give one:

    FileWhat it doesInstead, set
    viewer-link.keySigns report linksviewer.linkSecret
    mfa.keySeals the users' authenticator secretsauth.mfaKey
    saml-sp.pemSigns SAML requests, decrypts assertionsauth.saml.key

    Backups leave these out on purpose, so a stolen backup can't make codes or sign links. Keep a copy apart from the backups, or set them as secret: references (backup.md).

  • dataDir/auth.json holds password hashes, API key hashes and sealed MFA secrets. Sessions are not in backups.

  • Not encrypted at rest: the volume (templates, reports, data) and the keys above are plain files. Encrypt the disk.

Outbound requests ​

These requests go through reportd's address guard:

RequestAllowed toSetting for internal addresses
HTTP data sourcesThe connection's allowHosts (by default, its base URL's host); redirects to the same host onlyconnections[].allowPrivate
Remote assets, fetched once at publishThe environment's allowlisted origins; redirects to them onlyassets.remote.<env>.allowPrivate
Job webhooksThe origins in jobs.webhooks.endpoints; no redirectsjobs.webhooks.endpoints[].allowPrivate
Font sourcesThe source's hostsThe source's "allow internal addresses"
The example galleryexamples.gallery, by default the public repository on GitHub; an https gallery doesn't follow a redirect to httpNone. For a gallery inside your network, use a folder (examples.md)
The version check, once a dayupdates.releases, by default GitHub's API for the public repository; it sends no version, install ID or usage (updates.md)None. Turn it off with updates.check: false

The guard checks the address actually dialled, after DNS, so a name that resolves to an internal address is refused too. It refuses loopback, private, link-local (which includes cloud metadata at 169.254.169.254), carrier-grade NAT, multicast and reserved addresses, and IPv6 addresses that carry one of them (NAT64, 6to4). These requests use no proxy, since a proxy would hide the address. Every allowPrivate is off by default. Turn one on only for a service inside your network, and keep its hosts narrow.

These requests don't go through the guard, because an administrator names the address and it is often inside the network:

  • sign-in providers: OIDC discovery and keys, and SAML metadata;
  • the assistant's provider (the Anthropic API, or agents.assistant.url);
  • renderd (renderer.url, renderer.urls, renderer.discover);
  • SQL connections.

Renders never fetch anything. renderd makes no outbound requests.

Render limits ​

reportd bounds what reaches it: request bodies (limits.maxBodyBytes, 20 MB), engine calls (limits.timeoutSec, 30 s) and the rate of requests (rate-limits.md).

renderd bounds what it runs:

  • Bodies over 256 MB are refused (--max-body).
  • Work runs on as many workers as CPUs, with four times as many waiting (--workers, --queue). When the queue is full, a call gets 503 saturated at once.
  • Each render stops at 2,000 pages, sub-reports 5 deep, 1,000,000 nodes, 10,000,000 expression steps, 60 seconds, 200 MB of data, and images of 20 MB or 50 megapixels. A render that passes one fails, with an error that names the limit. A request may lower these limits, but not raise them.

Documents carry no scripts. Rendered HTML is served under a policy that runs none.

Reporting a vulnerability ​

Write to security@reportforge.example (a placeholder until the support mailbox exists). Please don't open a public issue. Say what you found, how to reproduce it, and which version you ran. We will answer, and agree on a date to publish once it is fixed.