在网站升级到 HTTPS 之后,我们还可以有很多玩意可以折腾,优化 HTTPS,让它更快更安全。这里是一篇 HTTPS 优化的总结,也包含问题的解决方法,不过不仅仅包括 HTTPS 的优化,也包含 HTTP 一些安全相关的配置。

因为平时用 Nginx 比较多,本文涉及到 Web Server 的大多数例子都会以 Nginx 为例。如果有错误欢迎指出。HTTPS 发展很快,尤其是在谷歌的推动之下,如果有过时的地方,也请指出。

HSTS

HSTS(HTTP Strict Transport Security)介绍

浏览器在访问站点的时候,如果没有指定 HTTPS 访问,会默认使用 HTTP,所以我们会将 HTTP 重定向(301或302)到 HTTPS。这样看起来没有问题,但是当使用重定向进行跳转时,网站就存在被劫持的可能。

因此有了 HSTS, 采用 HSTS 协议的网站将保证浏览器始终连接到网站的HTTPS版本,而不需要用户手动在URL地址栏中输入包含https://的加密地址,实现了一种新的跳转方式(浏览器识别后直接跳转),用户访问到的直接就是 HTTPS 的版本。HSTS 还可以用来防止基于 SSL Strip 的中间人攻击。

HSTS 的 HTTP 头部格式如下:

Strict-Transport-Security: max-age=<expire-time>[; includeSubDomains][; preload]
  • max-age:有效时间;浏览器记住的有效时间
  • includeSubDomains:是否包含子域名;可选参数
  • preload:是否预加载;可选参数

使用 HSTS

只需要加入相应的 HTTP 头部信息就可以。比如我们配置以头部信息:

Strict-Transport-Security: max-age=31536000; includeSubDomains

以 Nginx 为例可以这样配置:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

HSTS Preloading

介绍

在上一条 HSTS 中,我们实现了浏览器维持 HTTPS 连接,但是仍然存在一个问题,如果我们是第一次访问该站点呢?那浏览器并不知道该站点的配置,所以也就不知道应该用 HTTPS 去连接,这个问题怎么解决?

HSTS Preload List,是一个谷歌维护的列表,现在大部分主流浏览器都支持这个列表,这个列表直接告诉浏览器要用 HTTPS 访问的站点有哪些,所以在访问站点之前,浏览器先捞一遍这个列表,如果要访问的站点在这里面,就直接用 HTTPS 进行访问,所以即使是第一次访问,也会走 HTTPS 了。

加入Preload List

只需要前往这个站点 HSTS Preload List (可能需要科学上网访问)提交你的站点就可以,通过之后就加入 HSTS 预加载列表了。

在提交之前,你需要注意以下几点:

  1. 提供有效的站点证书

  2. 将 HTTP 重定向到 HTTPS

  3. 所有的子域名也都要支持 HTTPS

  4. HSTS 头部配置需要:

    1. max-age 需要至少 31536000 秒 (1年)
    2. 必须包含includeSubDomains参数
    3. 必须包含preload参数

    比如 Nginx 为例修改配置为:

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    

注意事项

加入预加载列表时候要想从列表中删除,需要很长的时间,如果你只是暂时玩玩 HTTPS,之后还会切换回 HTTP,需要谨慎考虑。

HTTP/2

HTTP/2 介绍

即 HTTP 2.0,是下一代的 HTTP 协议,目前大量采用的是 HTTP 1.1,HTTP/2 现在只支持 HTTPS 开启。

HTTP/2 有这些特点:

  • 彻底的二进制协议,头信息和数据体都是二进制
  • 多路复用请求
  • 对请求划分优先级
  • 压缩HTTP头
  • 服务器推送流(即Server Push技术)
  • 保持与HTTP 1.1语义的向后兼容性

其中有些新东西都处于摸索阶段,比如Server Push技术。目前 Nginx 1.13.9版本中已经包含了 Server Push,参考 Introducing HTTP/2 Server Push with NGINX 1.13.9;但是 Nginx 当前的 stable 版本是 1.12,我准备在未来 1.13 的 stable 版本中再开启,相信性能又会有一定的提升。

上 HTTP/2 给我们带来的最直观的体验就是,极大地加快了站点页面的加载速度。

使用 HTTP/2

如果是使用 Nginx,我们可以非常方便地就直接升级到 HTTP/2,只需要注意以下几点:

  • HTTP2 现在需要 HTTPS
  • HTTP2 要求 Nginx 版本是1.9.5以上
  • openssl 版本要求1.0.2

