Skip to content

reverse proxy

Alexander Herling edited this page Oct 11, 2026 · 12 revisions

Reverse proxy

rDownloader binds to loopback by default and provides no TLS of its own. Remote access means putting a proxy in front and telling rDownloader about it; otherwise four things go wrong in ways that do not look like a configuration problem. Bound beyond loopback, the service belongs behind a proxy for one more reason: it sets no limit on how long a client may take to send its request headers, and the proxy does.

What goes wrong when it is not configured

Symptom Cause
Everyone shares one rate limit; the session list shows one address for every device No trusted proxy is configured, so the proxy's own address is treated as the client's
Signing in appears to work, and the next page is the login screen again The session cookie is Secure but the deployment is plain HTTP, so the browser drops it
The page loads blank under a sub-path The mount point is not configured, so the asset references point at the root
Anyone can claim to be any address A forwarded header is believed from a peer that is not a configured proxy — this is why nothing is trusted by default

The settings

All under Settings → Security → Reachability & reverse proxy; in the settings document they are external_url, trusted_proxies, cookie_security and allowed_hosts.

external_url — what the outside world calls this installation, scheme and path included: https://rd.example.com, or https://home.example.com/downloads.

trusted_proxies — the address ranges whose X-Forwarded-For is believed, as CIDR or bare addresses: 127.0.0.1, 10.0.0.0/8, fd00::/8. Anything not in this list has its forwarded headers ignored entirely and is treated as the client itself. Empty is the safe default. Several X-Forwarded-For or Forwarded lines in one request are read as one list, so a proxy that appends a line of its own is believed rather than the client.

cookie_security — auto follows the scheme of the external URL and is right for almost every deployment. always is for a proxy that terminates TLS but is described by an http URL; never is for a plain-HTTP LAN deployment, where a Secure cookie would simply be dropped.

Allowed host names

rDownloader checks the host name of every request before it answers. These always work:

  • an IP address — http://192.168.1.20:8710, http://127.0.0.1:8710;
  • localhost, and names ending in .localhost;
  • a name that exists only on the local network — a single label without a dot (nas, syno-xyz, the Docker Compose service name rdownloader), a name ending in .local, and names under .home.arpa or .internal. Nobody can register these in public DNS, so no page can rebind to them;
  • the host of the external URL.

Any other name is refused with 403 and the code request.host_not_allowed until it is listed under Allowed host names (allowed_hosts), one bare name per line — nas.lan, downloads.example.org, no http://, no port. List the names you type into the browser yourself, such as a NAS hostname under a router's own domain. Open the interface by its IP address to add one. A browser that opens a refused name shows a short page instead of the interface: the name it was opened by, where to allow it and the address to make the change from.

This is the defence against DNS rebinding, where a stranger's web page points its own domain at 127.0.0.1 to reach rDownloader through your browser: that domain is not on the list.

Changes from another site's page

A request that changes something is refused with 403 and the code request.cross_site_refused when the browser reports that a page of another site sent it — its Origin names neither the address the request went to nor the external URL. That stops a stranger's page from installing a plugin or importing rules through your browser, which with the login switched off would otherwise run as the administrator. Behind a proxy, current browsers pass anyway; if an older browser is refused, set the external URL, or have the proxy pass the public name on (proxy_set_header Host $host). On a public port other than 80 or 443, set the external URL. Scripts and command-line tools are not affected.

Switching the administrator login off applies to this machine only. Without a login, a request is let in only when it comes from a loopback address and no proxy forwarded it. Behind a reverse proxy — even one on the same machine — and from any other computer the password is still required.

A proxy or tunnel on the same machine must set X-Forwarded-For. One that forwards requests without it connects from loopback on everybody's behalf, and with the login switched off everybody behind it is the administrator. Settings → Security therefore warns while the login is off and a trusted proxy or an external URL is set; switch the login back on under Settings → Security → Sign-in, or make sure the proxy sets the header.

