Since implementing HSTS and an HTTPS redirect properly was trickier than I'd imagined, I decided to write my own guide on how to do it. I'll also explain how to configure Nginx to use OSCP Stapling and IPv6 since I worked on getting both of them working at around the same time as I got the proper HTTPS redirect working.
Preface
Very recently, I decided to move my blog from Rackspace Cloud Files to a VPS at DigitalOcean. While the pricing for Cloud Files was very agreeable, you really did only get what you pay for:
- Extremely high latency (500-1500 ms)
- No support for TLS (SSL) on custom domains
- No way to have separate TTL and browser cache durations on objects
These might have been acceptable trade-offs if I were hosting a large, million-views-per-day website and only used Cloud Files for images, documents, and other static content, but I'm not, and these issues really started to bug me after a while.
Since I had about $100 of unused DigitalOcean credit, I decided to see how the blog would perform on their lowest-tier VPS running CentOS with Nginx. To my surprise, pages loaded much faster and with significantly less latency than on Cloud Files. I thought this was incredible and that the performance more than justified DigitalOcean's relatively-high pricing ($5/mo. for 512 MB RAM, 20 GB RAID 10 SSD, and 1 CPU core), so I decided then to stay with them until my credit runs out in 20 months.
Now that I was no longer on Cloud Files, I decided I'd finally implement HTTPS on my blog. On top of that, though, I wanted to implement HTTP Strict Transport Security since it's a really cool technology that helps prevent sslstrip-style man-in-the-middle attacks. Of course, while I don't believe anyone would ever consider my blog important enough to attack, there have been reported instances of Internet Service Providers injecting ads into plaintext-HTTP webpages and I want to ensure that the content my blog readers see is the content I posted on it.
HTTPS and HSTS on Nginx
To start, I used the Mozilla SSL Configuration Generator to create a nice
base config file for Nginx. Because I want as many people to securely view my
blog as possible, I chose to use the "Intermediate" list of ciphers. To
generate my DH paramaters, I followed the steps on this page. To enable
HSTS, I added add_header Strict-Transport-Security "max-age=31536000";
to
each HTTPS server
block in the config file (adding an HSTS header to plain
HTTP responses violates the HSTS specification).
For my specific setup, I want people to automatically get redirected to the www subdomain if they try to go to http(s)://cyrozap.com and for them to get redirected to HTTPS if they try to go to http://www.cyrozap.com. Finally, an HSTS header should be added to https://cyrozap.com so the browser knows not to try the HTTP link in case that redirect gets intercepted by an SSL-stripper. To do all of that, I used these simple configuration blocks:
server{
listen 80;
listen [::]:80;
server_name cyrozap.com www.cyrozap.com;
return 301 https://$host$request_uri;
}
The block above handles the HTTP->HTTPS redirect for both the main domain and the www subdomain. Essentially, all it does is listen for any connections to http://cyrozap.com or http://www.cyrozap.com and redirects them to https://cyrozap.com and https://www.cyrozap.com, respectively.
server{
listen 443 ssl;
listen [::]:443 ssl;
server_name cyrozap.com;
add_header Strict-Transport-Security "max-age=31536000";
return 301 https://www.$host$request_uri;
}
This block only handles connections to https://cyrozap.com. Its purpose is to add an HSTS header to the root domain and then immediately redirect it to the www subdomain. If I skipped using this block, browsers that had only connected to http://cyrozap.com in the past would always try to connect to that first, making them vulnerable to an SSL-stripping MitM.
OCSP Stapling
OCSP Stapling turned out to be more difficult to enable HSTS. To do it in Nginx, you need to add something that looks like this to your main server config block:
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/startssl.stapling.crt;
resolver 8.8.8.8 8.8.4.4;
resolver_timeout 5s;
The tricky thing here is the ssl_trusted_certificate
variable. You do not
set that to your TLS certificate's CA certificate. Instead, you need to
generate a certificate chain containing all the certificates leading up to the
one that signs the OCSP response. Since I use the StartCom Class 1 CA, I tried
using the certificate from this blog post, but it didn't work for me (I
kept getting the kind of errors seen here). To fix it, I just tried
different combinations of Root CA + Intermediate CA StartCom certs until I no
longer got the OCSP verification failure messages. To make sure I don't somehow
lose this certificate, I've reproduced it below:
-----BEGIN CERTIFICATE-----
MIIGNDCCBBygAwIBAgIBGTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEW
MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg
Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh
dGlvbiBBdXRob3JpdHkwHhcNMDcxMDI0MjA1NDE3WhcNMTcxMDI0MjA1NDE3WjCB
jDELMAkGA1UEBhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsT
IlNlY3VyZSBEaWdpdGFsIENlcnRpZmljYXRlIFNpZ25pbmcxODA2BgNVBAMTL1N0
YXJ0Q29tIENsYXNzIDEgUHJpbWFyeSBJbnRlcm1lZGlhdGUgU2VydmVyIENBMIIB
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtonGrO8JUngHrJJj0PREGBiE
gFYfka7hh/oyULTTRwbw5gdfcA4Q9x3AzhA2NIVaD5Ksg8asWFI/ujjo/OenJOJA
pgh2wJJuniptTT9uYSAK21ne0n1jsz5G/vohURjXzTCm7QduO3CHtPn66+6CPAVv
kvek3AowHpNz/gfK11+AnSJYUq4G2ouHI2mw5CrY6oPSvfNx23BaKA+vWjhwRRI/
ME3NO68X5Q/LoKldSKqxYVDLNM08XMML6BDAjJvwAwNi/rJsPnIO7hxDKslIDlc5
xDEhyBDBLIf+VJVSH1I8MRKbf+fAoKVZ1eKPPvDVqOHXcDGpxLPPr21TLwb0pwID
AQABo4IBrTCCAakwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD
VR0OBBYEFOtCNNCYsKuf9BtrCPfMZC7vDixFMB8GA1UdIwQYMBaAFE4L7xqkQFul
F2mHMMo0aEPQQa7yMGYGCCsGAQUFBwEBBFowWDAnBggrBgEFBQcwAYYbaHR0cDov
L29jc3Auc3RhcnRzc2wuY29tL2NhMC0GCCsGAQUFBzAChiFodHRwOi8vd3d3LnN0
YXJ0c3NsLmNvbS9zZnNjYS5jcnQwWwYDVR0fBFQwUjAnoCWgI4YhaHR0cDovL3d3
dy5zdGFydHNzbC5jb20vc2ZzY2EuY3JsMCegJaAjhiFodHRwOi8vY3JsLnN0YXJ0
c3NsLmNvbS9zZnNjYS5jcmwwgYAGA1UdIAR5MHcwdQYLKwYBBAGBtTcBAgEwZjAu
BggrBgEFBQcCARYiaHR0cDovL3d3dy5zdGFydHNzbC5jb20vcG9saWN5LnBkZjA0
BggrBgEFBQcCARYoaHR0cDovL3d3dy5zdGFydHNzbC5jb20vaW50ZXJtZWRpYXRl
LnBkZjANBgkqhkiG9w0BAQsFAAOCAgEAUiVivr7DGl0kxETnJMnlpLWn6AXQE+XS
L2TdMkbS3wY40tqEo2O/MJ54cf1WQgZilw659qpXYmxMNX2VGo8rz2HxPcBMaU2m
jRnVAyFXn2uLvTKUcBIu0Gs1qA75FBUOYxKudcreUaeWD2LlPgtdMX48mEU1Vmjt
DAHs1X7/z5j6c75c2VZ69xx05z1MKm4+32xsntq3EdcmskUBc2VZNqs6iz1+cCBT
QBdeP56f+1hFQ5HiisQ8bDVdeENoFO99kHAAtjv4pkmj4BYqBUVWJy3VXxJcOk3Q
Wyn5zwUps6EAJCAkY6u7rUiEhF4a3y7IV3llMx7V+lpkACGAKOlO2CVRohy+SDOi
VGkzF+dzn+dx8Dzk+XX8ttFMh7G/IkbOBXLUOEuIDshnupkJZZdIFznMWAWN0iOj
6qyb3+FwZAA5AZua5nrZrLeqK8ssn8L5sFQOfO9mwcgszBLr63VfH+y4WqSDyVj9
mVv3yh6zTKYoXOMjH722m0oPwmgFsNLVrRSMtGt3Vvh0oSoDCjOi594tqTwaemkt
FyGcXud7db/MkwC2MU0DC1QZX3YQpjuP/keRY/U46/oOwaqI8JnhNd6xyn4H4ufz
UAgl+Pu/aphb4RlClRuEL38a/Kq70wujW77vBXiEmjVOKnIkI2OElZ/AyIQS/jZf
AJX+NnYi6tU=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIHhzCCBW+gAwIBAgIBLTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEW
MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg
Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh
dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM3WhcNMzYwOTE3MTk0NjM2WjB9
MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi
U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh
cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA
A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk
pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf
OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C
Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT
Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi
HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM
Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w
+2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+
Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3
Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B
26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID
AQABo4ICEDCCAgwwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD
VR0OBBYEFE4L7xqkQFulF2mHMMo0aEPQQa7yMB8GA1UdIwQYMBaAFE4L7xqkQFul
F2mHMMo0aEPQQa7yMIIBWgYDVR0gBIIBUTCCAU0wggFJBgsrBgEEAYG1NwEBATCC
ATgwLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5w
ZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL2ludGVybWVk
aWF0ZS5wZGYwgc8GCCsGAQUFBwICMIHCMCcWIFN0YXJ0IENvbW1lcmNpYWwgKFN0
YXJ0Q29tKSBMdGQuMAMCAQEagZZMaW1pdGVkIExpYWJpbGl0eSwgcmVhZCB0aGUg
c2VjdGlvbiAqTGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0
aWZpY2F0aW9uIEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93
d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgG
CWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1
dGhvcml0eTANBgkqhkiG9w0BAQsFAAOCAgEAjo/n3JR5fPGFf59Jb2vKXfuM/gTF
wWLRfUKKvFO3lANmMD+x5wqnUCBVJX92ehQN6wQOQOY+2IirByeDqXWmN3PH/UvS
Ta0XQMhGvjt/UfzDtgUx3M2FIk5xt/JxXrAaxrqTi3iSSoX4eA+D/i+tLPfkpLst
0OcNOrg+zvZ49q5HJMqjNTbOx8aHmNrs++myziebiMMEofYLWWivydsQD032ZGNc
pRJvkrKTlMeIFw6Ttn5ii5B/q06f/ON1FE8qMt9bDeD1e5MNq6HPh+GlBEXoPBKl
CcWw0bdT82AUuoVpaiF8H3VhFyAXe2w7QSlc4axa0c2Mm+tgHRns9+Ww2vl5GKVF
P0lDV9LdJNUso/2RjSe15esUBppMeyG7Oq0wBhjA2MFrLH9ZXF2RsXAiV+uKa0hK
1Q8p7MZAwC+ITGgBF3f0JBlPvfrhsiAhS90a2Cl9qrjeVOwhVYBsHvUwyKMQ5bLm
KhQxw4UtjJixhlpPiVktucf3HMiKf8CdBUrmQk9io20ppB+Fq9vlgcitKj1MXVuE
JnHEhV5xJMqlG2zYYdMa4FTbzrqpMrUi9nNBCV24F10OD5mQ1kfabwo6YigUZ4LZ
8dCAWZvLMdibD4x3TrVoivJs9iQOLWxwxXPR3hTQcY+203sC9uO41Alua551hDnm
fyWl8kgAwKQB2j8=
-----END CERTIFICATE-----
IPv6 with TLS on Nginx
This final part was a little confusing, but I eventually figured it out. Since
DigitalOcean supports IPv6 in the datacenter my VPS is in, I decided I'd take
advantage of that feature and get my blog to work with it. After adding the
necessary AAAA records to my domain, I needed to configure Nginx to listen on
all IPv6 addresses (it doesn't by default). However, even after adding the
listen [::]:443 ssl;
and listen [::]:80;
statements to the proper server
configuration blocks, IPv6 still did not work properly. After some reading, it
seems that the default listening ports need to be modified in the main Nginx
config file as well (/etc/nginx/nginx.conf
), not just the included config
file for the specific site. In the only active server config block in that file,
I modified the single listen 80;
statement to look like this:
listen 80 default_server;
listen [::]:80 ipv6only=on default_server;
After adding those lines, everything seemed to work as it should, even with multiple TLS and non-TLS virtual hosts.