Nginx HTTPS 配置实践

前言

博客使用 HTTPS 已经有一段时间了,最近把当时的配置过程梳理了一遍,希望能对有需要的同学有所帮助。

概念介绍

最近 HTTPS 是越来越流行了,前一阵子的百度,最近的淘宝、知乎,都已经开启了全站 HTTPS 的时代。HTTPS 即 Hypertext Transfer Protocol Secure,简单理解就是在 HTTP 协议上加了一层加密。具体的细节以后弄透彻了再写写。

无利不起早,那么 HTTPS 的好处是什么呢?我个人觉得有以下几点:

  1. 数据传输加密,防止信息被窃取。使用 HTTP 协议时,用户的密码、银行账户、隐私信息等都是在网络上明文传输,很容易被中间人截取。
  2. 防欺诈。当你使用 EV 级别证书(https://www.wosign.com/EVSSL/index.htm)时,浏览器网址显示和一般 SSL 证书不一样。可以对比下:https://www.paypal.com/signin/ 和 https://www.baidu.com/。使用此类证书的网址,主流的浏览器都会在地址栏显示企业的名称(支持中文),增加了网站信任度,进一步防止用户被钓鱼网站欺骗,当然证书的价格也更贵一些。
  3. 防止劫持。这点可以说是国内各大公司下决心支持 HTTPS 的主要原因了。一旦被劫持,用户访问速度会变慢,看到的内容会被篡改,比如商家在百度投放的广告被替换成竞品的广告,淘宝的商品被带上小尾巴(返利链接),甚至直接跳转到x东。这些不光是损害用户的利益,更是直接影响公司收入。
  4. 使用新的技术。比如 SPDY/HTTP2(头部压缩、连接复用、Server Push等) 的基础都是 HTTPS,新协议对移动端 APP 性能提升帮助很大:《双11手淘前端技术: H5性能最佳实践》

当然 HTTPS 不是银弹,不是用了就保险了,安全是一个整体,这是典型的水桶场景,往往一个系统被攻破是在其薄弱的环节。

证书申请

证书申请的大概流程是,首先在自己的服务器使用 openssl 命令(尽量使用高版本的 OpenSSL,除了能规避漏洞,还能使用更高级的密钥加密/交换算法,提升安全性和性能)生成 csr 和 key 文件,命令如下:

如命令参数所示,私钥 key RSA 的加密强度是 2048 位(rsa:2048)。CSR 文件使用了更安全的 sha256(SHA-2)摘要算法(SHA-1算法在2016年将不被证书厂商和现代浏览器支持)。

-subj 参数指定证书申请者的信息,如果不直接使用此参数,也会有交互式的命令提示填写这些参数。此部分具体参数释义:

Country Name (2 letter code): 输入国家地区代码,例如中国的 CN

State or Province Name (full name): 地区省份

Locality Name (eg, city): 城市名称

Organization Name (eg, company): 公司名称

Organizational Unit Name (eg, section): 部门名称

Common Name (e.g. server FQDN or YOUR name): 申请证书域名,如果是泛域名证书,则应该填写 *.example.com

Email Address: 电子邮箱

命令还会提示输入challenge password,这个不是加密证书,而是相当于一个二次认证密码,如果厂商证书支持challenge password,而且你也设置了的话,后续证书操作需要验证这个密码,提高安全性。

生成的 CSR(Certificate Signing Request,即证书签名请求,不是证书)文件包含了证书的主要信息和公钥,同时生成了私钥 key 文件(注意保存)。然后把 CSR 文件提交给 CA 厂商(不用提交 key 文件),一般提交的时候会让你选择服务器类型,本文指选的 Nginx。中间还会有一个域名所有者验证过程,一般几分钟后(根据证书类型不同,时间不一样)就会发一份邮件给你。

邮件附件一般会带有厂商用 CA 私钥签好的证书文件 example.com.cert.pem(包含 域名、CA 信息,key 对应的公钥,CA 的签名)。同时会给你 CA 的证书链 example.com.cert.ca.pem,如果 CA 证书是多个文件(通常包括 1-2 个中间证书和 1 个根证书),那么需要按照证书信任链由下游到上游,把文件合并成 example.com.cert.ca.pem(cat COMODOECCDomainValidationSecureServerCA.crt COMODOECCAddTrustCA.crt AddTrustExternalCARoot.crt > example.com.cert.ca.pem)。不用把根证书放进 CA 证书链文件里面,因为浏览器和操作系统都内置了根证书,也只认自己内置的根证书。

不过现在更流行的是使用 acme.sh 自动申请和续签 Let’s Encrypt 证书,即免费又好用。acme.sh 提供的方式有很多种,大家按需配置即可,我是 DNSPod + Nginx,仅供参考:

证书处理

后面的步骤各个 HTTP Server 就有点不一样了:

tomcat 则比较特别,证书编码格式需要的不是 pem 而是 JKS,得转换一下。

到这里为止出现了好几个文件,可能有点迷糊,这里总结一下

文件名
释义
example.com.csr
Certificate Signing Request,证书签名请求,不是证书
example.com.key.pem
证书私钥(key),不能泄露(即使是对 CA 厂商),和CSR文件成对存在
example.com.cert.pem
自己的域名证书(pem格式),包含 key 对应的公钥
example.com.cert.ca.pem
CA厂商证书链(pem格式)
example.com.cert.full.pem
合并了自己证书和CA证书的完整证书链(pem格式)
example.com.key.cert.pem
合并了key和域名证书的文件(pem格式)
example.com.cert.pfx
pfx/p12格式的完整证书链(包含了key)
example.com.cert.jks
jks格式(JAVA)的完整证书链(包含了key)

对于这几种文件的区别,可以参考:https://www.cnblogs.com/yjmyzz/p/openssl-tutorial.htmlhttps://blog.freessl.cn/ssl-cert-format-introduce/。实际不用那么麻烦,一般证书厂商会提供不同 webserver 的配置文件,如果没有则需要动手一下,或者使用证书厂商提供的转换工具

TLS握手简述

借由这几个证书文件,简单介绍一下 TLS 的握手过程(RSA 密钥交换方式):

客户端
服务端
ClientHello
明文发送握手请求(上报支持的能力)
生成一个随机数r1
ServerHello
明文返回域名证书(example.com.cert.full.pem)
还会返回一个随机数r2
这里服务端也可以要求客户端提供证书进行校验(流程会有所不同,省略)
对证书进行验证,前面有介绍,证书中包含 CA 信息和签名信息,逐级向上验证即可
ClientKeyExchange
验证通过后从域名证书中拿出公钥,然后使用公钥加密PreMasterSecret(随机生成),发送给服务端(密文)
服务端使用私钥(example.com.key.pem)解密得出PreMasterSecret
客户端使用r1、r2、PreMasterSecret生成session key(会话密钥)
服务端也用同样的方式生成session key,生成算法相同(PRF
使用session key进行对称加密

可以看出核心就是 PreMasterSecret 是密文,其它都是明文。如果中间人想要截取就只能替换证书中的公钥(用自己私钥解密),因为证书本身也有完整性签名,就只能替换整个证书,但证书又是一级级到 Root CA 校验,所以 TLS 握手的结果是安全的。

证书逐级验证的过程(来源:https://security.stackexchange.com/questions/56389/ssl-certificate-framework-101-how-does-the-browser-actually-verify-the-validity

才疏学浅,介绍的非常简陋,建议阅读 https://halfrost.com/https_tls1-2_handshake/ 及系列文章

配置详解

下面正式开始 Nginx 的配置详解。

其实 Nginx 默认配置文件已经包含自签证书(关于自签证书可以参考:《给Nginx配置一个自签名的SSL证书》)的 SSL 配置项了,只要去掉注释,稍微修改下就行(我使用的是 Nginx 1.8,不同版本可能配置有差异):

这几个配置参数也很好理解。主要是填对证书文件的路径:

ssl_protocols

表示 SSL 的协议,Nginx 的配置文件已经说明不要使用 SSLv3 版本的协议(有安全问题)。其实 TLSv1 协议也不建议使用,目前建议配置是 TLSv1.2 TLSv1.3,但 IE9 默认最高只支持 TLSv1(《在 Internet Explorer 中启用 TLS 1.1 和 TLS 1.2》),如果为了兼容性还是得支持此协议。

ssl_ciphers

参数指的是加密算法,根据自身机器 OpenSSL 的版本不一样支持的算法也不一样,设置一个安全、健壮的加密算法尤为重要(可以看见 Nginx 默认就禁用了 MD5 算法),不安全的加密算法有安全风险,而且用户打开 URL 的时候浏览器会发出警告,甚至无法打开。可以通过 openssl ciphers 命令查看系统支持的算法:

但是你可能说这不是在逗我吗?这么多!谁搞得清楚,而且还要兼顾兼容性和安全性。不要急,伟大的 mozilla 公司已经提供了一个可视化配置页面:https://ssl-config.mozilla.org/,根据自身服务器环境和需求,简单填写几个参数就可以生成一份完美配置。

2015-12-29_000935

这时我们的配置项变成了这样(我这里后续修改过,可能和自动生成的配置文件有点点差别):

我们来看看都增加了些什么。

listen 443 ssl spdy

增加 spdy 支持,主要是提供请求复用特性,前面 HTTPS 好处阐述时有说明。目前最新版 Nginx 已支持 HTTP/2,而且 Chrome 51 开始已经不支持 spdy了,强烈建议使用 HTTP/2,直接将这行配置中的 spdy 替换为 http2 即可开启 HTTP/2,详见:配置Nginx,开启HTTP/2

ssl_dhparam

因为默认的是使用 SHA-1 做密钥交换签名(SSL 握手时交换公钥)加密的,不太安全,推荐使用 Diffie-Hellman 算法(一种确保共享 KEY 安全穿越不安全网络的方法)做密钥交换。生成方法:

ssl_prefer_server_ciphers

表示服务端加密算法优先于客户端加密算法,主要是防止降级攻击,不过最新建议是配置为 off(需使用更新的 Nginx),详见:https://github.com/mozilla/server-side-tls/issues/260

ssl_session_timeout、ssl_session_cache

ssl session 缓存的相关配置,防止每次请求都要重新握手协商,提高性能。

ssl_session_tickets

和上面一个配置簇(不过上面是基于 Session ID 会话恢复),客户端是否允许使用 Session Ticket 进行 TLS 连接恢复。这个有一定安全隐患,因为旧的 tickets 还能继续使用,要保证安全则 ssl_session_ticket_key(加密 tickets 的密钥)需要经常变换,这比较麻烦,因此一般建议关闭。需要注意的是如果 Nginx 有多个 https server 这个配置需要统一,不然有可能出现 SSL_PROTOCOL_ERROR 错误

Strict-Transport-Security(HSTS)

告诉浏览器这个域名在指定的时间(max-age)内应该强制使用 HTTPS 访问,目前现代浏览器都支持。即使用户手动输入 http://example.com,浏览器也会把请求重定向到 https://example.com。而且这个跳转发生在客户端,提高了安全性和性能。不过这里不能完全取代 301/302 重定向配置,因为好像是用户必须要访问过一次 https 的域名,才会被种下这个标记。

顺带说一下关于301/302 重定向,个人建议一般网站使用 301 永久重定向即可,因为这个状态会被浏览器缓存,能减轻服务端压力。但是你可能会发现淘宝使用 302 临时重定向比较多,主要还是因为这个状态码不会被浏览器长时间缓存,有利用服务端业务调整。至于 SEO 上的差别那就仁者见仁智者见智了。

ssl_stapling、ssl_stapling_verify

OCSP(Online Certificate Status Protocol,在线证书状态协议)是用来检验证书合法性的在线查询服务,这是客户端(浏览器)验证证书的方式之一(另一种是 CRL,本地通过证书名单验证),通过 http 请求向 CA 机构验证,不过可能遇到网络不通、延迟高等问题,造成证书验证时间过长或出错,网站访问慢甚至无法访问。而 OCSP Stapling,是指服务端主动获取 OCSP 查询结果并随着证书一起发送给客户端,从而让客户端跳过自己去验证的过程,提高 TLS 握手效率。

一般还需要配置 ssl_trusted_certificate(CA的中间证书和根证书,对应 acme.sh --install-cert 中的 --ca-file 设置),但有时候没配置好像也行,可能版本有差别(具体原因可参考:《从无法开启 OCSP Stapling 说起》)。验证是否配置成功的方法:

ssl_buffer_size

默认 16k,但有时候一个返回很大(静态文件),那么需要累积到 16k 的数据,才给客户端返回,一般这个时间很短,不过有时候为了减小 TTFB 可以把 buffer 设置的小一点,比如 4k。

其它安全配置

X-Frame-Options:告诉浏览器本宝宝的网站不允许被别人用 iframe 标签嵌入(sameorigin 表示自己域名的可以)。

X-Content-Type-Options:告诉浏览器不要猜测未知 Content-Type 的资源,主要是提高安全性。

至此基本的配置就完了,不要忘了重启 Nginx 哦。

测试验证

跑(装)个(ge)分(逼)(https://www.ssllabs.com/ssltest/):

2015-12-29_012004

还不错 A+,页面生成的报表还包含一些兼容性测试结果和改进意见,值得关注一下。

本文更新历史

Update 2016-07-05:

最近证书到期了,renew 时把证书从 RSA 加密换成了 ECC 加密(椭圆曲线,安全性和性能更好),这里简单记录一下:

签证书的命令和之前略有不同:

-name 参数指定具体的算法实现,openssl ecparam -list_curves 命令可以显示当前 openssl 版本(建议升级到最新版)支持的算法,同类的算法 key 的位数越大越安全,当然也更影响性能。你也可以在 https://www.ssllabs.com/ssltest/viewMyClient.html 查看自己浏览器支持的算法,辅助选择。我这里选择的是prime256v1(=secp256r1)也是 Nginx 当前版本默认的值。

生成 CSR:

和之前一样按照提示输入一些必要信息即可,然后提交 CSR 文件,几分钟新的证书就签下来了,替换掉之前的证书后 sudo nginx -s reload 重新加载配置即可。

新的证书:

2016-7-6 002509

Update 2020-01:

更新配置(安全)说明、acme.sh 使用、证书区别、TLS 握手过程。

参考资料

百度全站 https FAQ- 技术宅告诉你如何搜索更安全

全站 https 时代的号角 : 大型网站的 https 实践系列 - 系列文章(4 篇),强烈建议阅读、研究

Module ngx_http_ssl_module

linux升级openssl和php_openssl模块

Deprecation of SHA-1 and moving to SHA-2

一些安全相关的HTTP响应头

Should disable compression when using SSL

Enable SPDY header compression

Strong SSL Security on nginx

Security/Server Side TLS

生成并成功签署 ECC SSL 证书

BOTCHING FORWARD SECRECY - The sad state of server-side TLS Session Resumption implementations

Cloudflare's Internet facing SSL cipher configuration

沃通SSL证书、代码签名证书、客户端数字证书安装配置指南文档下载

SSL/TLS协议运行机制的概述

CC BY-NC-SA 4.0

扫码分享

本文不支持评论,如有疑问或建议请联系我,讨论以完善内容,期望帮助到更多的同学。