Reverse proxy and TLS

A reverse proxy is the web's concierge: it owns the public address, speaks HTTPS to the world, and walks each request to a plain service behind it. Putting one in front of tunnel-me is the recommended way to serve HTTPS — one wildcard certificate covers your base domain and every tunnel at once, and the proxy handles the details.

flowchart LR
    v(["Visitor"]) -->|"HTTPS"| p["Reverse proxy<br/>wildcard certificate"]
    p -->|"HTTP"| t["tunnel-me :8080"]

With this setup, leave --tls.enabled off (plain HTTP between proxy and tunnel-me on a private network is fine) and run the server on its default port.

The wildcard certificate

Every tunnel is a subdomain, so the certificate must be valid for *.tunnels.example.com and tunnels.example.com. Regular HTTP-based certificate renewal (Let's Encrypt "HTTP-01") cannot prove ownership of a wildcard name — use a DNS-based challenge instead (Let's Encrypt "DNS-01"): the certificate authority asks you to publish a temporary TXT record in your domain, the provider-specific Caddy/nginx plugins automate this. Most DNS providers are supported by the automation plugins of the popular proxies.

If you'd rather not automate, you can also buy any certificate covering *.tunnels.example.com + tunnels.example.com and drop the files in.

If you terminate TLS on tunnel-me itself instead (with --tls.enabled), the same wildcard requirement applies to the files you pass to --tls.cert.

Telling tunnel-me about the proxy

When TLS ends at the proxy, add this to the server:

--auth.oidc.trust-proxy

It makes every sign-in that goes through the proxy honor the forwarded protocol and host headers — session cookies get their secure flag and redirects use your public address. Without it, sign-ins misbehave behind TLS-terminating proxies. It only affects OIDC sign-ins; basic/htpasswd panels work unchanged.

Caddy

Caddy can request and renew the wildcard certificate on its own (this uses its DNS plugin for your provider — install the build for your DNS provider):

tunnels.example.com {
    reverse_proxy 127.0.0.1:8080
}

*.tunnels.example.com {
    tls {
        dns <provider> <provider-specific-options>
    }
    reverse_proxy 127.0.0.1:8080
}

Websockets — which the agents use — work without extra configuration.

nginx

server {
    listen 443 ssl;
    http2 on;
    server_name tunnels.example.com *.tunnels.example.com;

    ssl_certificate     /etc/ssl/tunnels.example.com.crt;   # must cover wildcard + base
    ssl_certificate_key /etc/ssl/tunnels.example.com.key;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        # websocket support for /agent (agents' connection)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 3600s;
    }
}

with the upgrade mapping once in the http block:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

proxy_read_timeout matters: an agent connection sits idle between visitor requests, and the nginx default of 60 seconds would keep dropping it (the agent reconnects, but pointlessly). An hour is a safe value.

Traefik (file provider)

http:
  routers:
    tunnel-me:
      rule: "Host(`tunnels.example.com`) || HostRegexp(`{sub}.tunnels.example.com`)"
      service: tunnel-me
      tls:
        certResolver: myresolver
  services:
    tunnel-me:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1:8080"

Websocket forwarding is automatic. For the wildcard certificate use a DNS-01 challenge in your certificate resolver configuration.

The X-User header and proxy-based login

Tunnels forward the signed-in visitor's username to your app in the X-User header (switchable per tunnel). If you also use the webauth panel login — where the proxy sets a header naming the signed-in admin — be aware both features read headers that a client could try to forge. Configure your proxy to overwrite (not append) those headers, and give the panel login its own header name (e.g. X-Panel-User) if your proxy sets X-User for other apps.