Enabling https access on Nginx reverse proxy - s4/7



first post: Cheap Home LAN Playground Using Docker

This post is the fourth post in the series to show how I play with different services on my LAN using Docker. I have added the second Jupyter Notebook container and the third service, ghost, behind the reverse proxy in the previous post. Let me clean them up (remove the concerned files) and focus on further reverse proxy configuration. I will list all the files that should be there at the end of this post including what's newly added.

Once the clean up is done, I will work to enable https on reverse proxy access. The diagram below is what things will look like. After this post, my access to Jupyter Notebook will be on https, and so as my access to the other services I will add behind the reverse proxy later.

Cleaning up examples used in the previous post

I have added the second Jupyter Notebook and Ghost just for the sake of adding more examples on using Nginx reverse proxy along with DNS records. Let's stop them.

First, stop the two Jupyter Notebook containers by running docker compose down at $HOME/mylan/jupyter. Secondly, update the docker-compose.yml file by removing "jupyter2" service section. Now I can run it again with docker compose up -d to run just one jupyter container.

Here is how docker-compose.yml should look like, with "jupyter2" service section removed completely.

    container_name: jupyter
    image: jupyter/base-notebook:notebook-6.5.1
    user: root
      - "8888:8888"
    command: "start-notebook.sh --ServerApp.password='' --ServerApp.token='' --ip= --no-browser"
      - "CHOWN_EXTRA=/home/jovyan"
      - type: volume
        source: jupyter_volume
        target: /home/jovyan
          nocopy: true

  jupyter_volume: {}

Thirdly, let's just shutdown ghost by moving to that directory $HOME/mylan/ghost and run docker compose down if it is still running.

And lastly, let me remove or rename the Nginx server configuration files for "jupyter2" and "blog". Here I am renaming from jupyter2.conf to jupyter2.conf.bak, and likewise for another one, blog.conf.bak. I can restart the container by docker compose restart at $HOME/mylan/rp, and now the configuration files ending with .bak are mounted all together with the other .conf file, but not those server configurations are not loaded. You can confirm the config by running docker exec rp nginx -T.

So now some configs were reverted and containers shutdown, here is how my directories look like.

$ cd $HOME/mylan
$ tree
 | |-docker-compose.yml
 | |-docker-compose.yml
 | |-docker-compose.yml
 | |-conf.d
 | | |-jupyter.conf
 | | |-blog.conf.bak
 | | |-jupyter2.conf.bak
 | |-docker-compose.yml
 | |-config
 | | |-a-records.conf

## btw here I am actually using "find" and "sed" and making it alias
$ grep tree ~/.bash_aliases
alias tree='find . -not -path "*/\.git/*"| sed -e "s/[^-][^\/]*\// |/g" -e "s/|\([^ ]\)/|-\1/"'

Preparation for https access

Things I need to configure Nginx reverse proxy server to handle https are the private key and certificates. If you do have your own domain, it's the best to go ahead and generate certificates/key for it. I actually do have a domain and generated certificates/key through let's encrypt service.

Private CA and self-signed certificate

Let me stick to this private domain, mylan.local, in this series. In this case, this will be the steps to prepare files I will use on my reverse proxy server:

  1. generate a private key and certificate for my private CA, certificate authority
  2. generate a CSR along with the private key
  3. sign the CSR using CA certificate/key to generate signed certificate
  4. generate a DH parameter file

I will work on a separate directory $HOME/mylan/openssl to generate necessary files.

cd $HOME/mylan
mkdir openssl
cd openssl

Now let me generate the private key/certificate for the CA.

# this will generate two files, crt and key
openssl req -x509 \
  -sha256 -days 3650 \
  -nodes \
  -newkey rsa:2048 \
  -subj "/CN=ca.mylan.local" \
  -keyout rootCA.key -out rootCA.crt

# here is the result
$ ls
rootCA.crt  rootCA.key

Next, generate a CSR for the domain *.mylan.local along with the private key.

# generate a CSR for "*.mylan.local"
# along with the private key for this CSR/certificate
openssl req -new \
  -newkey rsa:2048 -keyout tls.key -nodes \
  -out tls.csr \
  -subj "/CN=*.mylan.local"

# again, the result
$ ls
rootCA.crt  rootCA.key  tls.csr  tls.key

Next, prepare an openssl configuration file cert.conf that will be used when signing the CSR tls.csr to generate the signed certificate.

Here is the content of cert.conf.

keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = *.mylan.local

