Rodrigo Rosenfeld Rosas
Testing HTTPS in a Linux development environment with self-signed certificates
Note: if you only care about getting the certificates, jump to the end of the article and you'll find a button to just do that. This way you don't even need Linux to generate them.
For a long time I've been testing my application locally using a certificate issued by Let's encrypt, which I must renew every few months for domains such as dev.mydomain.com. Recently, I've been considering creating a new app and I don't have a domain for it yet.
So I decided to take some time to learn how to create self-signed certificates in such a way that browsers such as Chrome and Firefox would accept it without any disclaimer with no extra step.
It took me about 2 hours to be able achieve this task, so I decided to write it down so that it would save me time in the future when I need to repeat this process.
I'll use the myapp.example.com domain for my new app, since the example.com domain is reserved.
The first step is add that domain in /etc/hosts:
1 | 127.0.0.1 localhost myapp.example.com |
Recent browsers will require the subject alternate names extension, so the script will generate that extension using a template like this:
1 | [SAN] |
2 | subjectAltName = @alternate_names |
3 | |
4 | [ alternate_names ] |
5 | |
6 | DNS.1 = myapp.example.com |
7 | IP.1 = 127.0.0.1 |
8 | IP.2 = 192.168.0.10 |
Replace the second IP with your own fixed IP if you have one just in case you need to access it from another computer in the network, like some VM, for example. Edit the script below to change the template. You'll need to add the root CA certificate we'll generate soon to those other computers in the network in order to do so, as I'll explain in the last steps in this article. Just remove IP.2 if you don't care about it.
Then create this script to help generating the certificates in ~/.ssl/generate-certificates:
1 | #!/bin/bash |
2 | |
3 | FQDN=${1:-myapp.example.com} |
4 | |
5 | # Create our very own Root Certificate Authority |
6 | |
7 | [ -f my-root-ca.key.pem ] || \ |
8 | openssl genrsa -out my-root-ca.key.pem 2048 |
9 | |
10 | # Self-sign our Root Certificate Authority |
11 | |
12 | [ -f my-root-ca.crt.pem ] || \ |
13 | openssl req -x509 -new -nodes -key my-root-ca.key.pem -days 9131 \ |
14 | -out my-root-ca.crt.pem \ |
15 | -subj "/C=US/ST=Utah/L=Provo/O=ACME Signing Authority Inc/CN=example.net" |
16 | |
17 | # Create Certificate for this domain |
18 | |
19 | [ -f ${FQDN}.privkey.pem ] || \ |
20 | openssl genrsa -out ${FQDN}.privkey.pem 2048 |
21 | |
22 | # Create the extfile including the SAN extension |
23 | |
24 | cat > extfile <<EOF |
25 | [SAN] |
26 | subjectAltName = @alternate_names |
27 | |
28 | [ alternate_names ] |
29 | |
30 | DNS.1 = ${FQDN} |
31 | IP.1 = 127.0.0.1 |
32 | IP.2 = 192.168.0.10 |
33 | EOF |
34 | |
35 | # Create the CSR |
36 | |
37 | [ -f ${FQDN}.csr.pem ] || \ |
38 | openssl req -new -key ${FQDN}.privkey.pem -out ${FQDN}.csr.pem \ |
39 | -subj "/C=US/ST=Utah/L=Provo/O=ACME Service/CN=${FQDN}" \ |
40 | -reqexts SAN -extensions SAN \ |
41 | -config <(cat /etc/ssl/openssl.cnf extfile) |
42 | |
43 | # Sign the request from Server with your Root CA |
44 | |
45 | [ -f ${FQDN}.cert.pem ] || \ |
46 | openssl x509 -req -in ${FQDN}.csr.pem \ |
47 | -CA my-root-ca.crt.pem \ |
48 | -CAkey my-root-ca.key.pem \ |
49 | -CAcreateserial \ |
50 | -out ${FQDN}.cert.pem \ |
51 | -days 9131 \ |
52 | -extensions SAN \ |
53 | -extfile extfile |
54 | |
55 | # Update this machine to accept our own root CA as a valid one: |
56 | |
57 | sudo cp my-root-ca.crt.pem /usr/local/share/ca-certificates/my-root-ca.crt |
58 | sudo update-ca-certificates |
59 | |
60 | cat <<EOF |
61 | Here's a sample nginx config file: |
62 | |
63 | server { |
64 | listen 80; |
65 | listen 443 ssl; |
66 | |
67 | ssl_certificate ${PWD}/${FQDN}.cert.pem; |
68 | ssl_certificate_key ${PWD}/${FQDN}.privkey.pem; |
69 | |
70 | root /var/www/html; |
71 | |
72 | index index.html index.htm index.nginx-debian.html; |
73 | |
74 | server_name ${FQDN}; |
75 | |
76 | location / { |
77 | # First attempt to serve request as file, then |
78 | # as directory, then fall back to displaying a 404. |
79 | try_files $uri $uri/ =404; |
80 | } |
81 | } |
82 | EOF |
83 | |
84 | grep -q ${FQDN} /etc/hosts || echo "Remember to add ${FQDN} to /etc/hosts" |
Then run it:
1 | cd ~/.ssl |
2 | chmod +x generate-certificates |
3 | ./generate-certificates # will generate the certificates for myapp.example.com |
4 | |
5 | # to generate for another app: |
6 | ./generate-certificates otherapp.example.com |
The script will output a sample nginx file demonstrating how to use the certificate and will remind you about adding the entry to /etc/hosts if it detects the domain is not present already.
That's it. Even curl should work out-of-the-box, just like browsers such as Chrome and Firefox:
1 | curl -I https://myapp.example.com |
If you need to install the root certificate in other computers in the network (or VMs), it's located in ~/.ssl/my-root-ca.crt.pem. If the other computers are running Linux:
1 | # The .crt extension is important |
2 | sudo cp my-root-ca.crt.pem /usr/local/share/ca-certificates/my-root-ca.crt |
3 | sudo update-ca-certificates |
I didn't research about how to install them in other OS, so please let me know in the comments if you know and I'll update the article explaining the instructions for setting up VM guests of other operating systems.
I've also created a Docker container with a simple Ruby Rack application to generate those certs. The code is simple and is available at Github.
It's also published to Docker Hub.
You can give it a try here:
I hope you'll find it useful as much as I do ;)