Reverse proxy and HTTPS
Jiandu defaults to loopback HTTP for one-machine evaluation. To reach it from another device, put one HTTPS reverse proxy in front of a private HTTP listener and configure Jiandu to trust that exact ingress boundary.
browser ── HTTPS ──> reverse proxy ── private HTTP ──> Jiandu :8077
│
└── the only published application port
Choose the proxy you already operate:
- Caddy for the smallest configuration and automatic public certificates;
- Nginx for an established Nginx deployment and explicit buffering/timeouts;
- Traefik for label/file-driven container routing.
The repository ships a runnable Caddy Compose topology and reviewed Nginx and Traefik equivalents.
What Jiandu verifies
In https_reverse_proxy mode, a request is accepted only when:
- the kernel-observed direct TCP peer is inside
trusted_proxy_cidrs; - the request has one
Hostheader matching the host and port inpublic_origin; and - browser mutations carry an exact matching
Originand acceptable Fetch Metadata.
This makes a forged Host, Origin, X-Forwarded-For, or identity header insufficient by itself.
The proxy must still overwrite forwarding metadata and strip any trusted-identity headers so a
browser cannot smuggle authority through the trusted hop.
1. Choose one canonical public origin
Use the exact address people will enter, with no path or trailing slash:
https://documents.example.net
The origin must use a DNS hostname. Default HTTPS port 443 is omitted; a custom port remains part
of the origin, for example https://documents.example.net:8443. The same origin must be used by
passkeys, OIDC, and email connectors when those features are enabled. Changing it later can break
registered OIDC callbacks and existing passkey relying-party expectations.
Arrange DNS and a trusted certificate before inviting users. A public CA is simplest for a public DNS name. A private CA works for a private name only when every browser/device and the relevant server-side runtimes trust that CA.
2. Identify the direct proxy address
trusted_proxy_cidrs describes the address Jiandu sees on the accepted TCP connection—not the
browser, LAN, CDN, Docker host, or an X-Forwarded-For value.
Prefer the smallest stable network:
127.0.0.1/32and/or::1/128when proxy and Jiandu share a host namespace;- one pinned container address such as
172.30.0.2/32; or - one pod/node CIDR only when the platform cannot give the proxy a stable address and that whole CIDR is an intentional trusted-proxy boundary.
Catch-all /0 networks are rejected. A broad private CIDR may validate but lets every peer in that
network attempt the trusted hop, so use /32 or /128 whenever possible.
3. Configure Jiandu as one boundary
{
"schema_version": 1,
"server": {
"bind": "0.0.0.0:8077",
"request_timeout_seconds": 30,
"ingress": {
"mode": "https_reverse_proxy",
"public_origin": "https://documents.example.net",
"trusted_proxy_cidrs": ["172.30.0.2/32"]
}
}
}
0.0.0.0:8077 is appropriate only inside an internal container/network namespace or behind a host
firewall that limits the port to the proxy. When both processes share a host, prefer
127.0.0.1:8077 and trust loopback.
The equivalent environment boundary is:
export JIANDU_BIND=0.0.0.0:8077
export JIANDU_INGRESS_MODE=https_reverse_proxy
export JIANDU_PUBLIC_ORIGIN=https://documents.example.net
export JIANDU_INGRESS_TRUSTED_PROXY_CIDRS=172.30.0.2/32
Set all three JIANDU_INGRESS_* values together. Do not mix them with the deprecated
public_hostname, trust_tls_termination, or trusted_proxy_cidrs settings. Partial and
mixed-generation boundaries fail closed.
4. Preserve these proxy invariants
Every proxy-specific example does the following:
- listens on HTTPS for the exact public hostname;
- sends the original
Host(including a non-default port) to Jiandu; - overwrites
X-Forwarded-Protowithhttps; - derives and overwrites
X-Forwarded-Forrather than trusting a browser-supplied value; - removes
X-Jiandu-Proxy-Secretand the example identity assertion headers; - allows streaming/resumable request and response bodies without an unexpectedly short timeout; and
- adds
Strict-Transport-Security: max-age=31536000at the TLS edge.
Jiandu also emits that HSTS policy in reverse-proxy mode. The examples deliberately omit
includeSubDomains and preload; add either only if every affected subdomain has a permanent HTTPS
commitment.
If you later configure trusted-proxy authentication, keep the strip rules at the public edge and add authoritative identity headers only after authentication. Rename the strip list to match every configured header.
5. Keep the backend private
Only the proxy's HTTPS port should be published. In Docker Compose, use expose, not ports, on the
Jiandu service and attach proxy plus Jiandu to an internal: true backend network. On a host, bind
Jiandu to loopback. On Kubernetes, use a private ClusterIP service plus network policy and keep the
application replica count at one.
Do not rely on Jiandu's 421 rejection as the only firewall. A direct client that can reach port
8077 is already inside more of the deployment boundary than necessary.
6. Validate through the real path
Validate the configuration offline, then start both services:
jiandu --config /etc/jiandu/jiandu.json --check-config
curl --fail --silent https://documents.example.net/api/v1/health/live
curl --fail --silent https://documents.example.net/api/v1/health/ready
Run the following checks from a second device:
- the certificate is trusted and names the configured host;
- the exact public URL loads and sign-in works;
- a sample upload completes and its original downloads;
- the response includes
Strict-Transport-Security: max-age=31536000; - direct access to port
8077is blocked by routing/firewall policy; and - a request with the wrong host is rejected rather than routed to Jiandu.
For a local Caddy private-CA test, install the generated CA only on test devices rather than bypassing certificate warnings.
Diagnose by status
| Symptom | Likely boundary error |
|---|---|
421 ingress_not_allowed | Direct proxy address is outside the CIDR, or forwarded Host/port differs from public_origin |
403 origin_forbidden on mutations | Browser origin differs, proxy rewrote authority, or a cross-site request reached Jiandu |
| Redirect/cookie loop | Public origin, proxy host, and browser URL are not exactly aligned |
| Every request is one rate-limit peer | Proxy did not supply a trustworthy overwritten X-Forwarded-For chain |
| Uploads fail near a fixed size | Proxy body limit or request buffering, not Jiandu's whole-document limit |
| Long download/upload resets | Proxy read/send/idle timeout is shorter than the operation |
| OIDC callback fails | Provider redirect URI or issuer does not match the configured public origin/provider ID |