目录
前言
博客使用 HTTPS 已经有一段时间了,最近把当时的配置过程梳理了一遍,希望能对有需要的同学有所帮助。
概念介绍
最近 HTTPS 是越来越流行了,前一阵子的百度,最近的淘宝、知乎,都已经开启了全站 HTTPS 的时代。HTTPS 即 Hypertext Transfer Protocol Secure,简单理解就是在 HTTP 协议上加了一层加密。具体的细节以后弄透彻了再写写。
无利不起早,那么 HTTPS 的好处是什么呢?我个人觉得有以下几点:
- 数据传输加密,防止信息被窃取。使用 HTTP 协议时,用户的密码、银行账户、隐私信息等都是在网络上明文传输,很容易被中间人截取。
- 防欺诈。当你使用 EV 级别证书(https://www.wosign.com/EVSSL/index.htm)时,浏览器网址显示和一般 SSL 证书不一样。可以对比下:https://www.paypal.com/signin/ 和 https://www.baidu.com/。使用此类证书的网址,主流的浏览器都会在地址栏显示企业的名称(支持中文),增加了网站信任度,进一步防止用户被钓鱼网站欺骗,当然证书的价格也更贵一些。
- 防止劫持。这点可以说是国内各大公司下决心支持 HTTPS 的主要原因了。一旦被劫持,用户访问速度会变慢,看到的内容会被篡改,比如商家在百度投放的广告被替换成竞品的广告,淘宝的商品被带上小尾巴(返利链接),甚至直接跳转到x东。这些不光是损害用户的利益,更是直接影响公司收入。
- 使用新的技术。比如 SPDY/HTTP2(头部压缩、连接复用、Server Push等) 的基础都是 HTTPS,新协议对移动端 APP 性能提升帮助很大:《双11手淘前端技术: H5性能最佳实践》
当然 HTTPS 不是银弹,不是用了就保险了,安全是一个整体,这是典型的水桶场景,往往一个系统被攻破是在其薄弱的环节。
证书申请
证书申请的大概流程是,首先在自己的服务器使用 openssl 命令(尽量使用高版本的 OpenSSL,除了能规避漏洞,还能使用更高级的密钥加密/交换算法,提升安全性和性能)生成 csr 和 key 文件,命令如下:
1 |
openssl req -new -newkey rsa:2048 -sha256 -nodes -out example.com.csr -keyout example.com.key.pem -subj "/C=CN/ST=Beijing/L=Beijing/O=Example Inc./OU=Web Security/CN=example.com" |
如命令参数所示,私钥 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,仅供参考:
1 2 3 4 5 6 7 8 |
# 申请证书 acme.sh --issue -d example.com -d www.example.com -d api.example.com --dns dns_dp --keylength ec-256 # 安装证书位置 acme.sh --install-cert --ecc -d example.com \ --key-file /path/ssl/example.com.key.pem \ --ca-file /path/ssl/example.com.cert.ca.pem \ --fullchain-file /path/ssl/example.com.cert.full.pem \ --reloadcmd "sudo systemctl force-reload nginx" |
证书处理
后面的步骤各个 HTTP Server 就有点不一样了:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# Nginx,自己的证书和CA证书组成下游到根的完整证书链,key单独配置 cat example.com.cert.pem example.com.cert.ca.pem > example.com.cert.full.pem # Lighttpd,key和自己证书合并为一个新的证书,ca证书单独配置 cat example.com.key.pem example.com.cert.pem > example.com.key.cert.pem # 附 Lighttpd 的配置: $SERVER["socket"] == "xxx.example.com:443" { ssl.engine = "enable" ssl.pemfile = "/path/ssl/example.com.key.cert.pem" ssl.ca-file = "/path/ssl/example.com.cert.ca.pem" server.name = "xxx.example.com" …… } |
tomcat 则比较特别,证书编码格式需要的不是 pem 而是 JKS,得转换一下。
1 2 3 4 5 6 |
# 先转换为pfx格式 openssl pkcs12 –export –out example.com.cert.pfx –in example.com.cert.full.pem –inkey example.com.key.pem # 回车后输入pfx证书的密码两次 # 使用java jdk将pfx格式证书转换为jks格式证书 keytool -importkeystore -srckeystore example.com.cert.pfx –destkeystore example.com.cert.jks -srcstoretype PKCS12 -deststoretype JKS # 回车后输入一次pfx证书密码,然后输入两次要设置的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.html、https://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://halfrost.com/https_tls1-2_handshake/ 及系列文章
配置详解
下面正式开始 Nginx 的配置详解。
其实 Nginx 默认配置文件已经包含自签证书(关于自签证书可以参考:《给Nginx配置一个自签名的SSL证书》)的 SSL 配置项了,只要去掉注释,稍微修改下就行(我使用的是 Nginx 1.8,不同版本可能配置有差异):
1 2 3 4 5 6 7 8 9 10 |
server { # SSL configuration listen 443 ssl default_server; listen [::]:443 ssl default_server; ssl_certificate cert.pem; ssl_certificate_key cert.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don’t use SSLv3 ref: POODLE ssl_ciphers HIGH:!aNULL:!MD5; …… } |
这几个配置参数也很好理解。主要是填对证书文件的路径:
ssl_protocols
表示 SSL 的协议,Nginx 的配置文件已经说明不要使用 SSLv3 版本的协议(有安全问题)。其实 TLSv1 协议也不建议使用,目前建议配置是 TLSv1.2 TLSv1.3,但 IE9 默认最高只支持 TLSv1(开启浏览器TLS1.2支持),如果为了兼容性还是得支持此协议。
ssl_ciphers
参数指的是加密算法,根据自身机器 OpenSSL 的版本不一样支持的算法也不一样,设置一个安全、健壮的加密算法尤为重要(可以看见 Nginx 默认就禁用了 MD5 算法),不安全的加密算法有安全风险,而且用户打开 URL 的时候浏览器会发出警告,甚至无法打开。可以通过 openssl ciphers 命令查看系统支持的算法:
1 2 |
$ openssl ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:SRP-DSS-AES-256-CBC-SHA:SRP-RSA-AES-256-CBC-SHA:SRP-AES-256-CBC-SHA:DH-DSS-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DH-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DH-RSA-AES256-SHA256:DH-DSS-AES256-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DH-RSA-AES256-SHA:DH-DSS-AES256-SHA:DHE-RSA-CAMELLIA256-SHA:DHE-DSS-CAMELLIA256-SHA:…… |
但是你可能说这不是在逗我吗?这么多!谁搞得清楚,而且还要兼顾兼容性和安全性。不要急,伟大的 mozilla 公司已经提供了一个可视化配置页面:https://ssl-config.mozilla.org/,根据自身服务器环境和需求,简单填写几个参数就可以生成一份完美配置。
这时我们的配置项变成了这样(我这里后续修改过,可能和自动生成的配置文件有点点差别):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
server { listen 443 ssl spdy default_server; listen [::]:443 ssl spdy default_server; # SSL configuration ssl_certificate /path/ssl/example.com.cert.full.pem; ssl_certificate_key /path/ssl/example.com.key.pem; # don’t use SSLv3 ref: POODLE ssl_protocols TLSv1 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:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256: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-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA"; ssl_dhparam /usr/local/ssl/certs/dhparam.pem; ssl_prefer_server_ciphers on; ssl_session_timeout 1d; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; # HSTS (ngx_http_headers_module is required) add_header Strict-Transport-Security "max-age=63072000" always; # OCSP stapling ssl_stapling on; ssl_stapling_verify on; # verify chain of trust of OCSP response using Root CA and Intermediate certs ssl_trusted_certificate /path/ssl/example.com.cert.ca.pem; # To minimize Time To First Byte ssl_buffer_size 4k; # others add_header X-Frame-Options sameorigin always; add_header X-Content-Type-Options nosniff; …… } |
我们来看看都增加了些什么。
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 安全穿越不安全网络的方法)做密钥交换。生成方法:
1 |
openssl dhparam -out dhparam.pem 2048 # 如果你的机器性能足够强大,可以用 4096 位加密 |
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 说起》)。验证是否配置成功的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ echo QUIT | openssl s_client -connect www.baidu.com:443 -status 2> /dev/null | grep -A 17 'OCSP response' OCSP response: ====================================== OCSP Response Data: OCSP Response Status: successful (0x0) Response Type: Basic OCSP Response Version: 1 (0x0) Responder Id: 167E9BC262392BB7F6C9AC7778709D4F3471201C Produced At: Dec 27 22:09:05 2015 GMT Responses: Certificate ID: Hash Algorithm: sha1 Issuer Name Hash: D4B43B8E3D02491A65506F967314DDE8594452E4 Issuer Key Hash: D79B7CD822A015F7DDAD5FCE299B58C3BC4600B5 Serial Number: 7629AA20FA8A8E7624A21936F4AD1AAA Cert Status: good This Update: Dec 27 22:09:05 2015 GMT Next Update: Jan 3 22:09:05 2016 GMT |
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/):
还不错 A+,页面生成的报表还包含一些兼容性测试结果和改进意见,值得关注一下。
本文更新历史
Update 2016-07-05:
最近证书到期了,renew 时把证书从 RSA 加密换成了 ECC 加密(椭圆曲线,安全性和性能更好),这里简单记录一下:
签证书的命令和之前略有不同:
1 |
openssl ecparam -genkey -name prime256v1 -out iyaozhen.com.ecc.key |
-name
参数指定具体的算法实现,openssl ecparam -list_curves
命令可以显示当前 openssl 版本(建议升级到最新版)支持的算法,同类的算法 key 的位数越大越安全,当然也更影响性能。你也可以在 https://www.ssllabs.com/ssltest/viewMyClient.html 查看自己浏览器支持的算法,辅助选择。我这里选择的是prime256v1
(=secp256r1)也是 Nginx 当前版本默认的值。
生成 CSR:
1 |
openssl req -new -sha256 -key iyaozhen.com.ecc.key -out iyaozhen.com.ecc.csr |
和之前一样按照提示输入一些必要信息即可,然后提交 CSR 文件,几分钟新的证书就签下来了,替换掉之前的证书后 sudo nginx -s reload
重新加载配置即可。
新的证书:
Update 2020-01:
更新配置(安全)说明、acme.sh 使用、证书区别、TLS 握手过程。
参考资料
百度全站 https FAQ- 技术宅告诉你如何搜索更安全
全站 https 时代的号角 : 大型网站的 https 实践系列 - 系列文章(4 篇),强烈建议阅读、研究
Deprecation of SHA-1 and moving to SHA-2
Should disable compression when using SSL
Enable SPDY header compression
BOTCHING FORWARD SECRECY - The sad state of server-side TLS Session Resumption implementations
Cloudflare's Internet facing SSL cipher configuration