The purpose of this guide is to configure a reverse proxy with two main components:
This documentation was tested with Nginx. Other Reverse Proxy or Web server software (HAProxy, Caddy, Apache httpd…) probably work the same way.
These sites were used when configuring our reverse proxy. Many thanks to them. Please refer to these links for more details and configuration examples :
For the first TLS entry point, nginx stream module is used in order to :
Stream directives shoud be at same level than http server directives.
A stream.conf file should be created and called by nginx.conf
/etc/nginx/nginx.conf
include /etc/nginx/stream.conf;
with the following content :
/etc/nginx/stream.conf
stream { map $ssl_preread_server_name $selected_upstream { # all domains and sub domains using backend with TLS passthrough subdomain1.mydomain.org backend_a; subdomain2.mydomain.org backend_a; myotherdomain.info backend_b; # default to local reverse proxy - passthrough to a default RP going to handle ssl offloading for all other domain or redirect with appropriate errror page default default_RP; } upstream backend_a { server 192.168.99.11:443; } upstream backend_b { server 192.168.99.12:443; } upstream default_RP { server 127.0.0.1:8443; } server { listen 192.168.99.1:443; proxy_pass $selected_upstream; proxy_protocol on; ssl_preread on; } }
Here is additionnal information regarding each part of stream.conf configuration.
First, map function is used to redirect domain to defined backend
map $ssl_preread_server_name $selected_upstream { # all domains and sub domains using backend with TLS passthrough mysubdomain1.mydomain.org backend_a; mysubdomain2.mydomain.org backend_a; myotherdomain.info backend_b; # default to local reverse proxy - passthrough to a default RP going to handle ssl offloading for all other domain or redirect with appropriate error page default default_RP; }
Please note :
And then backend are configured accordingly :
upstream backend_a { server 192.168.99.11:443; } upstream backend_b { server 192.168.99.12:443; } upstream default_RP { server 127.0.0.1:8443; }
Note : due to issue to handle ipv6, 127.0.0.1 is used instead of localhost (to avoid nginx error on ::1)
The main section adds parameters to stream directive to reverse proxy to appropriate backend with two important directives:
server { listen 192.168.99.1:443; proxy_pass $selected_upstream; proxy_protocol on; ssl_preread on; }
Not all (sub)domains will be covered by a TLS passthrough. Some apps will be covered by a classical reverse proxy approach, with ssl offloading and Let's Encrypt management, etc…
As a usual nginx usage, each (sub)domain coverage will be handled by a dedicated configuration in sites-available directory. Enabling a vhost is triggered by creating a symbolic link in sites-enabled directory.
This is a classical reverse proxy configuration to a backend (upstream) server. But in order to properly catch the user remote ip and send it to the backend (for security, troubleshooting reasons…) we need to properly listen with proxy protocol and set remote ip in headers.
server { server_name subdomain.otherdomain.eu; location / { include proxy.conf; #common proxy conf proxy_pass https://[upstreamserver]/; #Please replace [upstreamserver] with your backend application server internal ip or internal dns name. # Log real ip from proxy protocol - needed for upstream - backend server to get Real IP proxy_set_header X-Real-IP $proxy_protocol_addr; proxy_set_header X-Forwarded-For $proxy_protocol_addr; # Security, Hardening # https://developer.mozilla.org/fr/docs/Mozilla/Add-ons/WebExtensions/manifest.json/content_security_policy add_header Content-Security-Policy "script-src 'self' 'unsafe-eval' 'unsafe-inline' blob:; object-src 'self'"; } # Manage real ip from proxy protocol to get original client ip set_real_ip_from 127.0.0.1; real_ip_header proxy_protocol; listen [::]:8443 ssl proxy_protocol; # managed by Certbot listen 8443 ssl proxy_protocol; # managed by Certbot ssl_certificate /etc/letsencrypt/live/subdomain.otherdomain.eu/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/subdomain.otherdomain.eu/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot }
: TODO
For backend behind TLS passthrough, a simple http reverse proxy can be set. Purpose of this simple http reverse proxy is to let backend handle redirection to https, and not break let's encrypt challenge using http for example.
server { # a very simple reverse proxy to port 80 : forcing https and redirect will be handle by upstream listen 80 ; listen [::]:80 ; server_name subdomain1.mydomain.org subdomain2.mydomain.org; access_log /var/log/nginx/mydomain.org-access.log; error_log /var/log/nginx/mydomain.org-error.log; location / { # handle real ip, and send request to backend include proxy.conf; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://_REPLACE_WITH_BACKEND_IP_OR_HOST_/; # backend application server internal address } }
For all other application, http 80 port can be handled in the same vhost.
We wimply redirect any http url to https. On server directive :
server { if ($host = subdomain.otherdomain.eu) { return 301 https://$host$request_uri; } listen 80 ; listen [::]:80 ; server_name subdomain.otherdomain.eu; return 404; # managed by Certbot }
This backend is simply listening on http port 80 behind a classical proxy pass reverse proxy. Main custom configuration aims to get remote ip through headers set by reverse proxy upfront :
server { listen 80; listen [::]:80; server_name yunohost.mydomain.test; # Use real ip for http flow Please Replace [REVERSEPROXY_IP] with internal IPV4 of your reverse proxy set_real_ip_from [REVERSEPROXY_IP]; real_ip_header X-Real-IP; #[...] }
Upstream (backend) is behind TLS Passthrough Reverse Proxy. Configuration will be modified to listen on PROXY protocol behind reverseproxy and to catch real browser IP, for security and functional reason. This will log real ip in log files.
server { listen 443 ssl http2 proxy_protocol; listen [::]:443 ssl http2 proxy_protocol; server_name yunohost.mydomain.test; # Manage real ip from proxy protocol to get original client ip # Please Replace [REVERSEPROXY_IP] with internal IPV4 of your reverse proxy. set_real_ip_from [REVERSEPROXY_IP]; real_ip_header proxy_protocol; # Log real ip from proxy protocol - needed for upstream - backend server to get Real IP proxy_set_header X-Real-IP $proxy_protocol_addr; proxy_set_header X-Forwarded-For $proxy_protocol_addr; #[...] }
Backend server behind “classical” reverse proxy can listen on any port, http or https.
Thanks to http headers set by upfront reverse proxy, actual user remote ip is available and can be used for logging.
Here are some configuration examples :
Apache :
# Please Replace [REVERSEPROXY_IP] with internal IPV4 of your reverse proxy. RemoteIPHeader X-Forwarded-For RemoteIPInternalProxy [REVERSEPROXY_IP]/32
: nginx configuration
Setting Yunohost behind a TLS Passthrough Reverse Proxy will let Yunohost handle certificate and keep most of the native configuration quite like behind a direct connexion. Once yunohost domains are configured in Reverse proxy, Yunohost owner can manage yunohost app and certificate independantly.
Nevertheless, some configuration changes are needed :
This configuration comes with some limitations :
We assume that a reverse proxy is configured as described above to proxy to Yunohost. For security purpose, for tracing connexion, configuration is simply modified to get browser real IP with X-Real-IP headers.
First, create a custom snippet. Please Replace [REVERSEPROXY_IP] with internal IPV4 of your reverse proxy.
/etc/nginx/snippets/YunoHost_behind_http_RP.inc
# Configuration for YunoHost behind http proxy # Use real ip for http flow Please Replace [REVERSEPROXY_IP] with internal IPV4 of your reverse proxy set_real_ip_from [REVERSEPROXY_IP]; real_ip_header X-Real-IP;
In all nginx configuration file, include this snippet in server directive on port 80 :
server { listen 80; listen [::]:80; server_name yunohost.mydomain.test *.yunohost.mydomain.test; # YunoHost behind http Reverse Proxy include /etc/nginx/snippets/YunoHost_behind_http_RP.inc; #[...] }
We assume that a reverse proxy is configured as described above to proxy to Yunohost. Configuration will be modified to listen on PROXY protocol behind reverseproxy and to catch real browser IP, for security and functional reason. This will log real ip in log files and let fail2ban correctly monitor the actual client ip.
Unfortunately fail2ban is currently triggering iptables rules. But banning ip in firewall won't work behind the reverse proxy. TODO : Find a way to actually ban remote ip (e.g. in nginx)
Create a custom snippet. Please Replace [YUNOHOST_INTERFACE_IP] with internal IPV4 of YunoHost interface listening on https behind Reverse Proxy. Please Replace [REVERSEPROXY_IP] with internal IPV4 of your reverse proxy.
/etc/nginx/snippets/YunoHost_behind_https_TLSPT_RP.inc
# Configuration Yunohost behind TLS PAssthrough - SNI Forward Reverse Proxy # Add proxy protocol in listen directive for external ipv4 interface only # Please Replace [YUNOHOST_INTERFACE_IP] with internal IPV4 of YunoHost interface listening on https behind Reverse Proxy. listen [YUNOHOST_INTERFACE_IP]:443 ssl http2 proxy_protocol; # Manage real ip from proxy protocol to get original client ip # for interface using proxy_protocol # Please Replace [REVERSEPROXY_IP] with internal IPV4 of your reverse proxy. set_real_ip_from [REVERSEPROXY_IP]; real_ip_header proxy_protocol; # Log real ip from proxy protocol - needed for upstream - backend server to get Real IP proxy_set_header X-Real-IP $proxy_protocol_addr; proxy_set_header X-Forwarded-For $proxy_protocol_addr;
In all nginx configuration file, simply include this snippet in server directive on port 443 :
server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name yunohost.mydomain.test; # YunoHost behind TLS Passthrough SNI Forward Reverse Proxy include /etc/nginx/snippets/YunoHost_behind_https_TLSPT_RP.inc; #[...] }