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 encyrption (HTTPS/SSL), authentication, load-balancing and security. If your services are properly written (not too many, but the best ones are) they will accept your reverse-proxy authentication directly without even the need to create users for each service, in this case your reverse-proxy will also be your SSO (Single Sign On) solution.

The reverse-proxy will also take care of handling HTTP/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 one certificate 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 under one single domain. There are limitations, mostly due to poorly written services or peculiar protocols, that might require independent sub-domains, but i will show you how to handle also these cases easily with the reverse-proxy.

NGINX

My choice for a web server in this case is NGINX between the many available as Open Source because:

  • It's much easier than Apache to setup as a reverse-proxy, also less resource hungry.
  • It has more features than Caddy
  • It is fully integrated in Let's Encrypt SSL infrastructure / CertBot script

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 PHP support or FastCGI, NGINX will support you without the need for an additional service on your server.

Base URLs and sub-domains

There are two different philosophies on how to host services. The one i like best, i think it's simpler and more elegant, is to use one single domain and expose each service in it's own sub-path or better call it Base URL. The alternative is to allocate one sub-domain for each service.

Let's assume you have your own domain mydomain.com and you want to expose a service called jellyfin (a well known media-server). You can expose it:

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 (not always possible)
  • Pros: easy to organize services in virtual sub-folders
  • Pros: the service existence is unknown to anybody not authorized
  • Cons: each service must support Base URL setting

As a sub-domain:

  • Pros: any service will work, no need to support Base URL
  • Cons: require additional certificates for HTTPS/SSL for each sub-domain
  • Cons: cannot easily organize together
  • 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 wildcard certificates that will match any subdomain, but there are drawbacks to this and it's not a good idea, security wise. You can still mitigate the one certificate per subdomain by adding each subdomain to the same certificate of course, but you will still need to extend your certificate each time you add a subdomain.

I prefer the sub-path whenever possible, but in some cases you will be forced to use sub-domains. And what if you cannot spin your sub-domains? Well, forget those services that require a subdomain.

Then using sub-paths, the use of a reverse-proxy like NGINX allows you a little bit of flexibility because you can, to an extend, perform rewrite operations on URL's and also on the response to the browser, but this all come to a cost in processing power and, moreover, it's not always feasible. In general for sub-paths to work properly it has to be supported by the service.

Authentication

Having a strong layer of authentication is mandatory for self-hosted services that are exposed to the internet. We talking about authentication it's important to remember that is has a double meaning: to recognize a user rather than another use, and to restrict access to your service based on who the user is.

A few assumptions: self-hosting for home access, which means a limited and trusted list of users which doesn't change often in time. Security is important, but ease of use is also important. Simplicity of user management is also important.

There are a few key points that i want to stress on authentication:

  • 2FA (Two Factor Authentication) will not be considered
  • You want to create users only once, as much as possible.
  • Only selected services will need to differentiate between users
  • Most services will not need to know who is accessing them
  • From outside, all services must require authentication
  • From inside, authentication is required only where a specific user makes a difference
  • Avoid double authentication when possible

For example, a media server will need to know who is connecting to show your preferred shows and your “resume from here…” movies. The printer control page instead should be accessible by anyone inside home.

Authentication will be required when connecting from outside, always, while will be needed only for selected services from inside.

The most simple and effective approach is to enable the PAM Authentication plugin of NGINX and connect your reverse-proxy authentication to your server user management. So that by adding a new user to your server, that will be automagically added to your services, or at least the ones that can link to reverse-proxy authentication.

You have the following combinations:

  • Services that do not require to differentiate the user
  • Services that needs to know who is connecting, and can get this info from the reverse-proxy
  • Services that needs to know who is connecting, and cannot get this info from the reverse-proxy

You will be able to play with the PAM authentication module of NGINX on a per-service base to achieve this.

The general rule is as follow:

Service From inside From outside
do not require authentication auth not required use PAM auth
Require auth, can use reverse-proxy auth use PAM auth use PAM auth
Require auth, cannot use reverse-proxy auth use service auth use service auth

Using PAM Auth on services that cannot understand reverse-proxy auth is great way to increase security as others will not even be able to reach your service, but will require the users to perform the authentication twice and might cause some mobile apps to fail.

Please note that for services that cannot use reverse-proxy auth you will need to create users.

There is a more complex solution which is using something like Authelia or Authentik which support 2FA and OAuth, but again whether your services will support it or not is hit-and-miss, and for my needs is simply too much.

Reverse Proxy propagation to external world

