Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revision | |||
1.selfhost:nginx [2025/03/13 09:29] – [6 The Reverse Proxy concept] willy | 1.selfhost:nginx [Unknown date] (current) – removed - external edit (Unknown date) 127.0.0.1 | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== F) The Reverse Proxy concept ====== | ||
- | |||
- | The use of a **reverse proxy** is the key at the foundation of ensuring security, isolation and flexibility in accessing your self-hosted services. | ||
- | |||
- | A reverse-proxy is a web server that sits in the middle and handles all requests toward your services adding, on top, layers of encryption (HTTPS/ | ||
- | |||
- | The reverse-proxy will take care of handling HTTPS/SSL certificates in one centralized place making it much easier to configure all your services without HTTPS then converting seamlessly all the HTTP traffic to HTTPS. It's much easier to manage all the certificates in one place rather than depending on each service capability to handle HTTPS independently. | ||
- | |||
- | Also, using a well known, solid and proven web server will alleviate the risk that each service might expose a poorly written, non-scalable or worse, internal web server to end users. | ||
- | |||
- | And as a final note, using a reverse-proxy you can organize easily all your services either under one single domain or with sub-domains, | ||
- | |||
- | ===== NGINX ===== | ||
- | |||
- | My choice for a web server in this case is [[https:// | ||
- | * It's much easier than [[https:// | ||
- | * It has more features than [[https:// | ||
- | * It is fully integrated in [[https:// | ||
- | |||
- | In general NGINX is fully featured but still very lightweight and secure HTTP server that shines as reverse-proxy. If you need to add more features, like [[https:// | ||
- | |||
- | ===== Base URLs and sub-domains ===== | ||
- | |||
- | There are two different philosophies on how to host services: serve as a sub-path of a domain, or use sub-domains. I used to like best the // | ||
- | |||
- | Let's assume you have your own domain **mydomain.com** and you want to expose a service called // | ||
- | * As a sub-path: **https:// | ||
- | * As a sub-domain: **https:// | ||
- | |||
- | Here are the main points and drawbacks of each solution. | ||
- | |||
- | As a **sub-path**: | ||
- | * Pros: only one domain needed, no need to create sub-domains | ||
- | * Pros: the service existence is unknown to anybody not authorized | ||
- | * Cons: each service must support Base URL setting (well, not all do!) | ||
- | * Cons: SSO support must be somehow consistent to avoid headaches (well, SSO support is still spotty today!) | ||
- | * Cons: security wise, cookies and CORS can bring unintended vulnerabilities between services, because they all share the same subdomain. | ||
- | * Cons: all services share the same HTTPS certificate. | ||
- | |||
- | As a **sub-domain**: | ||
- | * Pros: any service will work, no need to support Base URL | ||
- | * Pros: each service can have it's own HTTPS certificate | ||
- | * Pros: each service is neatly organized in it's own subdomain | ||
- | * Pros: cookies are not shared between services, and CORS protection works | ||
- | * Cons: exposed to public knowledge (DNS records are public) that the service exist | ||
- | * Cons: also public knowledge because there are services indexing all existing certificates. | ||
- | |||
- | __Note:__ you can create // | ||
- | |||
- | To make a story short, i go with subdomains for well separated services, while going with sub-paths when sharing stuff that kind belongs together. Also, a deciding factor is whether the selected services do support SSO properly or not. | ||
- | |||
- | |||
- | ===== Reverse Proxy propagation ===== | ||
- | |||
- | The reverse proxy is installed on the local server, i assume your local server is reachable from remote (see [[networking: | ||
- | |||
- | The reverse proxy will need to be accessible to both the internal users and the external users. You could setup two different proxies, but i prefer to have only one listening to both worlds. I will assume that there might be differences between internal and external users in terms of authentication or service availability. The underlying idea is that you will have your reverse proxy listening to different ports: one for internal access and one for external access. | ||
- | |||
- | The setup i am describing uses three different ports: | ||
- | * Port 80: both to local and remote, will just be a redirect to HTTPS | ||
- | * Port 443: standard HTTPS for **internal** access | ||
- | * Port 8443: HTTPS for **external** access | ||
- | |||
- | **Note:** for Let's Encrypt CertBot to work properly you **need** to redirect **both** port 80 and 443 from your external server to your internal server. CertBot will shutdown your NGINX and spin a custom NGINX server that you cannot tweak so it's critical that your SSH tunnels are properly forwarding ports 80 and 443 from the external server to the internal one, or it will not work. | ||
- | |||
- | |||
- | ===== Installing NGINX ===== | ||
- | |||
- | NGINX installation on the home server is pretty straightforward, | ||
- | * // | ||
- | * // | ||
- | * //sub// is used to allow substitutions inside the pages proxied, to fix web applications that don't play well with reverse-proxies | ||
- | * //gunzip// is used to unzip the requests and let the //sub// module works also on compressed requests | ||
- | * //realip// is needed by SSO like authelia | ||
- | |||
- | While NGINX support WebDAV, i strongly suggest you __dont__ enable it as you will not be using it. NGINX WebDAV support is lacking and not really recomended. | ||
- | |||
- | So create the file **/ | ||
- | <file - nginx> | ||
- | app-misc/ | ||
- | www-servers/ | ||
- | </ | ||
- | |||
- | Note: you might want to tweak the second line to your needs, see the [[https:// | ||
- | |||
- | Now install nginx: | ||
- | <code bash> | ||
- | emerge -v nginx | ||
- | </ | ||
- | |||
- | You can start it after you have configured it. | ||
- | |||
- | |||
- | ===== NGINX main configuration ===== | ||
- | |||
- | There are many ways to write nice NGINX config files, i will show you mine which i find quite effective, organized and simple. It make use of the //import// directive and splits the configuration to at least one file per service and one file per sub-domain. | ||
- | |||
- | Assumptions: | ||
- | * Your domain is **mydomain.com**, | ||
- | * Your service X is reachable under **https:// | ||
- | * Your service Y is reachable under **https:// | ||
- | * All HTTP traffic is redirected to HTTPS | ||
- | * You have a single Let's Encrypt SSL certificate which covers all the subdomains of your domain (either a wildcard or a comulative cert it's up to you) | ||
- | * You might have more than one main domain | ||
- | |||
- | The top-level **mydomain.com** will have it's own folder, then you will create a set of sub-folders stemming from the main domain, one folder for each sub-domains, | ||
- | |||
- | So you will need the following files: | ||
- | * **/ | ||
- | * **/ | ||
- | * **/ | ||
- | * **/ | ||
- | * **/ | ||
- | * plus any other SSO specific config files. | ||
- | |||
- | The **certbot.conf** file will be created later on, the specific SSO config files are described in the [[selfhost: | ||
- | |||
- | |||
- | ==== Top-level configuration | ||
- | |||
- | So, here is the content for the main **/ | ||
- | <file - nginx.conf> | ||
- | user nginx nginx; | ||
- | |||
- | error_log / | ||
- | |||
- | events { | ||
- | worker_connections 1024; | ||
- | use epoll; | ||
- | } | ||
- | |||
- | http { | ||
- | include / | ||
- | # Unknown stuff is considered to be binaries | ||
- | default_type application/ | ||
- | # Set a reasonably informing log format | ||
- | log_format main | ||
- | ' | ||
- | '" | ||
- | '" | ||
- | '" | ||
- | # Improve file upload to client by avoiding userspace copying | ||
- | tcp_nopush on; | ||
- | sendfile on; | ||
- | # Indexes are html by default | ||
- | index index.html; | ||
- | |||
- | # General catch-all for HTTPS redirection, | ||
- | server { | ||
- | listen 80 default_server; | ||
- | return 301 https:// | ||
- | } | ||
- | |||
- | # Using Authelia SSO can lead to longer headers, better increase buffers | ||
- | proxy_headers_hash_max_size 512; | ||
- | proxy_headers_hash_bucket_size 128; | ||
- | |||
- | # Add domains here (only the main config file for each domain!) | ||
- | include com.mydomain/ | ||
- | | ||
- | # This is for SSL and needs to be included only once for all the domains | ||
- | include / | ||
- | } | ||
- | </ | ||
- | |||
- | This will set your defaults for every service and site served by this reverse proxy, then will load the // | ||
- | |||
- | |||
- | ==== mydomain.com configuration | ||
- | |||
- | Now, for the specific **mydomain.com**, | ||
- | <file - mydomain.conf> | ||
- | |||
- | access_log / | ||
- | error_log / | ||
- | |||
- | # simple catch-all server for the domain | ||
- | server { | ||
- | # You might want to specify also the internal | ||
- | server_name mydomain.com; | ||
- | # Port for users from outside | ||
- | listen 8443 ssl; | ||
- | # Port for users from inside | ||
- | listen 443 ssl; | ||
- | http2 on; | ||
- | |||
- | # unauthenticated static landing page (maybe a "get off my lawn" GIF...) | ||
- | location / { | ||
- | root / | ||
- | } | ||
- | |||
- | # include all sub-paths for mydomain.com: | ||
- | | ||
- | |||
- | # include HTTPS certs stuff: | ||
- | | ||
- | } | ||
- | |||
- | # include all sub-domains entry points: | ||
- | include com.mydomain/ | ||
- | </ | ||
- | |||
- | This will create the basic setup for your base domain name. I have assumed you want a static landing page, but you might put a // | ||
- | |||
- | |||
- | ==== sub-domains configuration | ||
- | |||
- | It should be clear now that each sub-domain will have it's own sub-folder and contain at least one (or more) configuration files inside for each sub-path, like the one for serviceY. | ||
- | |||
- | I will assume that // | ||
- | <file - y.conf> | ||
- | server { | ||
- | | ||
- | | ||
- | | ||
- | | ||
- | | ||
- | | ||
- | # | ||
- | | ||
- | } | ||
- | # include HTTPS certs stuff: | ||
- | | ||
- | } | ||
- | </ | ||
- | |||
- | I suggest you split all sub-paths for each sub-domain in a separate config file and //include// them inside the //server// block, like i did above for // | ||
- | |||
- | |||
- | ==== Differentiate between Internal or External access for services ==== | ||
- | |||
- | In my setup i have some differences when a service is accessed from //within// the home network, or from //outside// the home network. | ||
- | |||
- | The key point is that // | ||
- | |||
- | So, for example, a service _only_ available inside the home network will have something like: | ||
- | < | ||
- | server { | ||
- | server_name internal_only.mydomain.com; | ||
- | listen 443 ssl; # internal access | ||
- | http2 on; | ||
- | access_log / | ||
- | error_log / | ||
- | location / { | ||
- | #Generic proxy pass to proxied service | ||
- | proxy_pass http:// | ||
- | } | ||
- | # include HTTPS certs stuff: | ||
- | | ||
- | } | ||
- | </ | ||
- | |||
- | While a service that can be accessed both from internal and external: | ||
- | < | ||
- | server { | ||
- | server_name serviceZ.mydomain.com; | ||
- | listen 8443 ssl; # external access | ||
- | listen 443 ssl; # internal access | ||
- | http2 on; | ||
- | access_log / | ||
- | error_log / | ||
- | location / { | ||
- | #Generic proxy pass to proxied service | ||
- | proxy_pass http:// | ||
- | } | ||
- | # include HTTPS certs stuff: | ||
- | | ||
- | } | ||
- | </ | ||
- | |||
- | A service where you want to differentiate between internal and external, for example adding SSO authentication only for external access: | ||
- | < | ||
- | server { | ||
- | server_name serviceZ.mydomain.com; | ||
- | listen 443 ssl; # internal access | ||
- | http2 on; | ||
- | access_log / | ||
- | error_log / | ||
- | location / { | ||
- | #Generic proxy pass to proxied service | ||
- | proxy_pass http:// | ||
- | } | ||
- | # include HTTPS certs stuff: | ||
- | | ||
- | } | ||
- | server { | ||
- | server_name serviceZ.mydomain.com; | ||
- | listen 8443 ssl; # external access | ||
- | http2 on; | ||
- | [[[ put here your SSO lines ]]] | ||
- | access_log / | ||
- | error_log / | ||
- | location / { | ||
- | #Generic proxy pass to proxied service | ||
- | proxy_pass http:// | ||
- | } | ||
- | # include HTTPS certs stuff: | ||
- | | ||
- | } | ||
- | </ | ||
- | |||
- | In this case, you can even optimize more by moving the **location** lines, which are identical, inside another file that you __include__ twice. Better to avoid redundancy! | ||
- | |||
- | Of course, refer to the [[selfhost: | ||
- | |||
- | |||
- | ===== Generate SSL certificates for HTTPS ===== | ||
- | |||
- | Nowadays HTTPS is a must for many reasons, including privacy and security. I assume this is a mandatory requirement. A lot of services will not even work without HTTPS. | ||
- | |||
- | Enabling HTTPS requires the generation of valid SSL certificates for your domain(s). You can do that with self-signed certificates but that will still flag as insecure on your browser and some client apps might even not work properly. A better solution is to use the [[https:// | ||
- | |||
- | How does it work? | ||
- | |||
- | first of all: | ||
- | - You ask Let's Encrypt to create a certificate for each one of your sub-domains (automated by CertBot) | ||
- | - You setup the certificate (automated by CertBot) | ||
- | - You renew periodically the certificate (automated by CertBot) | ||
- | |||
- | Then: | ||
- | - You connect with browser to **https:// | ||
- | - Your server provide the certificate | ||
- | - Your browser verify that the certificate is valid against the Let's Encrypt Root Certificate | ||
- | - You are good to go! | ||
- | |||
- | Using // | ||
- | |||
- | Luckly, Let's Encrypt provides a neat software called **CertBot** that can automate all the steps for the major web servers, including NGINX. CertBot will send requests to Let's Encrypt, spin up an NGINX server for you and store the certificate. The only thing you need to do is including the proper config file into NGINX and restart it. | ||
- | |||
- | Install CertBot and the NGINX plugin: | ||
- | <code bash> | ||
- | > emerge -v certbot-nginx certbot | ||
- | </ | ||
- | |||
- | This will pull in all the required software to perform the exchange with Let's Encrypt infrastructure. At this point you only need to run Certbot to generate a certificate for your external domain name: | ||
- | |||
- | <code bash> | ||
- | > certbot --nginx certonly -d mydomain.com -d y.mydomain.com -d xxxx | ||
- | </ | ||
- | |||
- | Now, you **must** generate certificates that chains toghether all the subdomains you use. This means that if you add, later on, another sub-domain to host a new service you will **need to** re-run the above //certbot// command adding //-d newsubdomain.mydomain.com// | ||
- | |||
- | Put this content into your **/ | ||
- | <file - certbot.conf> | ||
- | ssl_certificate / | ||
- | ssl_certificate_key / | ||
- | include / | ||
- | ssl_dhparam / | ||
- | </ | ||
- | |||
- | Of course, adapt the paths for your specific case. | ||
- | |||
- | Let's Encrypt certificates last 90 days, then they need to be renewed. This is automated by CertBot but you need to call it periodically. You can use crontab for this. Edit root crontab: | ||
- | <code bash> | ||
- | crontab -e | ||
- | </ | ||
- | |||
- | and write the following lines: | ||
- | <code crontab> | ||
- | 47 5 * * * certbot renew &>> | ||
- | 31 16 * * * certbot renew &>> | ||
- | </ | ||
- | |||
- | there you go! | ||
- | |||
- | You can now start your nginx server: | ||
- | |||
- | <code bash> | ||
- | rc-update add nginx default | ||
- | / | ||
- | </ | ||
- | |||
- | |||
- | |||
- | ==== Quick and dirty script for new subdomains ==== | ||
- | |||
- | When you need to **add** a new subbomain to your certificate, | ||
- | <file - certbot_script.sh> | ||
- | #!/bin/bash | ||
- | |||
- | DOMAINS=" | ||
- | |||
- | domains= | ||
- | for i in ${DOMAINS} | ||
- | do | ||
- | domains=" | ||
- | done | ||
- | |||
- | certbot certonly --expand --nginx ${domains} | ||
- | </ | ||
- | |||
- | So __FIRST__ you **update** the script adding the new domain at the end of the DOMAINS line, then you run the script and restart your NGINX. | ||
- | |||
- | |||
- | ===== Enable CGI support with NGINX ===== | ||
- | |||
- | To be able to run system scripts and, in general, [[https:// | ||
- | |||
- | For using CGI directly with NGINX (another option could be to run Apache or another web server in addition, but why?) you can install and setup [[https:// | ||
- | <code bash> | ||
- | emerge www-misc/ | ||
- | </ | ||
- | |||
- | Spawn-fcgi allows you to run one instance of fcgiwrap for each service you need to run. This is an excellent approach to keep services separated and each one in it's own user. | ||
- | |||
- | Since you want to run // | ||
- | * Setup your // | ||
- | * Create a start script in **/ | ||
- | |||
- | The contents of the config file sohuld be: | ||
- | <file - spawn-fcgi.fcgiwrap> | ||
- | # The " | ||
- | FCGI_SOCKET=/ | ||
- | FCGI_PORT= | ||
- | # The -f send stderr to nginx log | ||
- | FCGI_PROGRAM="/ | ||
- | FCGI_USER=nginx | ||
- | FCGI_GROUP=nginx | ||
- | FCGI_EXTRA_OPTIONS=" | ||
- | ALLOWED_ENV=" | ||
- | </ | ||
- | |||
- | And to do all the above: | ||
- | <code bash> | ||
- | cp / | ||
- | ln -s / | ||
- | rc-update add spawn-fcgi.fcgiwrap default | ||
- | / | ||
- | </ | ||
- | |||
- | Then enable it in your NGINX config by adding the following directives | ||
- | <file - cgi.conf> | ||
- | | ||
- | fastcgi_param DOCUMENT_ROOT / | ||
- | fastcgi_param SCRIPT_NAME | ||
- | fastcgi_pass unix:/ | ||
- | } | ||
- | </ | ||
- | |||
- | |||
- | ===== In short: add & enable a service ===== | ||
- | |||
- | Assuming you want to add a new service to your Reverse Proxy and the relative configuration has been written in **service.conf** file, you need to **include** it inside your URL's configuration file. If the service needs to be under **https:// | ||
- | < | ||
- | include " | ||
- | </ | ||
- | |||
- | and then restart nginx: | ||
- | <code bash> | ||
- | / | ||
- | </ | ||