from http to https with free certificates
Recently I’ve noticed that StartCom gives away free certificates through theirs StartSSL site. In this post I will explain how I managed to configure nginx to host two of my domains, on the same IP address, using HTTPS.
TL;DR summary
This is the recipe to create an RSA keypair, a Certificate Signing Request (CSR), install and configure nginx.
Create the RSA keypair:
cd /etc/ssl/private openssl req -newkey rsa:2048 -nodes -keyout ruilopes.com.key -out ruilopes.com.csr && chmod 400 ruilopes.com.key
Set Common Name (eg, YOUR name) field to: ruilopes.com
Paste the contents of the ruilopes.com.csr
file into the StartCom form, let’m generate the certificate, copy it into the ruilopes.com.crt
file.
Append the StartCom intermediate CA certificate to our own:
curl -O https://www.startssl.com/certs/sub.class1.server.ca.pem cat sub.class1.server.ca.pem >> ruilopes.com.crt
Install and configure nginx:
add-apt-repository ppa:nginx/stable apt-get update apt-get install nginx-full mkdir /var/www/ruilopes.com
cat<<"EOF">/var/www/ruilopes.com/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Hello World</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
EOF
cat<<"EOF">/etc/nginx/sites-available/ruilopes.com.conf
server {
server_name ruilopes.com www.ruilopes.com;
listen 80 default;
listen 443 default ssl;
ssl_certificate /etc/ssl/private/ruilopes.com.crt;
ssl_certificate_key /etc/ssl/private/ruilopes.com.key;
root /var/www/ruilopes.com;
access_log /var/log/nginx/ruilopes.com.access.log;
index index.html;
}
EOF
ln -s /etc/nginx/sites-available/ruilopes.com.conf /etc/nginx/sites-enabled/ rm /etc/nginx/sites-enabled/default service nginx restart
Test with openssl:
openssl s_client -connect ruilopes.com:443 -servername ruilopes.com
Test by opening the following address:
https://ruilopes.com/
That’s it!
But really, keep reading! I’ll explain how this works and also add a second domain to the same IP address… this is a somewhat lengthy post… so bear with me! In the end, I hope you’ll understand what is going on.
From http to HTTPS
Roughly, we’ll follow this plan:
- create a RSA keypair (two keys: a private and a public key).
- create a Certificate Signing Request (CSR).
- submit the CSR to StartCom; they will create and sign a certificate that binds our public key and domain name to their Certification Authority (CA).
- configure nginx to use our private key and the certificate signed by StartCom.
I’ll use Ubuntu 10.04, so the nginx install and file-system paths are tied to this particular Linux flavor.
All the commands displayed here should be run inside a shell with root privileges.
Lets start by installing nginx:
apt-get install nginx
And a plain nginx configuration (no certificates involved) for my primary domain ruilopes.com
:
cat<<"EOF">/etc/nginx/sites-available/ruilopes.com.conf
server {
server_name ruilopes.com www.ruilopes.com;
listen 80;
root /var/www/ruilopes.com;
access_log /var/log/nginx/ruilopes.com.access.log;
index index.html;
}
EOF
Create a simple index page:
mkdir /var/www/ruilopes.com
cat<<"EOF">/var/www/ruilopes.com/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Hello World</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
EOF
Enable the new site, remove the default site (which ships by default with Ubuntu), and notify nginx to reload its configuration:
ln -s /etc/nginx/sites-available/ruilopes.com.conf /etc/nginx/sites-enabled/ rm /etc/nginx/sites-enabled/default service nginx reload
Try to access the site, to see if its working:
curl --silent --dump - http://ruilopes.com/
You should see something like:
HTTP/1.1 200 OK
Server: nginx/0.7.65
Date: Mon, 31 Jan 2011 21:54:22 GMT
Content-Type: text/html
Content-Length: 141
Last-Modified: Sun, 30 Jan 2011 20:26:43 GMT
Connection: keep-alive
Vary: Accept-Encoding
Accept-Ranges: bytes
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Hello World</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
Now that we have the baseline working, lets add some crypto to the mix.
Using openssl(1) genrsa(1) create an 2048-bit RSA keypair (without password protection; use -des3
for password protection):
cd /etc/ssl/private openssl genrsa -out ruilopes.com.key 2048 chmod 400 ruilopes.com.key
NB You can later password protect a key with:
openssl rsa -des3 -in ruilopes.com.key -out ruilopes.com.key.new && mv ruilopes.com.key.new ruilopes.com.key
You can inspect the private and public key components with:
openssl rsa -noout -text -in ruilopes.com.key
It looks something like:
Private-Key: (2048 bit) modulus: 00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee: ... publicExponent: 65537 (0x10001) privateExponent: ... prime1: ... prime2: ... exponent1: ... exponent2: ... coefficient: ...
Or just inspect the public content:
openssl rsa -pubout -in ruilopes.com.key 2>/dev/null | openssl rsa -noout -text -pubin
Which should look something like:
Modulus (2048 bit): 00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee: ... Exponent: 65537 (0x10001)
Using openssl req(1) create the Certificate Signing Request (CSR):
openssl req -new -key ruilopes.com.key -out ruilopes.com.csr
NB You can also generate the keypair and CSR with a single command:
openssl req -newkey rsa:2048 -nodes -keyout ruilopes.com.key -out ruilopes.com.csr
This step will ask you for information to include in the CSR, in our case, StartCom level 1 certificates only need the domain name (aka X.509 Common Name or CN; NB all the other information is ignored by StartCom), here’s how openssl req asks for this information:
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]:. Organizational Unit Name (eg, section) []:. Common Name (eg, YOUR name) []:ruilopes.com Email Address []:. Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []:
NB “.” is an openssl convention that means the field should be empty/omitted.
You can inspect the certificate signing request using:
openssl req -noout -text -in ruilopes.com.csr
It looks something like:
Certificate Request: Data: Version: 0 (0x0) Subject: CN=ruilopes.com Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public Key: (2048 bit) Modulus (2048 bit): 00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee: ... Exponent: 65537 (0x10001) Attributes: a0:00 Signature Algorithm: sha1WithRSAEncryption 47:98:bd:ee:41:cb:29:de:29:54:f9:0d:d7:ff:6d:00:55:ec: ...
You are now ready to send the CSR to StartCom… just copy&paste its contents to the StartCom control panel and let them generate our certificate.
NB: They also let you add one sub-domain, I’ve choosen “www” (for www.ruilopes.com
).
After the certificate is generated, copy&paste it into the ruilopes.com.crt
file, and inspect the certificate with:
openssl x509 -noout -text -in ruilopes.com.crt
It will look something like:
Certificate: ... Issuer: C=IL, O=StartCom Ltd., OU=Secure Digital Certificate Signing, CN=StartCom Class 1 Primary Intermediate Server CA Validity Not Before: Jan 29 17:45:36 2011 GMT Not After : Jan 30 19:25:11 2012 GMT Subject: description=345115-eJ9Q0A1Uk83j6dMv, C=PT, O=Persona Not Validated, OU=StartCom Free Certificate Member, CN=www.ruilopes.com/[email protected] ... X509v3 Subject Alternative Name: DNS:www.ruilopes.com, DNS:ruilopes.com
You are now ready to configure nginx with the private keypair (inside ruilopes.com.key
file) and the certificate (inside ruilopes.com.crt
file). So lets do that:
cat<<"EOF">/etc/nginx/sites-available/ruilopes.com.conf
server {
server_name ruilopes.com www.ruilopes.com;
listen 80 default;
listen 443 default ssl;
ssl_certificate /etc/ssl/private/ruilopes.com.crt;
ssl_certificate_key /etc/ssl/private/ruilopes.com.key;
root /var/www/ruilopes.com;
access_log /var/log/nginx/ruilopes.com.access.log;
index index.html;
}
EOF
NB We’ve just added the listen 443
, ssl_certificate
and ssl_certificate_key
lines.
Notify nginx to reload the configuration:
service nginx reload
And try to access our shinny HTTPS server! This time using openssl s_client(1):
openssl s_client -connect ruilopes.com:443 -servername ruilopes.com
The output should look something like:
CONNECTED(00000003) depth=0 /description=345115-eJ9Q0A1Uk83j6dMv/C=PT/O=Persona Not Validated/OU=StartCom Free Certificate Member/CN=www.ruilopes.com/[email protected] verify error:num=20:unable to get local issuer certificate verify return:1 depth=0 /description=345115-eJ9Q0A1Uk83j6dMv/C=PT/O=Persona Not Validated/OU=StartCom Free Certificate Member/CN=www.ruilopes.com/[email protected] verify error:num=27:certificate not trusted verify return:1 depth=0 /description=345115-eJ9Q0A1Uk83j6dMv/C=PT/O=Persona Not Validated/OU=StartCom Free Certificate Member/CN=www.ruilopes.com/[email protected] verify error:num=21:unable to verify the first certificate verify return:1 --- Certificate chain 0 s:/description=345115-eJ9Q0A1Uk83j6dMv/C=PT/O=Persona Not Validated/OU=StartCom Free Certificate Member/CN=www.ruilopes.com/[email protected] i:/C=IL/O=StartCom Ltd./OU=Secure Digital Certificate Signing/CN=StartCom Class 1 Primary Intermediate Server CA --- Server certificate -----BEGIN CERTIFICATE----- ... omitted ... -----END CERTIFICATE----- subject=/description=345115-eJ9Q0A1Uk83j6dMv/C=PT/O=Persona Not Validated/OU=StartCom Free Certificate Member/CN=www.ruilopes.com/[email protected] issuer=/C=IL/O=StartCom Ltd./OU=Secure Digital Certificate Signing/CN=StartCom Class 1 Primary Intermediate Server CA ---
NB for now, ignore the errors that are displayed in this output.
Lets try with curl:
curl --silent --dump - https://ruilopes.com/
And you’ll notice that something is not right, as no text output is displayed… lets see whats is going on by removing the --silent
switch, and try again. This time the output should be something like:
curl: (60) SSL certificate problem, verify that the CA cert is OK. Details: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed More details here: http://curl.haxx.se/docs/sslcerts.html curl performs SSL certificate verification by default, using a "bundle" of Certificate Authority (CA) public keys (CA certs). If the default bundle file isn't adequate, you can specify an alternate file using the --cacert option. If this HTTPS server uses a certificate signed by a CA represented in the bundle, the certificate verification probably failed due to a problem with the certificate (it might be expired, or the name might not match the domain name in the URL). If you'd like to turn off curl's verification of the certificate, use the -k (or --insecure) option.
This happens because curl, by default, does not even try to validate intermediate CA certificates. Recall this piece of information from openssl s_client output:
--- Certificate chain 0 s:/description=345115-eJ9Q0A1Uk83j6dMv/C=PT/O=Persona Not Validated/OU=StartCom Free Certificate Member/CN=www.ruilopes.com/[email protected] i:/C=IL/O=StartCom Ltd./OU=Secure Digital Certificate Signing/CN=StartCom Class 1 Primary Intermediate Server CA ---
This means that our HTTPS server is returning a single certificate, issued by the Certification Authority (CA) that has the Common Name (CN) StartCom Class 1 Primary Intermediate Server CA
. Lets see if Ubuntu ships with a certificate that has that CN:
ls -F /etc/ssl/certs | grep -i startcom
It outputs something like:
StartCom_Certification_Authority.pem StartCom_Ltd..pem
Lets see the CN:
openssl x509 -noout -text -in /etc/ssl/certs/StartCom_Certification_Authority.pem
It should look something like (we are only interested in the Issuer and Subject fields):
Issuer: C=IL, O=StartCom Ltd., OU=Secure Digital Certificate Signing, CN=StartCom Certification Authority Subject: C=IL, O=StartCom Ltd., OU=Secure Digital Certificate Signing, CN=StartCom Certification Authority
As you can see, the Subject CN is not the CN we are looking for… lets try the other certificate:
openssl x509 -noout -text -in /etc/ssl/certs/StartCom_Ltd..pem
It should look something like:
Issuer: C=IL, ST=Israel, L=Eilat, O=StartCom Ltd., OU=CA Authority Dep., CN=Free SSL Certification Authority/[email protected] Subject: C=IL, ST=Israel, L=Eilat, O=StartCom Ltd., OU=CA Authority Dep., CN=Free SSL Certification Authority/[email protected]
So neither certificate Subject has the CN we are looking for… so Ubuntu does not ship with the StartCom Class 1 Primary Intermediate Server CA
certificate.
NB This is a simplification of the certificate resolution algorithm; eg. they are not matched just by their CN, but by their Distinguished Name (DN) and public key value/fingerprint (and other details I’m omitting here).
Lets download it from StartCom site, and see its contents:
curl -O https://www.startssl.com/certs/sub.class1.server.ca.pem openssl x509 -noout -text -in sub.class1.server.ca.pem
It should look something like:
Issuer: C=IL, O=StartCom Ltd., OU=Secure Digital Certificate Signing, CN=StartCom Certification Authority Subject: C=IL, O=StartCom Ltd., OU=Secure Digital Certificate Signing, CN=StartCom Class 1 Primary Intermediate Server CA
So, the Subject we were looking for is here! Also notice that the Subject is different than the Issuer… this means this is an intermediate certificate; the ones that have the Issuer equal to Subject are called root certificates (aka self-signed certificates).
Also notice that the Issuer CN is the same has the one inside the /etc/ssl/certs/StartCom_Certification_Authority.pem
file! So, Ubuntu ships with that root certificate. So, we are missing a link in the certificate chain we return in our HTTPS server… lets fix that by appending it there:
cat sub.class1.server.ca.pem >> ruilopes.com.crt
Notify nginx to reload the configuration:
service nginx reload
See how the certificate chain is now returned:
openssl s_client -connect ruilopes.com:443 -servername ruilopes.com
The output should look something like:
--- Certificate chain 0 s:/description=345115-eJ9Q0A1Uk83j6dMv/C=PT/O=Persona Not Validated/OU=StartCom Free Certificate Member/CN=www.ruilopes.com/[email protected] i:/C=IL/O=StartCom Ltd./OU=Secure Digital Certificate Signing/CN=StartCom Class 1 Primary Intermediate Server CA 1 s:/C=IL/O=StartCom Ltd./OU=Secure Digital Certificate Signing/CN=StartCom Class 1 Primary Intermediate Server CA i:/C=IL/O=StartCom Ltd./OU=Secure Digital Certificate Signing/CN=StartCom Certification Authority ---
So, now the chain is composed by two links:
- Subject with CN=www.ruilopes.com; issued by Issuer with CN=StartCom Class 1 Primary Intermediate Server CA
- Subject with CN=StartCom Class 1 Primary Intermediate Server CA; issued by Issuer with CN=StartCom Certification Authority
See how the first certificate Issuer is the same has the second certificate Subject? That’s how the first certificate is validated by the second, which in turn is validated by the root certificate installed on our local Ubuntu. You see, a chain is formed, starting on our certificate till the root certificate installed on our machine.
OK, looks good… lets see if it actually works by trying again with curl:
curl --silent --dump - https://ruilopes.com/
You should see something like:
HTTP/1.1 200 OK
Server: nginx/0.7.65
Date: Mon, 31 Jan 2011 21:54:22 GMT
Content-Type: text/html
Content-Length: 141
Last-Modified: Sun, 30 Jan 2011 20:26:43 GMT
Connection: keep-alive
Vary: Accept-Encoding
Accept-Ranges: bytes
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Hello World</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
So it worked!
Finally… lets try with a browser! Using Google Chrome you should see something like:
NB the window with the certificate information appears after you click the green padlock on the address bar.
So, that’s it! Our first domain is ready to use!
Now on to the other domain… well, now it much easier, just repeat everything, but with a different domain name and configuration file!
Create the configuration file:
cat<<"EOF">/etc/nginx/sites-available/statica.info.conf
server {
server_name statica.info;
listen 80 default;
listen 443 default ssl;
ssl_certificate /etc/ssl/private/statica.info.crt;
ssl_certificate_key /etc/ssl/private/statica.info.key;
root /var/www/statica.info;
access_log /var/log/nginx/statica.info.access.log;
index index.html;
}
EOF
ln -s /etc/nginx/sites-available/statica.info.conf /etc/nginx/sites-enabled/
Notify nginx to reload the configuration:
service nginx reload
And… it fails with:
Restarting nginx: [emerg]: a duplicate default server for 0.0.0.0:80 in /etc/nginx/sites-enabled/ruilopes.com.conf:4 configuration file /etc/nginx/nginx.conf test failed
This happens because we already had a default domain (ruilopes.com
); so, remove the “default
” word from the “listen
” configuration lines:
cat<<"EOF">/etc/nginx/sites-available/statica.info.conf
server {
server_name statica.info;
listen 80;
listen 443 ssl;
ssl_certificate /etc/ssl/private/statica.info.crt;
ssl_certificate_key /etc/ssl/private/statica.info.key;
root /var/www/statica.info;
access_log /var/log/nginx/statica.info.access.log;
index index.html;
}
EOF
And try again:
service nginx reload
Now it fails with a different error:
Restarting nginx: [emerg]: "ssl" parameter can be specified for the default "listen" directive only in /etc/nginx/sites-enabled/statica.info.conf:5 configuration file /etc/nginx/nginx.conf test failed
It turns out we need a newer version of nginx… fortunately, there is a PPA at https://launchpad.net/nginx Lets, remove the old nginx, and add the new version to our system:
service nginx stop add-apt-repository ppa:nginx/stable apt-get update apt-get remove nginx # remove 0.7.65 apt-get install nginx-full # add 0.8.54
And start it:
service nginx start
It should now work fine!
If you are still with me, you might be wondering how it possible to have two (or more) certificates on the same IP address? This is possible because nginx has support for Server Name Indication (SNI), which you can see with:
nginx -V
Which will output something like:
nginx version: nginx/0.8.54 TLS SNI support enabled
For this to work, the browser also has to support SNI. When it does, the browser will send the domain name (aka server name) that its trying to access in the initial TLS handshake.
This also means that older browsers (without SNI) will not be able to access our site…
And that’s it! I hope you now understand a bit more how certificates work!
– RGL