When you visit a website using HTTP, data exchanged between the web server and the browser is not encrypted. Any information you enter on the website, including passwords or credit card numbers, is susceptible to interception by malicious actors. That’s why over the years, all major browsers have been promoting HTTPS, the secure version of HTTP. You must have noticed the “Not Secure” warning when visiting HTTP only websites.

HTTPS adds a layer of security by using SSL/TLS protocols to encrypt the data transmitted. In order to establish an HTTPS connection, a website must obtain an SSL/TLS certificate from a trusted Certificate Authority (CA). This certificate verifies the authenticity of the website and enables the encryption of data. Let’s Encrypt is a popular non-profit certificate authority with the goal of all websites being secure and using HTTPS. Together with client software certbot, it has significantly lowered the barrier of enabling HTTPS.

However, within an internal network, to get certificate from a public CA is challenging. E.g., you may need to own a public domain name. A self-signed certificate might be a good alternative – it’s a digital certificate generated by the server itself rather than being issued by a third-party CA. There are some downsides of course, e.g., you need to explicitly trust the certificate in browser/OS settings. But for homelab testing/development purposes, self-signed certificate is still a good fit. This post shows how to create a self-signed certificate for a server within a home network, with related configurations on Nginx.

Prepare Certificate

Some tutorials suggesting using the following command to interactively create key and certificate pair using openssl.

sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/nginx-self.key -out /etc/ssl/certs/nginx-self.crt

However, the certificate created this way does not contain SubjectAltName. In recent Chrome versions, SubjectAltName is required for self-signed certificate to be valid.

An alternative way I found was using a req config file, which I personally feel more convenient. First prepare the config file below:

[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
C = US
ST = California
L = Los Angeles
O = Yeguang Home
OU = IT
CN = your_domain.home
[v3_req]
subjectAltName = @alt_names
basicConstraints = CA:TRUE
[alt_names]
DNS.0 = your_domain.home

Then run the below command to generate key and certificate from the config file:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout nginx-self.key -out nginx-self.crt -config req.conf -extensions 'v3_req'

To extract info from the generated certificate file, the following command can be used:

openssl x509 -in nginx-self.crt -noout -text

Nginx Setup

With the key and certificate pair ready, move them to desired location and prepare the following snippets for Nginx.

/etc/nginx/snippets/self-signed.conf

ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt;
ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;

While using OpenSSL, it’s suggested create a strong Diffie-Hellman (DH) group for negotiating Perfect Forward Secrecy with clients. It can be done with the following command:

sudo openssl dhparam -out /etc/nginx/dhparam.pem 4096

As suggested by this tutorial, the following parameters snippet sets Nginx up with a strong SSL encryption settings.

/etc/nginx/snippets/ssl-params.conf

ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/dhparam.pem; 
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_ecdh_curve secp384r1;
ssl_session_timeout  10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Disable strict transport security for now. You can uncomment the following
# line if you understand the implications.
#add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

Lastly, modify the Nginx configuration file to listen to port 443 with the SSL settings we set before, and redirect http request to https.


server {
    listen 80;
    listen [::]:80;
    server_name your_domain.home;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    include snippets/self-signed.conf;
    include snippets/ssl-params.conf;
    
	root /var/www/your_domain/html;
    index index.html index.htm index.nginx-debian.html;
	server_name your_domain.home;
	location / {
        try_files $uri $uri/ =404;
	}
}

You can test the configuration with sudo nginx -t. If everything is fine, restart nginx service to apply the changes. A warning regarding ssl_stapling is expected and the server should still encrypt connections correctly.

Client Setup

Now, you’re ready to test the SSL server. Unfortunately when you type https://your_domain.home, you will still receive a warning “Your connection is not private”, because browsers like Chrome are unable to verify the CA. Don’t worry, as a temporary solution, you can simply click Advanced and proceed.

For a more permanent solution, you can export the certificate (.cer) file from your browser, and trust the certificate in system level. On a Mac, you can import the certificate to Keychain and change trust policies of the certificate.

References:

  1. Let’s Encrypt ACME
  2. How To Create a Self-Signed SSL Certificate for Nginx in Ubuntu 20.04
  3. Wikipedia: Diffie–Hellman key exchange
  4. How to create a self-signed SSL Certificate with SubjectAltName(SAN)
  5. Change certificate trust policies on Mac

License


Comments