Finally, sign for the CSR to generate the certificate for *.mylan.local. Check the content of the signed certificate by openssl x509 -in tls.crt -text -noout.

openssl x509 -req \
  -in tls.csr \
  -CA rootCA.crt -CAkey rootCA.key \
  -CAcreateserial -out tls.crt \
  -days 365 \
  -sha256 -extfile cert.conf

# to check the content of the signed certificate file
openssl x509 -in tls.crt -text -noout

Additionally, I will generate DH parameter file here.

# generate DH parameter file as dhparam.pem
openssl dhparam -out dhparam.pem 2048

# the end result
$ ls
cert.conf  dhparam.pem  rootCA.crt  rootCA.key  rootCA.srl  tls.crt  tls.csr  tls.key

Configuring https

Let me go back to $HOME/mylan/rp and start setting up https.

I will prepare a directory tls to store files for https access. I will copy the files generated here.

cd $HOME/mylan/rp
mkdir tls
cd tls
cp $HOME/mylan/openssl/tls.key .
cp $HOME/mylan/openssl/tls.crt .
cp $HOME/mylan/openssl/dhparam.pem .

Let me also add Nginx tls.conf file here. I am planning to mount this new tls directory at /etc/nginx/tls, and ssl_dhparam, ssl_certificate, and ssl_certificate_key are set accordingly to correctly point to these certificate/key files prepared.

# dhparam
ssl_dhparam /etc/nginx/tls/dhparam.pem;

# options
ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;

# cert/key
ssl_certificate /etc/nginx/tls/tls.crt;
ssl_certificate_key /etc/nginx/tls/tls.key;

I update docker-compose.yml at $HOME/mylan/rp to let rp additionally mount this tls directory. Here I also update the ports from 80 to 443.

    container_name: rp
    image: nginx:1.23.2
      - "443:443"
      - ./conf.d:/etc/nginx/conf.d
      - ./tls:/etc/nginx/tls

I then update the reverse proxy config file for jupyter access, $HOME/mylan/rp/conf.d/jupyter.conf. I am including the tls.conf file and changing listen from 80 to 443.

server {
    listen 443 ssl http2;
    server_name jupyter.mylan.local;

    # docker resolver
    resolver valid=30s;

    # tls
    include /etc/nginx/tls/tls.conf;

    location / {
        set $upstream;
        proxy_pass http://$upstream;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;

Now restart the rp docker compose and I can access https://jupyter.mylan.local to access the service.

Adding root CA certificate on my client machine

I believe your web browser is showing you the warning message when you start accessing using https. As the message already may have told you, the browser does not trust the certificate used by this server. Copy rootCA.crt file to your client machine, ask Google to find out how to add root CA certificate on your system, and once added your browser should no longer show warnings when accessing your services configured to use this self-signed certificate generated.

Files I have so far

As a closing of this post, let me go through the directories and files I have. Below is the output of find . -not -path "*/\.git/*"| sed -e "s/[^-][^\/]*\// |/g" -e "s/|\([^ ]\)/|-\1/" executed at $HOME/mylan, and my brief notes on each file. You might have some more files ending with .bak under rp/conf.d if you have renamed the server config files for other services covered in this series so far.

$ tree

 |-jupyter  # directory for jupyter
 | |-docker-compose.yml  # running Jupyter Notebook, listening on 8888

 |-openssl  # directory for certificate related files
 | |-rootCA.crt  # root CA certificate
 | |-dhparam.pem  # DH parameter file to use on web server
 | |-tls.key  # private key for the server certificate
 | |-rootCA.srl  # root CA serial, generated when signing CSR
 | |-cert.conf  # configuration for root CA to sign CSR
 | |-tls.crt  # signed server certificate
 | |-rootCA.key  # private key of root CA
 | |-tls.csr  # CSR presented to root CA to generate signed server certificate

 |-rp  # directory for reverse proxy server
 | |-docker-compose.yml  # running Nginx, listening on 443
 | |-conf.d
 | | |-jupyter.conf  # Nginx server configuration for jupyter.mylan.local
 | |-tls
 | | |-tls.conf  # Nginx server configuration related to https
 | | |-dhparam.pem  # DH parameter file
 | | |-tls.key  # private key for the certificate
 | | |-tls.crt  # certificate used by the server

 |-dns  # directory for dns
 | |-docker-compose.yml  # running Unbound, listening on 53 tcp and udp
 | |-config
 | | |-a-records.conf  # custom A records, forward lookup records added here

next: Adding authentication for web service access