rdownloader doctor prints the resolved contract and warns about the half-configured combinations. The settings are read at start and applied again whenever they are saved.

The whole application works under a sub-path — API, event stream and MCP endpoint included, and every file the interface loads: its scripts and stylesheets, fonts and the logo are asked for under the sub-path too, so a proxy that passes only /rd/ serves every page and nothing is requested at the root.

nginx

server {
    listen 443 ssl;
    server_name rd.example.com;

    location / {
        proxy_pass http://127.0.0.1:8710;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;   # not read; the scheme comes from external_url

        # The event stream is a long-lived response. Without these, updates arrive in
        # bursts when a buffer fills, or the connection is closed after 60 seconds.
        proxy_buffering off;
        proxy_read_timeout 1h;
        proxy_http_version 1.1;
    }
}

Settings: external_url = https://rd.example.com, trusted_proxies = 127.0.0.1.

Under a sub-path, keep the same path on both sides — rDownloader strips its own mount point, so the proxy must not strip it as well:

location /downloads/ {
    proxy_pass http://127.0.0.1:8710;   # no trailing path: the prefix is passed through
    # …the same headers as above
}

Settings: external_url = https://rd.example.com/downloads.

Caddy

rd.example.com {
    reverse_proxy 127.0.0.1:8710 {
        flush_interval -1   # do not buffer the event stream
    }
}

Caddy sets X-Forwarded-For itself. Settings: external_url = https://rd.example.com, trusted_proxies = 127.0.0.1.

DynDNS and Let's Encrypt with Caddy

For a home connection without a fixed address: a dynamic DNS name points at your router, the router forwards HTTPS to the machine running Caddy, and Caddy fetches and renews a Let's Encrypt certificate by itself. rDownloader keeps listening on loopback only.

  1. Get a DynDNS name and keep it current. Most routers do this themselves — on a FRITZ!Box under Internet → Permit Access → DynDNS (or use its MyFRITZ! name); otherwise a small update client from your DynDNS provider. Example name: rd.example.dyndns.org.

  2. Forward the ports in the router to the machine Caddy runs on — on a FRITZ!Box under Internet → Permit Access → Port Sharing:

    • TCP 443 — the HTTPS traffic;
    • TCP 80 — Let's Encrypt checks the name over plain HTTP before it issues a certificate, and Caddy answers that check itself and redirects everything else to HTTPS.

    Never forward rDownloader's own port (8710); only Caddy is reachable from outside.

  3. Run Caddy on that machine with this Caddyfile:

    rd.example.dyndns.org {
        reverse_proxy 127.0.0.1:8710 {
            flush_interval -1   # do not buffer the event stream
        }
    }

    On first start Caddy requests the certificate; from then on it renews it before it runs out. On Windows, caddy run in the folder holding the Caddyfile, or install it as a service; on Linux the caddy package runs it as a systemd service.

  4. Tell rDownloader about it under Settings → Security → Reachability & reverse proxy:

    • external_url = https://rd.example.dyndns.org
    • trusted_proxies = 127.0.0.1 — or Caddy's address when it runs on another machine, or the container network (see In Docker).

    The host of the external URL is allowed automatically; nothing needs to go under Allowed host names.

  5. Check it from outside, for example from a phone on mobile data: https://rd.example.dyndns.org shows the sign-in page with a valid certificate, and the session list under Settings → Security → Sessions shows the phone's address rather than Caddy's.

When port 80 cannot be opened (some providers block it), Caddy can prove the name over DNS instead: that needs a Caddy build with your DNS provider's module and an API token of that provider, configured with tls { dns <provider> … }. Not every DynDNS service offers such an API.

Without a public IPv4 address (DS-Lite, carrier-grade NAT) nothing can be forwarded on IPv4. Either forward over IPv6 — the DynDNS name then needs an AAAA record, and your router has to open the port for the machine's IPv6 address — or use a tunnel such as Cloudflare Tunnel, which needs no open port at all.