The reverse proxy is installed on the local server, you should have already guessed that remote access is performed using the SSH tunnelling described in the specific page. The underlying idea is that you will have your reverse proxy listening to different ports, and these ports will be forwarded to your external server using the SSH tunnels. Differentiating the ports is required to be able to apply PAM authentication depending on where your user connects from.

The setup i am proposing 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, no PAM authentication
  • Port 8443: HTTPS with PAM authentication for external access

Note: for Let's Encrypt CertBot to work properly you need to redirect 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, but you need to enable one specific authentication module, the pam authentication module, because that will link NGINX authentication to your home server users directly, without the need to create more users and passwords. If you prefer to use a different authentication, like basic_auth, i leave this out to you.

So create the file /etc/portage/package.use/nginx with the following lines:

nginx
app-misc/mime-types nginx
www-servers/nginx NGINX_MODULES_HTTP: auth_pam gunzip sub

The gunzip and sub modules might be useful to support URL rewrite and such.

Note: you might want to tweak the second line to your needs, see the flags for nginx and adapt.

A brief explanation of the above USE flags:

  • auth_pam is used to enable PAM based authentication
  • 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

Now install nginx:

 > emerge -v nginx

NGINX pam_auth

I think it's nice that with NGINX you can authenticate your users directly with your home server users. This means you don't need to add a second set of users, and that the users will only need one password, and no sync is required between HTTP users and server users. This is achieved using the pam_auth module on Linux. You have already built nginx with pam_auth support, but you need to configure it.

Create the file /etc/pam.d/nginx with these lines:

nginx
auth required pam_unix.so
account required pam_unix.so

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:

You will need the following files:

  • /etc/nginx/nginx.conf: main config file, entry point.
  • /etc/nginx/com.mydomain/: folder to store all configs related to mydomain.com
  • /etc/nginx/com.mydomain/certbot.conf: SSL certificates configuration for mydomain.com
  • /etc/nginx/com.mydomain/mydomain.conf: global config for mydomain.com
  • /etc/nginx/com.mydomain/y/: folder to store all configs related to y.mydomain.com
  • /etc/nginx/com.mydomain/y/y.conf: config for serviceY on y.mydomain.com
  • /etc/nginx/com.mydomain/serviceX.conf: config for serviceX on mydomain.com

The certbot.conf file will be created later on.

Note that when multiple services are hosted on the same domain (like serviceX on mydomain.com) for clarity i prefer to split them into separated config files.

So, here is the content for the /etc/nginx/nginx.conf:

nginx.conf
user nginx nginx;

error_log /var/log/nginx/error_log info;

events {
        worker_connections 1024;
        use epoll;
}

http {
        include /etc/nginx/mime.types;
        # Unknown stuff is considered to be binaries
        default_type application/octet-stream;
        # Set a reasonably informing log format
        log_format main
                '$remote_addr - $remote_user [$time_local] '
                '"$request" $status $bytes_sent '
                '"$http_referer" "$http_user_agent" '
                '"$gzip_ratio"';
        # 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, we don't like serving plain HTTP
        server {
                listen 80 default_server;
                return 301 https://$host$request_uri;
        }

        # Add domains here (only the main config file for each subdomain!)
        include com.mydomain/mydomain.conf;
        include com.mydomain/y/y.conf;
}

I will show you come raw example for the mydomain.conf and y.conf, detailed examples for each service will be described within each service description page.

The following mydomain.conf is required when you need to differentiate on PAM authentication between internal and external access. You will need to create two different server blocks with slightly different configuration (please do not use the 'if' statement here as duplicating the server block is the preferred way by NGINX):

mydomain.conf
# Manage tunnels request from external server
# HTTPS listen on a custom port (8443) because this is authenticated
server {
        server_name 10.0.0.1 mydomain.com;
        listen 8443 ssl;
        access_log /var/log/nginx/mydomain.com_access_log main;
        error_log /var/log/nginx/mydomain.com_error_log info;
        root /data/web/htdocs;
        auth_pam "Home";
        auth_pam_service_name "nginx";
        include "com.mydomain/serviceX.conf";
        include com.mydomain/certbot.conf;
}

# Manage direct request inside home network
# It's identical to the remote one, but it has no authentication
# HTTPS on port 443 for direct local connections
server {
        server_name 10.0.0.1 mydomain.com;
        listen 8443 ssl;
        access_log /var/log/nginx/mydomain.com_access_log main;
        error_log /var/log/nginx/mydomain.com_error_log info;
        root /data/web/htdocs;
        include "com.mydomain/serviceX.conf";
        include com.mydomain/certbot.conf;
}