然后,修改Nginx的配置:在监听端口的配置 listen 443 ssl 后面加上http2 default_server就行了。

注意事项

如果在 Chrome51 版本的 Chrome 浏览器中,HTTP/2不生效,检查一下是否支持 ALPN,支持 ALPN 需要开启 OCSP Stapling。

OCSP Stapling

OCSP Stapling 是什么

OCSP (Online Certificate Status Protocol) 通常是 CA 提供来实时验证证书是否合法有效的。客户端就可以根据证书中的 OCSP 信息,发送查询请求到 CA 的在线验证地址来查询证书是否有效。OCSP 的问题在于,对 CA 机构的验证接口高可用性有要求,增加了浏览器握手的延时。

OCSP Stapling 技术是对 OCSP协议 的进一步升级。服务器事先模拟浏览器对证书链进行验证,然后将 OCSP 验证结果缓存到本地。这样,当浏览器访问站点时,在握手阶段,可以直接拿到 OCSP 响应结果和证书链,就不需要再向 CA 请求接口,对访问速度有明显提升。

开启 OCSP Stapling

检测 OCSP Stapling 的状态:

openssl s_client -connect [yoursite.com]:443 -status

如果支持 OCSP Stapling 会看到OCSP Response Data内有以下内容:

OCSP Response Status: successful (0x0)

而如果不支持,不会有OCSP Response Data的内容。

Nginx 中开启 OCSP Stapling

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /path/to/certs/chained.pem;

如果 ssl_certificate 指令指定了完整的证书链,则 ssl_trusted_certificate 可省略。例如:

server
{
    listen 443 ssl;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    ssl_certificate /etc/ssl/bundle.crt;
    ssl_certificate_key /etc/ssl/your_domain_name.key;

    ssl_stapling on;
    ssl_stapling_verify on;
}

重启 Nginx 即可生效。

Apache 中开启 OCSP Stapling

<VirtualHost></VirtualHost> 中添加:

SSLUseStapling on

<VirtualHost></VirtualHost> 外添加:

SSLStaplingCache shmcb:/tmp/stapling_cache(128000)

例如:

SSLStaplingCache shmcb:/tmp/stapling_cache(128000)
<VirtualHost *:443>
SSLEngine on
SSLProtocol all -SSLv3 -SSLv2

SSLCertificateFile /path/to/your_domain_name.crt
SSLCertificateKeyFile /path/to/your_private.key
SSLCertificateChainFile /path/to/root.crt

SSLUseStapling on
</VirtualHost>

然后重启 Apache。

TLS False Start

TLS False Start 意味着抢先开始。在 TLS 握手的过程中,客户端在发送 Change Cipher Spec 和 Finished,即握手完成前,就开始发送应用层的请求数据,服务端在 TLS 握手完成时直接返回响应数据。

开启 TLS False Start 非常简单,以 Nginx 为例,在配置中加上:

ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA256:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EDH+aRSA+AESGCM:EDH+aRSA+SHA256:EDH+aRSA:EECDH:!aNULL:!eNULL:!MEDIUM:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS:!RC4:!SEED;

ssl_ciphers 加密方式的配置因人而异,可以参考一些资料琢磨一下。

Session resumption

Session ID resumption

如果客户端和服务器端都保存了 session keys,我们就可以重用加密的 session。通过给每个连接一个唯一标识,服务端可以知道一个进来的连接是否在之前已经建立过连接,如果在服务器中也存在这个 session 的 key,那么它就能重用。

重用 Session ID 需要服务器保存 Session 状态等,这样下次连接才能复用,这就需要服务器保存很多状态信息,所以耗费内存。

重用 Session ID 在 Apache 中可以通过 SSLSessionCache 配置,在 Nginx 中可以通过 ssl_session_cache 设置。

以 Nginx 为例,我们配置以下内容:

  • ssl_session_cache 设置储存SSL会话的缓存类型和大小。默认值为 ssl_session_cache off,off为关闭,还有一些其它的缓存类型,不过这里建议使用shared共享缓存类型,这种方法更为有效。
  • ssl_session_timeout 客户端能够反复使用储存在缓存中的会话参数时间

例如:

ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;

共享缓存,缓存大小为50m,缓存时间1天。

Session ticket resumption

在 Session ticket 重用中,服务器不需要保存所有的创建的 session 的状态信息,反而将状态保存成块状数据交给客户端来维护。Session tickets 允许服务器将某些信息存储到客户端,类似于HTTP cookies 在信息验证的应用。