Before you open it to the internet: a strong administrator password and a second factor — a passkey or an authenticator app (see Accounts, sessions and permissions). With the external URL set, passkeys work under the DynDNS name too (Passkeys need the external URL). HTTPS is also what Web Push needs: the interface installed as an app on a phone can receive notifications.

Traefik

http:
  routers:
    rdownloader:
      rule: "Host(`rd.example.com`)"
      service: rdownloader
      tls: {}
  services:
    rdownloader:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1:8710"
        responseForwarding:
          flushInterval: "1ms"   # the event stream again

In Docker

The trusted range is the container network, not loopback. For the default bridge that is 172.16.0.0/12.

rdownloader doctor prints the configured trusted range and its warnings, but it observes no request — the address requests actually arrive from is in the proxy's access log or in docker network inspect.

Cloudflare Tunnel

cloudflared connects out to Cloudflare and hands the requests for a public name on to rDownloader; no port is opened to the internet, and Cloudflare provides the HTTPS. Point the public hostname at rDownloader on loopback — in the tunnel's configuration file, or as the public hostname's service (HTTP, 127.0.0.1:8710) in the Zero Trust dashboard:

ingress:
  - hostname: rd.example.com
    service: http://127.0.0.1:8710
  - service: http_status:404

The first visit through the tunnel shows rDownloader does not answer to this host name (request.host_not_allowed): the name is not allowed yet. Open rDownloader by an address it always answers to — http://127.0.0.1:8710 on the machine itself, or its LAN address — and set under Settings → Security → Reachability & reverse proxy:

  • External URL https://rd.example.com. Its host is always allowed, the session cookie is marked Secure and passkeys bind to it. If the tunnel is only a second way in and the external URL names something else, put rd.example.com under Allowed host names instead.
  • Trusted proxies 127.0.0.1. cloudflared connects from this machine; Cloudflare sends the visitor's address as X-Forwarded-For (beside its own CF-Connecting-IP), which rDownloader believes only from a trusted proxy. Without it every visitor has cloudflared's address in the sign-in limiting and in the session list. With cloudflared in a container, trust its container network instead (see In Docker).

Signing in stays on through the tunnel, even with the login switched off for this computer. The live updates need no extra setting.

Cloudflare Access as a second lock. An Access application on rd.example.com puts Cloudflare's own sign-in (a code by e-mail, your identity provider) in front of the tunnel, so a stranger never sees rDownloader's login page. It comes in addition to rDownloader's password, never instead of it (see below). Clients that are not a browser — the browser extension's pairing, Sonarr, Radarr and the other *arr apps, the command line — cannot answer Access's sign-in: give them an Access service token where they can send its headers, or keep them on the LAN address.

Passkeys need the external URL

A passkey is bound to the origin it was created at — that is its phishing resistance. rDownloader takes that origin from external_url whenever it is set, never from a header the caller chooses. Two consequences:

  • Without external_url, passkeys work only at http://localhost:<port>. Reaching the service at http://127.0.0.1:8710 does not work: browsers bind a passkey to a domain, never to an address. rDownloader refuses with that explanation.
  • Changing external_url invalidates existing passkeys. They were registered against the old origin and the browser will not offer them at the new one. The password still signs in; enrol the passkeys again afterwards. Moving from a bare host name to a subdomain, or from http to https, both count as a change.

Nothing else about passkeys depends on the proxy. They work under a base path, and the X-Forwarded-For settings do not affect them.

What is deliberately not supported

Authentication at the proxy as a replacement for rDownloader's own (forward-auth). Any request reaching the service directly — a container on the same network, a misconfigured firewall — would then be unauthenticated, and trusted proxies vouch for an address, not for a person. The administrator password stays the credential; to sign in with the provider your proxy already uses, set it up as an OpenID Connect provider instead — see Sign in with an identity provider, whose redirect URI comes from the external URL.

Path rewriting in the proxy. Stripping the prefix at the proxy breaks as soon as something generates an absolute URL. Pass the prefix through and set external_url instead.

Related pages

Clone this wiki locally