In this case i assume you have more than one service, so i split the service config in separated files (serviceX.conf), imagine to have one for each service.

Instead, i will assume that serviceY perform it's own authentication and cannot use reverse-proxy auth, so PAM auth is not needed:

y.conf
server {
        server_name y.mydomain.com;
        listen 8443 ssl; # external access
        listen 443 ssl; # internal access
        # Enable these two lines to use PAM auth
        #auth_pam "Home";
        #auth_pam_service_name "nginx";
        access_log /var/log/nginx/y.mydomain.com_access_log main;
        error_log /var/log/nginx/y.mydomain.com_error_log info;
        location / {
                #Generic proxy pass to proxied service
                proxy_pass http://127.0.0.1:8000;
        }
        include com.mydomain/certbot.conf;
}

Of course, if instead your service do require authentication but can use reverse-proxy auth, uncomment the two auth_pam lines and there you go.

Again, detailed configurations will be provided for each service.

Generate SSL certificates for HTTPS

Nowadays HTTPS is a must for many reasons, including privacy and security. I assume this is a mandatory requirement.

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 Let's Encrypt certification authority which is an open-source, public and free Certificate Authority that let's you generate and manage your certificates.

How does it work?

first of all:

  1. You ask Let's Encrypt to create a certificate for each one of your sub-domains (automated by CertBot)
  2. You setup the certificate (automated by CertBot)
  3. You renew periodically the certificate (automated by CertBot)

Then:

  1. You connect with browser to https://mydomain.com
  2. Your server provide the certificate
  3. Your browser verify that the certificate is valid against the Let's Encrypt Root Certificate
  4. You are good to go!

Using self-signed certificates works too, but since for the browser to validate the certificate needs to already know the associated Certificate Authority, the site will still appear as untrusted. Since Let's Encrypt is A nonprofit Certificate Authority providing TLS certificates with the mission to provide everybody with security and trust, there is no reason not to use it.

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:

 > 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:

 > 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. And do not forget all the older ones! Luckly, domain names can be chained to on single certificate, so you do not have to edit your NGINX config ever again for CertBot to work.

This is also why using sub-paths is simpler: you do not have to extend your certificate for a new service.

Put this content into your /etc/nginx/com.mydomain/certbot.conf:

certbot.conf
ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

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:

 > crontab -e

and write the following lines:

47 5 * * * certbot renew  &>> /var/log/certbot.log
31 16 * * * certbot renew  &>> /var/log/certbot.log

there you go!

You can now start your nginx server:

 > rc-update add nginx default
 > /etc/init.d/nginx start

Enable CGI support with NGINX

To be able to run system scripts and, in general, CGIs on NGINX you need to do some additional configuration. NGINX is not capable of running CGI scripts at all. It has only support for FastCGI protocol, which is quite different and not directly compatible with standard CGI.

For using CGI directly with NGGINX (another option coule be run Apache or another web server) you can install and setup fcgiwrap and it's companion spawn package:

emerge www-misc/fcgiwrap www-servers/spawn-fcgi

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 fcgiwrap set up like this:

  • Setup your spawn-fcgi config file in /etc/conf.d/spawn-fcgi.fcgiwrap
  • Create a start script in /etc/init.d/spawn-fcgi.my-cgi.

The contents of the config file sohuld be:

spawn-fcgi.fcgiwrap
# The "-1" is added on my system, check on your YMMV!
FCGI_SOCKET=/var/run/fcgiwrap.sock-1
FCGI_PORT=
# The -f send stderr to nginx log
FCGI_PROGRAM="/usr/sbin/fcgiwrap -f"
FCGI_USER=nginx
FCGI_GROUP=nginx
FCGI_EXTRA_OPTIONS="-M 0700"
ALLOWED_ENV="PATH"

And to do all the above:

cp /etc/conf.d/spawn-fcgi /etc/conf.d/spawn-fcgi.fcgiwrap
 ln -s /etc/init.d/spawn-fcgi /etc/init.d/spawn-fcgi.fcgiwrap
rc-update add spawn-fcgi.fcgiwrap default
/etc/init.d/spawn-fcgi.fcgiwrap start

Then enable it in your NGINX config by adding the following directives

cgi.conf
       location /my_cgi {
            fastcgi_param DOCUMENT_ROOT /path/to/gci/executable/folder/;
            fastcgi_param SCRIPT_NAME   my_cgi;
            fastcgi_pass unix:/var/run/fcgiwrap.sock;
       }

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://mydomain.com you will need to add it like:

include "com.mydomain/service.conf";

and then restart nginx:

/etc/init.d/nginx restart