Appearance
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
publicURLto the address people use, such ashttps://reports.example.com(custom-domain.md). With anhttpspublicURL, the session cookie is alwaysSecure. Passkeys and sign-in providers need publicURL too. - Set
security.trustedProxiesto the proxy's address or network. Then reportd takes the client's address from the proxy's header, and believesX-Forwarded-Protoonly from the proxy. Without it, every client shares the proxy's rate limits and sign-in throttle (rate-limits.md). - Set
security.trustedOriginsonly if other sites' pages must send changes with a user's session. Empty is right for most installs. - On a workstation, plain
http://localhostis fine. Setup warns about plain http on any other host.
Ports
| Port | What | Expose it? |
|---|---|---|
| 43123 | reportd | Yes, 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. |
| 43124 | renderd | Never. |
| 5432 | Postgres (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 issecret:env:NAMEorsecret:file:NAME. File references are read fromsecrets.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 setadmin.passwordinstead, use asecret:reference; a literal password must be changed at the first sign-in.Keys reportd makes in
dataDiron first start, each mode 0600, when the config doesn't give one:File What it does Instead, set viewer-link.keySigns report links viewer.linkSecretmfa.keySeals the users' authenticator secrets auth.mfaKeysaml-sp.pemSigns SAML requests, decrypts assertions auth.saml.keyBackups 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.jsonholds 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:
| Request | Allowed to | Setting for internal addresses |
|---|---|---|
| HTTP data sources | The connection's allowHosts (by default, its base URL's host); redirects to the same host only | connections[].allowPrivate |
| Remote assets, fetched once at publish | The environment's allowlisted origins; redirects to them only | assets.remote.<env>.allowPrivate |
| Job webhooks | The origins in jobs.webhooks.endpoints; no redirects | jobs.webhooks.endpoints[].allowPrivate |
| Font sources | The source's hosts | The source's "allow internal addresses" |
| The example gallery | examples.gallery, by default the public repository on GitHub; an https gallery doesn't follow a redirect to http | None. For a gallery inside your network, use a folder (examples.md) |
| The version check, once a day | updates.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 gets503 saturatedat 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.