Session ticket 就是加密的存储了重用 TLS 连接所需要的信息的块数据(比如 session keys)。通常使用只有服务器才知道的 ticket key 来加密。

服务器在初始握手期间向客户端发送 Session ticket 以便本地存储。当重用 session 时,客户端会将 Session ticket 发送回服务器交给服务器进行解密,然后恢复会话。

复用 session ticket 在 Apache 中可以通过 SSLTicketKeyDefault 配置,在 Nginx 中可以通过 ssl_session_tickets 设置。
在 Nginx 中,例:

ssl_session_tickets on;
ssl_session_ticket_key current.key;
ssl_session_ticket_key previous.key;

Nginx 中使用 ssl_session_ticket_key file; 来配置用于加密和解密 SSL session_ticket的密钥,如果用了多个指令文件,则仅第一个指令文件中的密钥用来加密和解密;其它的密钥文件(下面的)用来解密,这样的原因是,我们最好定期轮换加解密的 key,轮换的时候把旧的放在下面用来解密旧的 ticket,第一个放新的,用来加解密新的请求。
如果没有配置 key 文件,则 openssl 默认会在 ssl 初始化的时候生成随机数的 key;这种时候只有在重启 web server 的时候才会重新生成随机 key。

Session ID resumption 与 Session ticket resumption

复用 session ticket 和 复用 session ID 的区别在于,复用 session ID 时在服务器和客户端存储了 key,连接时比对两边的数据是否一致;而 session ticket 将数据加密后存储在客户端,客户端请求时带回数据让服务器解密,正常则复用,只有发布的服务端能够解密该数据。

如果在握手阶段 session ID 和 session ticket 都提供了,将以 session ticket 为准,如果在 session ticket 阶段被 pass 掉了才通过 session id 取 cache 中的信息来复用。

Public-Key-Pins

Public-Key-Pins 用来做什么呢

任何一家受信任的 CA 都可以签发任意网站的站点证书,浏览器识别起来都是合法的,这些受信任的 CA 可以签发任意网站的站点证书(包括你的站点),而这些受信任的 CA 有很多,如果某 CA 中的某链被攻破,就可以造成由伪造或不正当手段获得网站证书的中间人攻击。

所以 Public-Key-Pins 就是用来告诉浏览器当前网站的证书指纹,包括配置过期时间,在过期时间内,浏览器再次访问这个网站的话就必须验证证书链中的证书指纹,如果跟之前指定的证书指纹不匹配,就无法访问。

如果我们自己更换了证书呢?为了避免这个情况导致的问题,所以我们在配置指纹的时候,至少配置两个,其中包含一个备用指纹。

关于 HTTP Public Key Pinning (HPKP)的介绍,这里非常详细:Public_Key_Pinning

Public-Key-Pins 的 HTTP 头的格式如下:

Public-Key-Pins: pin-sha256="base64=="; max-age=expireTime [; includeSubDomains][; report-uri="reportURI"]
  • pin-sha256:Base64加密的证书指纹;一般情况至少指定两个,其中包含一个备用指纹。
  • max-age:过期时间,秒
  • includeSubDomains:是否包含子域
  • report-uri:可选参数;验证失败时的上报地址

pin-sha256证书指纹这个配置很容器搞错,我们需要指定一个备用指纹,而这个指纹并不是当前域名证书链中的指纹,应该是一个不在当前链中,未来有可能更换到该链的备用指纹。比如我的证书链,根证书是 DigiCert Global Root CA,中间证书是 Encryption Everywhere DV TLS CA - G1,再加上域名证书,我就可以配置中间证书为第一个pin-sha256,而第二个证书配置 Let’s Encrypt 的证书指纹,这样以后如果用 Let’s Encrypt Authority 签发证书,老用户不会受到影响。

这里 HTTP Public Key Pinning: You’re doing it wrong! 非常详细地介绍了在配置这个配置时会出现的问题。

使用

生成pin-sha256指纹,可以通过以下的方式来生成:
通过 RSA key 文件生成:

openssl rsa -in my-rsa-key-file.key -outform der -pubout | openssl dgst -sha256 -binary | openssl enc -base64

通过 ECC key 文件生成:

openssl ec -in my-ecc-key-file.key -outform der -pubout | openssl dgst -sha256 -binary | openssl enc -base64

通过 CSR 文件生成:

openssl req -in my-signing-request.csr -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64

通过域名之间生成:

