A couple of weeks ago, I was tasked with figuring out a way to enable two way SSL. I am a programmer, and have had only a limited experience with networking concepts like SSL/TLS in my short career. So I turned up blank on how I could make it possible. Moreover, the terminology you'd find online is not uniform. From a programming point of view, the term "Two way SSL" led me to limited results, and I soon realized that other communities have different terminology. For example the "Two way SSL" is also known as "Mutual TLS" or "mTLS" or "Client Certificate Authentication" in Cloud/DevOps communitites. This makes finding the right resources online more difficult.
Our services have SSL enabled, but only the usual one -- similar to the one you'd find while visiting this website, but this was a unique thing for me as I didn't even know two way SSL existed. I figured out how it works. However, there were more problems with trying to understand how to implement this. Most guides that I found on the internet were very incomplete, and straight away skipped many parts for someone who'd be new to this. This is my attempt at explaining what I have learned, and documenting the same for future reference. It will not explain how two way SSL works, but how to make it work. If you need a quick refresher however, I would direct you to this easy to understand article by Cloudflare explaining where and how mTLS is used.
This guide is only for Unix like systems like macOS or Linux. Though I tried this on Windows, but could not make it work.
Prerequisites
- Nginx
- sudo privileges on your system
Installing and Configuring Nginx
Install
Install1 nginx using apt
command on your Linux system.
sudo apt update
sudo apt install nginx
This will install all of Nginx on the path /opt/nginx
. All the configuration files we will be editing for two-way SSL would be found within this directory.
Configure Nginx to start
Before starting up Nginx for use, we need to enable some ports on the firewall that our Nginx can listen incoming connections from.
sudo ufw app list
Output:
Available applications:
Nginx Full
Nginx HTTP
Nginx HTTPS
OpenSSH
The utility ufw
can be used to manage our firewall. Though a knowledge of ufw
is not necessary for us, more information can be found here.
You can see in the above output that the utility responds us with four profiles. When we installed Nginx, it registered itself with the ufw
utility with three profiles.
-
Nginx HTTP
allows Nginx to listen through port 80, for normal HTTP traffic. -
Nginx HTTPS
allows Nginx to listen through port 443, for HTTPS traffic. -
Nginx Full
is a combination of the above both, enabling port 80 and 443 both.
We will enable Nginx Full
as we have to use our server for SSL, but using normal HTTP connections is not an uncommon use-case either. To enable it, run:
sudo ufw allow 'Nginx Full'
You should see the activated profile if you run the below command
sudo ufw status
Output:
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
Nginx Full ALLOW Anywhere
OpenSSH (v6) ALLOW Anywhere (v6)
Nginx Full (v6) ALLOW Anywhere (v6)
The Nginx service should already be up and running now. You can check by executing
systemctl status nginx
Output:
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
Active: active (running) since Fri 2018-04-20 16:08:19 UTC; 3 days ago
Docs: man:nginx(8)
Main PID: 2369 (nginx)
Tasks: 2 (limit: 1153)
CGroup: /system.slice/nginx.service
├─2369 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
└─2380 nginx: worker process
Creating Certificates
Terminology
-
Certificate Authority (CA): This is an Organization which provides you a Certificate (the
.crt
file). In reality, anyone (including yourself) can be the CA which issues certificates -
Certificate Signing Request (CSR
.csr
): An "input form" with details (like Name, Organization, Address, etc.) filled by the requester of the certificate which is submitted to the CA -
Private Key (
.key
): The private key file used by the CA in conjunction with CSR to sign and generate the certificates -
Certificate (
.crt
): The certificate generated by the CA -
Self Signed Certificate: Consider yourself as a Certificate Authority (CA) and generate the
.crt
files.
Now that we know the terminology, it would be easier to proceed with generating the certificates. The following part of the tutorial requires root/superuser privileges, so switch to the super user using the sudo su
command.
Create a directory called certs
under the root of the file-system which will hold all the certificates and related files.
# Create directory
mkdir /certs
# Change directory to /certs
cd /certs
# Verify the present working directory
pwd
Output:
/certs
In this tutorial, we would be designating ourselves as a Certificate Authority, and then self-sign and generate the certificates.
Generate Certificate Authority (CA) files
We would be first generating a CA key, which would be the basis for all further certificate generation.
openssl genrsa -des3 -out ca.key 4096
Output:
Generating RSA private key, 4096 bit long modulus (2 primes)
.............................................................++++
....................................................................................................................................................................................++++
e is 65537 (0x010001)
Enter pass phrase for ca.key:<passphrase>
Verifying - Enter pass phrase for ca.key:<passphrase>
Command options:
-
genrsa
: generate RSA private key -
-des3
: encrypts the output key using des3 encryption -
-out
: specifying the output key file -
4096
: size of private key in bits
This generates a file named ca.key
in the current directory. You would have to provide a password/passphrase for the key. For the purposes of this tutorial, I'll be using the same passphrase whenever required.
<passphrase>
= root
The next step is the generation of ca.crt
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt
Output:
Enter pass phrase for ca.key:<passphrase>
Command options:
-
req
: used for creating certificate requests -
-new
: generates new certificate request -
-x509
: outputs a new self-signed certificate instead of a certificate request -
-days 3650
: validity of certificate (in this case, 10 years/3650 days) -
-key
: RSA key to be used for generating the certificate -
-out
: output file for the certificate
Once you enter the passphrase (which is the same as we put in the previous step), it'll ask you to provide more information for the self-signed certificate.
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:CertAuth
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:
Note the Organization Name
parameter is named as CertAuth
for this example. This is because we are a CA and CAs should ideally be independent to enable other parties to trust other parties and establish a chain of trust.
A PEM file certificate would also be needed. This file is way of encoding the certificates.2 To create a PEM file, simply do the following:
cat ca.key > ca.pem
cat ca.crt >> ca.pem
The ca.pem
would be something like
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,0E2D38C130456B75
3+0Em2EKRkmCCu79bR7E2uFy/G1huIGEGsItwDf0C70Hf2bmUUDYazK/CPZxZCut
PDximngoGaLSdLQ2HWGjjCe59pJxxZknxHu9QVy3mIWLixAZWevDUnoK1q+Wqy0M
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIFSzCCAzOgAwIBAgIUeawaQJUIAPKOKhrIJDjMVCYVx4MwDQYJKoZIhvcNAQEL
BQAwNTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxETAPBgNVBAoM
-----END CERTIFICATE-----
Generating Client/User Certificates
At this point, we have three files in the certs
directory.
ca.key
ca.crt
ca.pem
We now would be generating client/user certificates.
The command to generate the user.key
is similar to the one used to create the ca.key
openssl genrsa -des3 -out user.key 4096
Output:
Generating RSA private key, 4096 bit long modulus (2 primes)
.....................................................................................................................................................++++
.......................++++
e is 65537 (0x010001)
Enter pass phrase for user.key:
Verifying - Enter pass phrase for user.key:
As mentioned earlier, the passphrase to be used for user.key
is still the same.
The client/user certificate aren't supposed to be self-signed, and we would need to generate a CSR. It means the signing is to be done by a CA(even though in this case, we own the CA!). The way to do this is
openssl req -new -key user.key -out user.csr
Output:
Enter pass phrase for user.key:<passphrase>
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:User
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
The Organization Name
would be User
as this certificate is for a Client/User. DO NOT keep it the same as the one for CA.
The next step would be to create a User Certificate from the User CSR.
openssl x509 -req -days 365 -in user.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out user.crt
Output:
Signature ok
subject=C = AU, ST = Some-State, O = User
Getting CA Private Key
Enter pass phrase for ca.key:<passphrase>
Command options:
-
x509
: standard format for public key certificates -
-CA
: the CA certificate -
-CAkey
: the key to generate the certificate from -
-set_serial 01
: the serial version of the certificate to be generated. The user certificate can be regenerated by incrementing the serial number from the sameuser.csr
after 365 days when it expires.
You can see that it displays data from the CSR, and then uses the CA key to generate user.crt
.
To enable us to use the certificate from a web browser, we would need to create the certificate in the PFX file format.
openssl pkcs12 -export -out user.pfx -inkey user.key -in user.crt -certfile ca.crt
Output:
Enter pass phrase for user.key:<passphrase>
Enter Export Password:<passphrase>
Verifying - Enter Export Password:<passphrase>
You can verify if the generated certificate can be decrypted using the CA certificate by the following command
openssl verify -verbose -CAfile ca.crt user.crt
Output:
user.crt: OK
The files that we currently have in the directory are
ca.key
ca.crt
ca.pem
user.key
user.csr
user.crt
user.pfx
Generating Server Certificates
This is similar to generating the User certificates, with the following commands
Generate the Server key. For server certificates, the standard naming convention seems to be <website-domain-name>
.<key>
openssl genrsa -out nginx.mssl.com.key 4096
Output:
Generating RSA private key, 4096 bit long modulus (2 primes)
.....................................++++
.........................++++
e is 65537 (0x010001)
Use the above generated key to generate a CSR.
openssl req -new -key nginx.mssl.com.key -out nginx.mssl.com.csr
Output:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Server
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:nginx.mssl.com
Email Address []:
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
Make sure to put the Organization Name
as Server
and Common Name
(which is the website name) as nginx.mssl.com
We can now use the CSR along with the CA files to generate the CRT for our server.
openssl x509 -req -days 365 -sha256 -in nginx.mssl.com.csr -CA ca.crt -CAkey ca.key -set_serial 1 -out nginx.mssl.com.crt
Output:
Signature ok
subject=C = AU, ST = Some-State, O = Server, CN = nginx.mssl.com
Getting CA Private Key
Enter pass phrase for ca.key:<passphrase>
All the certificates are now ready for our use!
ca.key
ca.crt
ca.pem
user.key
user.csr
user.crt
user.pfx
nginx.mssl.com.key
nginx.mssl.com.csr
nginx.mssl.com.crt
Setting up our "Website" with Nginx
Of course, we aren't setting up a real website. But we do want to make our SSL authentication work on the domain nginx.mssl.com
. We can do this by a little "hack" by changing our /etc/hosts
file.
Fire up a new shell, and use
sudo vim /etc/hosts
Add the following line to the file and save.
127.0.0.1 nginx.mssl.com
This will enable your local machine to resolve the domain nginx.mssl.com
to your locahost.
The website will not work at the moment -- you'd have to specify the sources/web pages it needs to serve when requested.
Create a file index.html
on the directory path /usr/share/nginx/mssl
with the contents:
<html>
<body>
<h1>Welcome to nginx!-mutual ssl test</h1>
</body>
</html>
If the TLS handshake is successful, the web server should serve the file created above.
Configuring the Nginx server to enable two way SSL
All our certificates are at root in /certs
directory. For simplicity, we would be copying the certificates to /etc/nginx/certs
directory.
cp -r /certs /etc/nginx/certs
Also ensure that the certificates can be used by nginx service by providing them the access
chmod 777 -R certs/
We would not be fiddling with the default nginx configuration which is present in the nginx.conf
file. Instead, we would be creating a new file called proxy.conf
using in the /etc/nginx/sites-available
directory.
Create the file proxy.conf
using the command
cd /etc/nginx/sites-available
touch proxy.conf
Enter the contents as the following (Notice the comments have more explanation):
server {
# Listen on port 443 for HTTPS connections
listen 443;
# Turn SSL on
ssl on;
# Name of the server/website
server_name nginx.mssl.com;
# See https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ssl_server_name
proxy_ssl_server_name on;
# This is the server SSL certificate
ssl_certificate /etc/nginx/certs/nginx.mssl.com.crt;
# This is the server certificate key
ssl_certificate_key /etc/nginx/certs/nginx.mssl.com.key;
# Important:
# This is the CA cert against which the client/user will be validated
# In our case since the Server and the Client certificate is
# generated from the same CA, we use the ca.crt
# But in actual production, the Client certificate might be
# created from a different CA
ssl_client_certificate /etc/nginx/certs/ca.crt;
# Enables mutual TLS/two way SSL to verify the client
ssl_verify_client on;
# Number of intermediate certificates to verify. Good explanation of
# certificate chaining can be found at
# https://cheapsslsecurity.com/p/what-is-ssl-certificate-chain/
ssl_verify_depth 2;
# Any error during the connection can be found on the following path
error_log /var/log/nginx/error.log debug;
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-ECDSA-RC4-SHA:RC4-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK';
keepalive_timeout 10;
ssl_session_timeout 5m;
# Matches the "root" of the website
# If TLS handshake is successful, the request is routed to this block
location / {
# path from which the website is served from
root /usr/share/nginx/mssl;
# index file name
index index.html index.htm;
}
}
Nginx has a good Beginner's Guide to Nginx.
To enable Nginx to use the above configuration, we also have to link the same in the /etc/nginx/sites-enabled
directory.
To accomplish this, use
ln -s /etc/nginx/sites-available/proxy.conf /etc/nginx/sites-enabled/proxy.conf
This creates a symbolic link into /etc/nginx/sites-enabled/proxy.conf
from /etc/nginx/sites-available/proxy.conf
. Consider it as you having many server configurations in the sites-available
, but only what you chose to "enable" in the sites-enabled
will be active.
We now just have to restart the Nginx for the configuration to be active!
systemctl restart nginx
I'll be using Postman to test our changes. However, you'll need to configure Postman to send the Client Certificates with the request3.
To configure the Certificates, navigate to Settings -> Certificates Tab.
There will be a section to add the CA Certificate named CA Certificates
, and this certificate should be a PEM file. Select the ca.pem
from /etc/nginx/certs
. A mistake would be to select the file from the root directory /certs
but this will not work as Postman wouldn't be able to access the file.
There would be another section below for Client Certificates
. Click on Add Certificate
, and put the details as the following:
- Host:
nginx.mssl.com
- CRT File:
/etc/nginx/certs/user.crt
- KEY File:
/etc/nginx/certs/user.key
- PFX File:
/etc/nginx/certs/user.pfx
- Passphrase:
<passphrase>
Now try to fire a GET request to the domain https://nginx.mssl.com
. If everything was configured successfully, you'll get the following response:
<html>
<body>
<h1>Welcome to nginx!-mutual ssl test</h1>
</body>
</html>
If there's an error in the configuration, you'll get the following error response:
<html>
<head>
<title>400 No required SSL certificate was sent</title>
</head>
<body bgcolor="white">
<center>
<h1>400 Bad Request</h1>
</center>
<center>No required SSL certificate was sent</center>
<hr>
<center>nginx/1.14.0 (Ubuntu)</center>
</body>
</html>
Hope this post was of some help!!
Reference:
- This article is good for the demo part.
- The post Client Certificate Auth With Nginx was instrumental in explaining the
ssl_client_certificate
directive and how to use it. - This post is as close to perfection as it gets regarding the steps for generating Certificates, but I couldn't manage to make it work fully with Nginx. (Especially because it skips the demo part + it could be a little more descriptive with generation of the certificates)
- This article is good, but still unclear for someone starting out.
- I used this article to actually learn about generating the certificates especially because it provides easy commands and decent explanation.
-
The main resource for installing Nginx is this tutorial by DigitalOcean. All the relevant steps have been described in my article. ↩
Top comments (15)
Hi quality post, thanks for the detailed explanation. I'm In a similar situation that you described in the introduction. Also the references with your opinions about them are quite cool to read. Thanks.
I'm glad my post could be of help!
Yes, you must be glad
Very good. I would change the chmod 777 though.
I followed the tutorial as-is. I am getting 502 bad gateway. Any insights please?
I have tried making the tutorial and documented even the smallest steps. Never encountered the 502 error myself, but as a last resort, you can retry all the steps from scratch
Thank you. Yes, I did retry thrice in ec2. :(
Can you verify if the EC2 settings are correct? Maybe it is adding something while connecting to the NGNIX service within
Sure. Did you mean the network rules?
Yes
Thanks for sharing this, man. It really helps me. Just one correction on PEM file generation, I think the second line should be
cat ca.crt >> ca.pem
Thanks for reading! I would never have realized the mistake if you hadn't pointed it out. Thanks a lot!
Thanks for this, its very helpful.
This was a great explanation dude. Thanks for this.
Glad you liked it!