Open Chat Interfacedocs

Reverse proxy

TLS in front of the web container, the security headers OCI sends, and the one path that needs a policy of its own — the artifact frame.

Put a TLS-terminating reverse proxy or load balancer in front of the web container (port 8080). The web container's Caddy does the rest: it serves the app, forwards /api/* to the API, and sets security headers. APP_URL must be the public https:// address.

Requirements for your proxy:

  • Do not buffer responses. Replies stream; a buffering proxy holds them back until they finish. Disable response buffering for /api/* (in nginx, proxy_buffering off;).
  • Allow long requests. A reply can stream for minutes. Raise read timeouts accordingly.
  • Allow large uploads: files up to your upload limit (20 MB by default) and imports up to IMPORT_MAX_UPLOAD_BYTES (512 MB by default).
  • Set TRUSTED_PROXIES on the web container to your proxy's addresses, so OCI records each person's own address rather than the proxy's. See below.
  • Send everything through the web container. Do not route /api straight to the API service around it: the API then has no trustworthy client address to record.
  • Do not forward /metrics. It is served by the API, not the web container, and should be scraped from inside your network.

Behind another proxy or an ingress

OCI records a client address on sessions (a person's active sessions under People) and in the audit log, and the sign-in limit counts attempts per address. The web container's Caddy decides that address and passes exactly one to the API, as X-Forwarded-For; the API believes nothing else. Caddy also drops X-Real-IP, CF-Connecting-IP, True-Client-IP and Forwarded, so a client cannot choose its own address by sending them.

By default Caddy trusts nothing in front of it, so the address is whatever connected to the web container. Exposed directly, that is the client. Behind a load balancer, another reverse proxy or a Kubernetes ingress, it is that proxy, so everybody appears to come from one address and shares one sign-in limit.

Set TRUSTED_PROXIES on the web container to the addresses of the proxies in front of it, separated by spaces (not commas). Each entry is an IP address or a CIDR range; private_ranges stands for all private and loopback ranges:

# docker/.env
TRUSTED_PROXIES=10.0.0.0/8 192.168.10.5

Caddy then reads X-Forwarded-For from those proxies right to left, skipping trusted hops, and the first address that is not trusted is the client. A value a client put at the left of the header is never reached. Requests from any other address ignore the header.

  • Trust only addresses that really are your proxies, and make sure each one sets or appends X-Forwarded-For. Most do by default; nginx needs proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;.
  • In Kubernetes, use the ingress controller pods' range, or the cluster's pod CIDR.
  • A proxy that replaces the client address entirely (a TCP load balancer without the PROXY protocol) cannot be recovered from; use the load balancer's HTTP mode.
  • The bundled Compose file passes TRUSTED_PROXIES through to the web container. Elsewhere, set it as an environment variable of the web image. The API has no such setting.

After deploying, sign in and check the address on your own session under People, or in the audit log.

Changed in v0.9.2

Before v0.9.2 the web container passed CF-Connecting-IP and X-Real-IP from the browser through to the API, which trusted them, so any client could choose the address it was recorded and rate-limited under. Upgrade to v0.9.2 or later. See v0.9.2.

The headers OCI sends

Every response from the web container, except one path, carries:

Content-Security-Policy: default-src 'self'; connect-src 'self'; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; script-src 'self'; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Cross-Origin-Opener-Policy: same-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()

Add Strict-Transport-Security at your TLS proxy.

The artifact frame

HTML and SVG artifacts run in a sandboxed frame loaded from /artifact-frame.html, a static file of the web image, with sandbox="allow-scripts" and no allow-same-origin, so it has an opaque origin. It is the one page OCI frames itself, so it gets its own policy instead of the one above:

Content-Security-Policy: sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; base-uri 'none'; form-action 'none'; frame-ancestors 'self'
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), display-capture=()
X-Content-Type-Options: nosniff
Referrer-Policy: no-referrer
Cache-Control: no-cache

and no X-Frame-Options: DENY.

If your proxy adds headers to every response

A proxy that adds its own Content-Security-Policy or X-Frame-Options to every response must exempt /artifact-frame.html, and send the policy above for it, including sandbox allow-scripts and frame-ancestors 'self'. Otherwise artifact previews stay blank (the rest of OCI is unaffected). The page also protects itself: it writes nothing unless it is framed with an opaque origin, so a proxy that sends no policy for it cannot make it run code on OCI's origin.

For example, in nginx, where your server block adds headers:

location = /artifact-frame.html {
    proxy_pass http://oci_web;
    # Let the web container's own headers for this path through unchanged:
    # do not add X-Frame-Options or a Content-Security-Policy here.
}

Identity providers on private networks

If your OIDC or SAML identity provider is on a private network, list its origin in AUTH_TRUSTED_ORIGINS, or discovery is refused. See Configuration.

On this page