openssl s_client -servername www.example.com -connect www.example.com:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64

生成证书指纹之后就可以将指纹加入到配置中。

Apache配置:

Header always set Public-Key-Pins "pin-sha256=\"base64+primary==\"; pin-sha256=\"base64+backup==\"; max-age=5184000; includeSubDomains"

Nginx配置:

add_header Public-Key-Pins 'pin-sha256="base64+primary=="; pin-sha256="base64+backup=="; max-age=5184000; includeSubDomains' always;

DNS CAA

DNS CAA 是什么

CAA(Certificate Authority Authorization),即证书颁发机构授权。简单地说,就是当域名的 DNS 解析存在 CAA 记录时,则只允许在记录中列出的 CA 机构颁发针对该域名(或子域名)的证书。CAA 记录可以控制单域名 SS L证书的发行,也可以控制通配符证书。

所以设置了 CAA,如果有一天想更换非 CAA 记录中的 CA,要记得把 DNS CAA 的解析记录消掉。否则颁发会失败。

设置 CAA

目前国内的云服务中,阿里云支持 CAA 的 DNS 解析,因为我本人用的阿里云,所以其它云服务产商不太了解,在 DNS 解析处看看就知道支持不支持了,如果不支持想添加的话也可以换用支持的 DNS 服务商来解决。

添加 DNS 解析,选择 CAA 类型,填写通配符还是非通配符的,然后就是记录值了。如下图:

关于记录值,我们可以简单地使用 CAA Record Helper 来自动生成,非常方便。将生成的记录值(比如 0 issue "digicert.com" 这样的)填入到 DNS 解析中既可。

X-Frame-Options 响应头

X-Frame-Options HTTP 响应头是用来给浏览器指示是否允许一个页面在 <frame><iframe> 或者 <object> 中展现的标记。通过设置 X-Frame-Options HTTP 响应头,我们可以确保自己的网站内容没有被嵌到别人的网站中去,也从而避免了点击劫持 (clickjacking) 的攻击。

X-Frame-Options 有三个值:

  • DENY
    表示该页面不允许在 frame 中展示,即便是在相同域名的页面中嵌套也不允许。
  • SAMEORIGIN
    表示该页面可以在相同域名页面的 frame 中展示。
  • ALLOW-FROM uri
    表示该页面可以在指定来源的 frame 中展示。

就是说,如果设置为 DENY,不光在别人的网站 frame 嵌入时会无法加载,在同域名页面中同样会无法加载;如果设置为 SAMEORIGIN,那么页面就可以在同域名页面的 frame 中嵌套。

CSP Level 2 规范中的 frame-ancestors 指令会替代这个非标准的 header。CSP 的 frame-ancestors 会在 Gecko 4.0 中支持,但是并不会被所有浏览器支持。然而 X-Frame-Options 是个已广泛支持的非官方标准,可以和 CSP 结合使用。

Apache 配置

要把下面这行添加到“site”的配置中:

Header always append X-Frame-Options SAMEORIGIN

Nginx 配置

在“http”,“server”或者“location”的配置中:

add_header X-Frame-Options SAMEORIGIN;

X-Content-Type-Options 响应头

我们都知道 Content-Type 是用来标识资源类型的。浏览器有个特性,就是当有些资源的 Content-Type 没定义或者定义错了,浏览器会启用 MIME sniffing 来检测该资源的类型然后解析内容并执行。所以攻击者可以利用浏览器这个特性让原本的请求中的资源类型解析为其它类型,所以一般情况下我们都禁止浏览器去检测类型:

X-Content-Type-Options: nosniff

在 Nginx 中我们可以加上配置:

add_header X-Content-Type-Options nosniff;

Server Banner

服务器版本号不应该存在在响应头中,这样容易让攻击者找到弱点。

Nginx 中可以增加以下配置:

server_tokens off;

就会去除版本号,比如 nginx/1.10.3 就变成了 ningx.

Web framework Information

和服务器版本号一样,我们应该移除一些头部框架信息,比如 X-Powered-By,X-Runtime,X-Version,X-AspNet-Version等。

在 Nginx 中加配置:

proxy_hide_header X-Powered-By;

如果是 fastcgi 模式的 PHP 应用则用:

fastcgi_hide_header X-Powered-By;

在 PHP 配置 php.ini 中移除版本号,可以设置 expose_php:

expose_php = off

X-XSS-Protection 响应头

XSS Protection 显然是用来防止 XSS 攻击的,这个不需要多解释了。

