Introduction

TSL, or transport layer security, and its predecessor SSL, which stands for secure socket layer, are the web protocols used to wrap normal traffic in a protected, encrypted wrapper. Using this technology, servers can send traffic safely between the server and client without the possibility of the messages being intercepted by outside parties.

In this guide, I’ll show you how to set up a self-signed SSL certificate for use with Nginx web server on Ubuntu machine hosted on Azure. You don’t need to purchase any domain or update DNS entries for this to work.

Prerequisites

Before you begin, you should have :

  1. An Azure subscription where you will host your virtual machine.
  2. An Azure Virtual machine. In this tutorial, I am working with the Ubuntu machine.
  3. Allow communication at ports 80 and 443 for generating SSL, we will later change this to allow connection only at port 443.

Use case scenarios

This can be used in multiple scenarios :

  • You want to host a static website securely on Azure and want full control on hosting.
  • You don’t want to purchase a domain and use the Azure custom domain available.
  • You want to configure any application on Azure VM and use the URL for accessing this application securely, this is the most common scenario where you can host Jenkins, Grafana, etc, and allow your users to access this securely.
  • In this demo, I will install Grafana and let my users access Grafana securely. You can install any other application based on your requirement.

Steps

1.) The first step is to create a machine on Azure using: Create a Linux VM in the Azure portal.

2.) The second step is to assign an FQDN to the virtual machine created in the first step using Create FQDN for a VM in the Azure portal. I assigned “blog-test.centralindia.cloudapp.azure.com” in my case and will use it throughout this demo. You also need to add an inbound NSG rule to allow communication on ports 80 and 443.

3.) Login to this machine using FQDN we just assigned and install all the required packages such as Nginx and Let’s Encrypt. You can also install the application that you are planning to host on this machine. I am sharing the steps to install Grafana as well if you want to follow along:

#NGINX installation
sudo apt install -y nginx 

#Let's Encrypt Installation
sudo apt install -y letsencrypt

#Steps to install Grafana
sudo apt-get install -y apt-transport-https
sudo apt-get install -y software-properties-common wget
wget -q -O - https://packages.grafana.com/gpg.key | sudo apt-key add -
echo "deb https://packages.grafana.com/enterprise/deb stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list
echo "deb https://packages.grafana.com/enterprise/deb beta main" | sudo tee -a /etc/apt/sources.list.d/grafana.list
sudo apt-get update
sudo apt-get install -y grafana-enterprise
sudo systemctl start grafana-server
sudo systemctl enable grafana-server.service

4.) In this step we configure Nginx to route our traffic to the index file that we will create in step 5. Create grafana.conf file under Nginx configuration (sites-available). This is where we are setting up a reverse proxy so that letsencrypt let’s us create the encryption keys in our next steps.

Go to /etc/nginx/sites-available/ folder and create a conf file. You can name it anything, I created a file named grafana.conf for my simplicity. Now the file location is : /etc/nginx/sites-available/grafana.conf and the file contents are as below. We are basically telling Nginx to listen on port 80 and the www location is grafana folder under html :


server {
root /var/www/html/grafana;
listen 80;
server_name blog-test.centralindia.cloudapp.azure.com;
}

Since we have created grafana.conf file as part of our configuration, we want Nginx to refer to this file whenever we browse the site. We will unlink the default conf file and link grafana.conf file to the sites-enabled using the below commands :

unlink /etc/nginx/sites-enabled/default
ln -s /etc/nginx/sites-available/grafana.conf /etc/nginx/sites-enabled/grafana.conf

5.) We now create an index file under /var/www/html/ folder. I created this file under a subfolder grafana since this is the location I specified in Nginx conf file for step 4. So it looks like : /var/www/html/grafana/index.html.

This file helps to check if our routing is working or not. But if you are planning to host your website, this will be the root folder where you host your website. My index file looks like this :

Nginx configuration is done and we will confirm the status using “nginx -t” which would return test status as successful. Now restart nginx using “service nginx restart.”

After restarting the services browse this URL and it should show the content of index.html file under grafana. If it’s the case, then it is working fine.

