Trusted proxies
Paron limits the requests and the connections from each client address.
When a proxy is in front of Paron, all connections come from the address of the proxy. Paron then counts all users as one client, and the requests of one user can use the limit of all of them.
To prevent this, tell Paron which proxies to trust. Paron then gets the address of each client from the proxy.
TCP proxies (L4)
Section titled “TCP proxies (L4)”A TCP proxy passes the TLS connection to Paron and does not read it.
To give Paron the address of the client, the proxy must send the PROXY protocol header at the start of each connection. Paron reads version 1 and version 2 of this header. Give Paron the address of the proxy:
paron server run --trusted-proxies 10.0.0.7,192.168.0.0/24| Sender | PROXY header | Result |
|---|---|---|
In --trusted-proxies |
Yes | Paron uses the client address from the header. |
In --trusted-proxies |
No | Paron uses the address of the sender. |
Not in --trusted-proxies |
Yes | The connection fails. |
Not in --trusted-proxies |
No | Paron uses the address of the sender. |
When --trusted-proxies has a value, Paron logs the warning
not a trusted L4 proxy, PROXY protocol forwarding is ignored for the first connection from an
address that is not in the list. The warning shows one time, with or without a PROXY header.
No other setting is necessary. Paron gets its certificate and accepts tunnels through the proxy.
Examples
Section titled “Examples”services: haproxy: image: haproxy:lts-alpine restart: unless-stopped depends_on: - paron-server configs: - source: haproxy target: /usr/local/etc/haproxy/haproxy.cfg ports: - "443:443/tcp" networks: - paron
paron-server-init: image: ghcr.io/nicolabovolato/paron:latest command: ["server", "bootstrap"] volumes: - paron-server-data:/data network_mode: none
paron-server: image: ghcr.io/nicolabovolato/paron:latest command: ["server", "run"] restart: unless-stopped stop_grace_period: 30s depends_on: paron-server-init: condition: service_completed_successfully read_only: true cap_drop: - ALL security_opt: - no-new-privileges:true tmpfs: - /tmp volumes: - paron-server-data:/data # The proxy carries only TCP, so a tunnel uses TLS over TCP. To give tunnels QUIC, publish the # UDP port from Paron itself: # ports: # - "443:8443/udp" networks: - paron environment: PARON_PUBLIC_URL: https://paron.example.com PARON_LETSENCRYPT: production PARON_ACME_CONTACT: admin@example.com # The network of the proxy. Paron reads the PROXY protocol header only from these addresses. PARON_TRUSTED_PROXIES: 172.30.0.0/24
networks: paron: ipam: config: - subnet: 172.30.0.0/24
volumes: paron-server-data:
configs: haproxy: content: | defaults mode tcp # HAProxy needs these three. Paron sends a keep-alive on each terminal and each tunnel # every 30 seconds, so 2 minutes with no data means that the connection is dead. timeout connect 5s timeout client 2m timeout server 2m
frontend https bind :443 default_backend paron
backend paron # send-proxy-v2 adds the PROXY protocol header. server paron paron-server:8443 send-proxy-v2services: nginx: image: nginx:stable-alpine restart: unless-stopped depends_on: - paron-server configs: - source: nginx target: /etc/nginx/nginx.conf ports: - "443:443/tcp" networks: - paron
paron-server-init: image: ghcr.io/nicolabovolato/paron:latest command: ["server", "bootstrap"] volumes: - paron-server-data:/data network_mode: none
paron-server: image: ghcr.io/nicolabovolato/paron:latest command: ["server", "run"] restart: unless-stopped stop_grace_period: 30s depends_on: paron-server-init: condition: service_completed_successfully read_only: true cap_drop: - ALL security_opt: - no-new-privileges:true tmpfs: - /tmp volumes: - paron-server-data:/data # The proxy carries only TCP, so a tunnel uses TLS over TCP. To give tunnels QUIC, publish the # UDP port from Paron itself: # ports: # - "443:8443/udp" networks: - paron environment: PARON_PUBLIC_URL: https://paron.example.com PARON_LETSENCRYPT: production PARON_ACME_CONTACT: admin@example.com # The network of the proxy. Paron reads the PROXY protocol header only from these addresses. PARON_TRUSTED_PROXIES: 172.30.0.0/24
networks: paron: ipam: config: - subnet: 172.30.0.0/24
volumes: paron-server-data:
configs: nginx: content: | events {}
stream { server { listen 443; proxy_pass paron-server:8443; # proxy_protocol adds the PROXY protocol header. proxy_protocol on; } }services: traefik: image: traefik:v3 restart: unless-stopped depends_on: - paron-server command: - --entrypoints.https.address=:443 - --providers.file.filename=/etc/traefik/paron.yml configs: - source: traefik target: /etc/traefik/paron.yml ports: - "443:443/tcp" networks: - paron
paron-server-init: image: ghcr.io/nicolabovolato/paron:latest command: ["server", "bootstrap"] volumes: - paron-server-data:/data network_mode: none
paron-server: image: ghcr.io/nicolabovolato/paron:latest command: ["server", "run"] restart: unless-stopped stop_grace_period: 30s depends_on: paron-server-init: condition: service_completed_successfully read_only: true cap_drop: - ALL security_opt: - no-new-privileges:true tmpfs: - /tmp volumes: - paron-server-data:/data # The proxy carries only TCP, so a tunnel uses TLS over TCP. To give tunnels QUIC, publish the # UDP port from Paron itself: # ports: # - "443:8443/udp" networks: - paron environment: PARON_PUBLIC_URL: https://paron.example.com PARON_LETSENCRYPT: production PARON_ACME_CONTACT: admin@example.com # The network of the proxy. Paron reads the PROXY protocol header only from these addresses. PARON_TRUSTED_PROXIES: 172.30.0.0/24
networks: paron: ipam: config: - subnet: 172.30.0.0/24
volumes: paron-server-data:
configs: traefik: content: | tcp: routers: paron: entryPoints: - https rule: HostSNI(`*`) service: paron services: paron: loadBalancer: servers: - address: paron-server:8443 # proxyProtocol adds the PROXY protocol header. proxyProtocol: version: 2Save the file as docker-compose.yml, set PARON_PUBLIC_URL, and start the services:
docker compose up -dA tunnel tries QUIC first, and QUIC uses UDP. See Transport. These proxies carry only TCP, so you have two options:
| Option | Result |
|---|---|
| Publish only the proxy | Paron is not exposed directly. Tunnels use TLS over TCP through the proxy. |
| Also publish UDP port 443 from Paron | Tunnels use QUIC. Paron is exposed directly on that port. |
Each example has the lines for the second option as a comment.
HTTP proxies (L7)
Section titled “HTTP proxies (L7)”An HTTP proxy ends the TLS connection of the browser and opens its own connection to Paron.
- The browser gets the certificate of the proxy. So the proxy needs the public certificate, and Paron uses its own certificate.
- Paron gets the address of the proxy. The proxy sends the address of the client in a header.
- A tunnel is not HTTP, so the proxy cannot end its TLS connection. For tunnels, the proxy must be a TCP proxy.
The proxy sends connections of both kinds, so give Paron its address in both lists:
paron server run \ --trusted-l7-proxies 10.0.0.7 \ --trusted-proxies 10.0.0.7| Connection | The proxy must | Reason |
|---|---|---|
| Web UI | Connect to Paron with HTTPS | Paron serves HTTPS only. In a private network, the proxy can accept the certificate of Paron with no check. |
| Web UI | Pass WebSocket connections | The terminal is a WebSocket. |
| Web UI | Set or remove the address headers | Paron reads the client address from them. |
| Tunnel | Pass the connection as it is, with the PROXY protocol header | The machine accepts only the key of Paron. |
The proxy finds a tunnel by the protocol name in the first TLS message. A tunnel announces ptp1.
Paron reads the client address from three headers: X-Forwarded-For, Forwarded and X-Real-IP.
It reads them only when the connection comes from an address in --trusted-l7-proxies.
A header can have a list of addresses, one for each proxy that the request went through. Paron
starts at the end of the list and uses the first address that is not in --trusted-l7-proxies.
Examples
Section titled “Examples”services: nginx: image: nginx:stable-alpine restart: unless-stopped depends_on: - paron-server configs: - source: nginx target: /etc/nginx/nginx.conf volumes: - nginx-acme:/var/cache/nginx/acme ports: - "443:443/tcp" networks: - paron
paron-server-init: image: ghcr.io/nicolabovolato/paron:latest command: ["server", "bootstrap"] volumes: - paron-server-data:/data network_mode: none
paron-server: image: ghcr.io/nicolabovolato/paron:latest command: ["server", "run"] restart: unless-stopped stop_grace_period: 30s depends_on: paron-server-init: condition: service_completed_successfully read_only: true cap_drop: - ALL security_opt: - no-new-privileges:true tmpfs: - /tmp volumes: - paron-server-data:/data # The proxy carries only TCP, so a tunnel uses TLS over TCP. To give tunnels QUIC, publish the # UDP port from Paron itself: # ports: # - "443:8443/udp" networks: - paron environment: PARON_PUBLIC_URL: https://paron.example.com # The network of the proxy. The proxy passes tunnels through with the PROXY protocol header, # and it sends the web UI as HTTP requests with the X-Forwarded-For header. PARON_TRUSTED_PROXIES: 172.30.0.0/24 PARON_TRUSTED_L7_PROXIES: 172.30.0.0/24
networks: paron: ipam: config: - subnet: 172.30.0.0/24
volumes: paron-server-data: nginx-acme:
configs: nginx: content: | load_module modules/ngx_http_acme_module.so;
events {}
stream { upstream tunnels { server paron-server:8443; } upstream web { server 127.0.0.1:8444; }
# A tunnel announces the protocol ptp1. It goes to Paron as it is. map $$ssl_preread_alpn_protocols $$target { ~\bptp1\b tunnels; default web; }
server { listen 443; ssl_preread on; proxy_pass $$target; proxy_protocol on; } }
http { map $$http_upgrade $$connection_upgrade { default upgrade; "" close; }
# nginx gets the certificate from Let's Encrypt and renews it. Set your e-mail address in # "contact", and your domain in "server_name" below. resolver 127.0.0.11; acme_shared_zone zone=acme:1M; acme_issuer letsencrypt { uri https://acme-v02.api.letsencrypt.org/directory; contact admin@example.com; challenge tls-alpn-01; state_path /var/cache/nginx/acme; accept_terms_of_service; }
server { listen 127.0.0.1:8444 ssl proxy_protocol; http2 on; server_name paron.example.com; acme_certificate letsencrypt; ssl_certificate $$acme_certificate; ssl_certificate_key $$acme_certificate_key; ssl_certificate_cache max=2;
location / { # Paron serves HTTPS only, with its own certificate. proxy_pass https://paron-server:8443; proxy_http_version 1.1; proxy_set_header Host $$host; # The terminal is a WebSocket. proxy_set_header Upgrade $$http_upgrade; proxy_set_header Connection $$connection_upgrade; # Paron reads these three headers. Set the real address, and remove the other two. proxy_set_header X-Forwarded-For $$proxy_protocol_addr; proxy_set_header Forwarded ""; proxy_set_header X-Real-IP ""; } } }services: traefik: image: traefik:v3 restart: unless-stopped depends_on: - paron-server command: - --entrypoints.https.address=:443 - --providers.file.filename=/etc/traefik/paron.yml # Traefik gets the certificate from Let's Encrypt. Set your e-mail address here. - --certificatesresolvers.letsencrypt.acme.tlschallenge=true - --certificatesresolvers.letsencrypt.acme.email=admin@example.com - --certificatesresolvers.letsencrypt.acme.storage=/acme/acme.json configs: - source: traefik target: /etc/traefik/paron.yml volumes: - traefik-acme:/acme ports: - "443:443/tcp" networks: - paron
paron-server-init: image: ghcr.io/nicolabovolato/paron:latest command: ["server", "bootstrap"] volumes: - paron-server-data:/data network_mode: none
paron-server: image: ghcr.io/nicolabovolato/paron:latest command: ["server", "run"] restart: unless-stopped stop_grace_period: 30s depends_on: paron-server-init: condition: service_completed_successfully read_only: true cap_drop: - ALL security_opt: - no-new-privileges:true tmpfs: - /tmp volumes: - paron-server-data:/data # The proxy carries only TCP, so a tunnel uses TLS over TCP. To give tunnels QUIC, publish the # UDP port from Paron itself: # ports: # - "443:8443/udp" networks: - paron environment: PARON_PUBLIC_URL: https://paron.example.com # The network of the proxy. The proxy passes tunnels through with the PROXY protocol header, # and it sends the web UI as HTTP requests with the X-Forwarded-For header. PARON_TRUSTED_PROXIES: 172.30.0.0/24 PARON_TRUSTED_L7_PROXIES: 172.30.0.0/24
networks: paron: ipam: config: - subnet: 172.30.0.0/24
volumes: paron-server-data: traefik-acme:
configs: traefik: content: | tcp: routers: # A tunnel announces the protocol ptp1. It goes to Paron as it is. tunnels: entryPoints: - https rule: HostSNI(`*`) && ALPN(`ptp1`) service: tunnels tls: passthrough: true services: tunnels: loadBalancer: servers: - address: paron-server:8443 proxyProtocol: version: 2 http: routers: web: entryPoints: - https # Set your domain here. rule: Host(`paron.example.com`) service: web middlewares: - client-address tls: certResolver: letsencrypt middlewares: # Traefik sets X-Forwarded-For and X-Real-IP. Paron also reads Forwarded, so remove it. client-address: headers: customRequestHeaders: Forwarded: "" services: web: loadBalancer: serversTransport: paron servers: - url: https://paron-server:8443 serversTransports: # Paron serves HTTPS only, with its own certificate. paron: insecureSkipVerify: trueSave the file as docker-compose.yml, set PARON_PUBLIC_URL, and start the services:
docker compose up -dThe UDP port is a comment in these files too, for the same reason as above.
© 2026 Nicola Bovolato. Paron is source-available under the PolyForm Shield License 1.0.0.