我们只需要知道,现在主流浏览器都支持,并且默认都开启了 XSS 保护。配置这个响应头可以将它关闭,但是如果你没有更加好的防范 XSS 的解决方案,就留着吧。

在 Nginx 中配置启用XSS保护,并在检查到XSS攻击时,停止渲染页面:

add_header X-Xss-Protection "1; mode=block";

Content Security Policy

Content Security Policy(CSP),是用来有效防止 XSS 攻击的,实际上就是提供了一个白名单告诉客户端,哪些外部资源可以加载和执行。通过页面的<meta>标签和 HTTP 的 Content-Security-Policy 头信息的都可以控制 CSP。

关于 CSP,我觉得阮一峰老师的这篇文章挺不错的:Content Security Policy 入门教程,以及 google developer 的:内容安全政策

CSP 的配置需要根据自己的实际情况来配置。这里我就举个例子:

Content-Security-Policy: default-src 'self' blob: data:; script-src 'self' 'unsafe-inline' *.baidu.com; img-src *

这里定义所有的资源类型都默认只能从当前域名加载('self'),然后定义了 blob:data:(实际上如果不是非要加载字体 font 等,使用要谨慎,攻击者很容器利用data: URIs注入攻击) 数据类型也允许加载;script-src 定义了只能从当前域名加载,'unsafe-inline' 是为了使用监听事件,允许加载百度的域名 *.baidu.com(使用百度统计的话);img-src * 而图片文件可以从任何域名加载。

以上只是随便举一个例子,这一套配置需要自己来定制。而且这个配置挺麻烦的,需要自己慢慢调试,特别是如果引用了很多外部资源,不过在安全上起到很好的效果。

Cache Control

Cache Control 表示输出页面的缓存首选项。强烈推荐自定义缓存的偏好,如果不定义 Cache Control,将由浏览器或者代理来选择是否缓存内存,而这种不受控制的选择有可能会导致性能问题或安全问题。

每个资源都可通过 Cache-Control HTTP 标头定义其缓存策略,Cache-Control 指令控制谁在什么条件下可以缓存响应以及可以缓存多久。

Cache-Control 是一个灵活的配置,我们应该根据自己的需求定义最佳 Cache-Control 策略,参考 HTTP 缓存

Secure Cookies

如果站定已经是基于 HTTPS 的,包含敏感信息的 cookie,特别是 session id(在使用 session时,会在客户端存储一个cookie,记录 session id),需要被标记为安全的。
例如以下这个设置 cookie 的头:

set-cookie: PHPSESSID=03196cccbf3a8cd7d4fb22214fc5111e; path=/

HTTPS 站点加上 secure 标志:

set-cookie: PHPSESSID=03196cccbf3a8cd7d4fb22214fc5111e; path=/; secure; 

这样只有在 HTTPS 下 cookie 才能生效。另一种防范不安全的 cookie 通过 HTTP 传送的方式是 HSTS,上面已经提到过了,建议同时开启 HSTS 和 secure cookie。

Session cookies 应该标记上 HttpOnly,防止通过 JavaScript 访问,攻击者可以利用这点进行 XSS 攻击窃取 Session cookies。其它的 cookie 可以不这么严格地标记,但是除非有从 JavaScript 去访问的需求,都建议标记上 HttpOnly。
例如上面的 set-cookie,我们需要修改成以下的头信息:

set-cookie: PHPSESSID=03196cccbf3a8cd7d4fb22214fc5111e; path=/; secure; HttpOnly

不要混用 HTTPS 和 HTTP

最后一点,不要 HTTPS 和 HTTP 混用,既然上了 HTTPS,还留 HTTP 做什么呢?为了 SEO 么?

随着时间推移,越早全站 HTTPS 优势只会越明显。

其它优化操作

SRI(Subresource Integrity)

子资源完整性(SRI)是允许浏览器检查其获得的资源(例如从 CDN 获得的)是否被篡改的一项安全特性。它通过验证获取文件的哈希值是否和你提供的哈希值一样来判断资源是否被篡改。

SRI 目前的浏览器兼容性不好,而且会增加代码和配置的复杂度。可以参考:Subresource Integrity

Iframe Sandbox

Iframe Sandbox是html5的新属性,是专门为 iframe 定制的,如果你用了 iframe,才建议加上这个属性。可以参考:iframe

总结

基本上以上内容做完就差不多了,SSLLabs 上测试肯定是 A+ 的。先到这里,以后再看看有没有什么要修改或者补充的。

测试站点

本文参考资料