If you see the site forbidden or any other error, cross-check the index.html file you created previously in step 5. If this file is deleted or missing somehow, you can recreate the file, restart nginx and try to browse this site. This will be an unsecured HTTP connection right now as we see below :

6.) Our site is now working, we will use letsencrypt to generate the certificates, use the below command and options to do that. You can change the path based on your www location. It will create the cert files in /etc/letsencrypt/live/<FQDN> folder (highlighted in below image). This will be used later in step 9. Use below command to generate the certificates :

letsencrypt certonly --webroot-path /var/www/html/grafana -d blog-test.centralindia.cloudapp.azure.com

7.) Now generate DH parameters using below command, this might take long time, so wait for this to complete successfully.

openssl dhparam -out /etc/letsencrypt/ssl-dhparams.pem 2048

8.) In this step we create the SSL config file named /etc/letsencrypt/options-ssl-nginx.conf with below content in the file :


# This file contains important security parameters. If you modify this file
# manually, Certbot will be unable to automatically provide future security
# updates. Instead, Certbot will print and log an error message with a path to
# the up-to-date file that you will need to refer to when manually updating
# this file.
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;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";

9.) As part of above steps we have generated the certs, created nginx conf file. We will now update the nginx conf file for 443 traffic with SSL settings. So go back to /etc/nginx/sites-available/grafana.conf and replace existing content with the below content. Points to note here are :

  • I am using proxy_pass and proxy_redirect to redirect the incoming request at port 3000 to my FQDN. You can change the port number based on your application. Since I am using Grafana in this demo, I have used default Grafana port 3000
  • ssl_certificate and ssl_certificate_key location should be changed properly for this to work. Please refer step 6 for these values.
  • You need to replace “blog-test.centralindia.cloudapp.azure.com” in below example with your FQDN everywhere.
server { 
listen 443 http2 ssl;
server_name blog-test.centralindia.cloudapp.azure.com;
access_log /var/log/nginx/blog-test.centralindia.cloudapp.azure.com.log;
error_log /var/log/nginx/blog-test.centralindia.cloudapp.azure.com.log;
location /.well-known/acme-challenge/ {
root /var/www/html/grafana; # Temp for generating letsencrypt
default_type text/plain;
}

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

#Fix the “It appears that your reverse proxy set up is broken” error.

proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 90;
proxy_redirect http://127.0.0.1:3000 http://blog-test.centralindia.cloudapp.azure.com/;

#Required for new HTTP-based CLI

proxy_http_version 1.1;
proxy_request_buffering off;
}

ssl_certificate /etc/letsencrypt/live/blog-test.centralindia.cloudapp.azure.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/blog-test.centralindia.cloudapp.azure.com/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
}

server {
if ($host = blog-test.centralindia.cloudapp.azure.com) {
return 301 https://$host$request_uri;
} # managed by Certbot

listen 80;
server_name blog-test.centralindia.cloudapp.azure.com;
return 404; # managed by Certbot
}

10.) Once this is done we will unlink and create symlink again. This is the configuration file created in step 4 and later edited in step 9 :

unlink /etc/nginx/sites-enabled/grafana.conf 
ln -s /etc/nginx/sites-available/grafana.conf /etc/nginx/sites-enabled/grafana.conf

11.) Check nginx status using nginx -t and restart nginx service using service nginx restart. Now open the HTTPS link in your browser, this is https://blog-test.centralindia.cloudapp.azure.com/ in my case and it looks like this. At this point you can block all the ports on the VM NSG except 443.

12.) Let’s Encrypt certificates expire after 90 days and we can add a cron job to renew this certificate automatically. Steps to do that :

  • Open crontab file using “crontab -e” command.
  • Add the certbot command to run daily. In this example, we run the command every day at noon. The command checks to see if the certificate on the server will expire within the next 30 days, and renews it if so. The --quiet directive tells certbot not to generate output.
    • 0 12 * * * /usr/bin/certbot renew –quiet
  • Save and close this file. All installed certificates will be automatically renewed and reloaded.