送走了 8102 年,大量活化石网站依然不启用 TLS。好在 9102 年已经扑面而来,无论这些技术领域的封建老僵尸是否关注,新技术总会不断发展。

本文旨在提供一个渐进式指南,让人能够从最基础的正确监听端口开始,逐步增添一些离博物馆比较遥远的配置,形成一个在 9102 年看上去不太老土的范式。

当然,你应该以实际需求为导向,适时停下来。我们并非试图把网站变成新玩意的试验田,只是希望 9102 年的新人在搜索教程时,不要再复制到 SSLv3。

注:

  1. 为了操作简便,本文所涉及软件使用主流软件包管理器中默认源包含的较新版本,而非需要自行编译的最新版本。
  2. 为了内容集中,本文尽量避免一些功能性的内容,例如 PHP、Python 等脚本语言或 404 页面、访问日志的配置。
  3. 文章中的网站使用本站域名 yvesx.com 作为示例,但本站实际使用的配置并非文中所述。

跑起来再说

如果你的 Nginx 没有在安装完成后自启动,要把服务启动一下。用浏览器访问 IP 地址,能看到 Nginx 的默认页面。

把域名解析指向服务器(可能需要一点时间生效),然后配置最基本的信息:监听端口、域名、网站目录。

server {
    listen 80;
    root /your_website_root_directory/;
    server_name www.your_domain.com;
}

建议使用 nginx -t 检查一下明显的语法错误。(它不是万无一失的,尤其对于爱用 if 语法的人——记住,你正在编辑的只是一份配置文档,它不是一个编程语言。)

正确保存配置文件以后,软重启 Nginx 服务即可使其生效。如:systemctl reload nginx

在浏览器中输入域名访问你的网站,如果成功,那么它已经是一个看上去还算正常的网站了。

简单配置 TLS

启用 TLS 根本没有很多人想像的那样麻烦,在得到证书与私钥后,只需两行指定。

server {
    listen 80;
    listen 443 ssl;

    root /your_website_root_directory/;
    server_name www.your_domain.com;

    ssl_certificate /your_certificate_directory/your_certificate.cer;
    ssl_certificate_key /your_certificate_directory/your_private_key_of_the_certificate.key;
    # 显然,证书与私钥的路径是任意的。它们既不一定在同一目录下,也不一定是 *.cer 与 *.key
}

依然要检测和软重启生效 Nginx 配置,后文不再就此赘述。

你可以在浏览器中访问 https://www.your_domain.com/ 观察 TLS 部署是否成功。

统一入口

第一,除了 www.your_domain.com,我们还应该正确解析 your_domain.com。第二,我们应该对这两个域名同时支持 http://https:// 的正常访问。第三,这样一来任何页面或资源都有了 4 种可用的 URL,所以我们要让它们统一到同一个上面。

一般的思路是在监听 80 端口、监听 443 端口并分别配置好 TLS 的基础上,使用 301 跳转到你想要的 URL。例如:


# 对于裸域,监听 80 端口与 443 端口,一律跳转。
server {
    listen 80;
    listen 443 ssl;

    server_name your_domain.com;

    ssl_certificate /your_certificate_directory/your_certificate.cer;
    ssl_certificate_key /your_certificate_directory/your_private_key_of_the_certificate.key;
    # 注意这里的证书与私钥都是签给裸域的。

    return 301 https://$host$request_uri;
}

# 对于 www 的 80 端口也要跳转。
server {
    listen 80;
    server_name www.your_domain.com;

    return 301 https://$host$request_uri;
}

# www 的 TLS 是我们要的“正式”访问方式。
server {
    listen 443 ssl;

    root /your_website_root_directory/;
    server_name www.your_domain.com;

    ssl_certificate /your_certificate_directory/your_certificate.cer;
    ssl_certificate_key /your_certificate_directory/your_private_key_of_the_certificate.key;
    # 注意这里的证书与私钥都是签给 www 主机名的。
}

比较容易出错的地方是,对于带 www 与无 www 的两个域名,你需要正确配置两张匹配的证书。

更快更安全更现代的 TLS

很多人启用 TLS 是为了安全(还有一些人是认为绿锁好看),但不幸的是,旧的协议并不安全。(其实不止如此,OpenSSl 历史上的几次安全事件严重动摇了人们对开源软件的看法,以往大家都天真地以为开源软件意味着人人审计、共同维护,这会带来更好的安全性,但残酷的现实是,一个人人都在用的开源组件漏洞百出,被利用很长时间才在社区中有反响。)
很多人不用 TLS 是为了性能,但欣慰的是,我们有缓存机制。

在客户端,可能存在使用 OCSP 实时查询证书有效性的情况。这无疑相当影响性能。我们启用 OCSP Stapling,让服务器获取查询结果并缓存起来,以直接提供给客户端,绕过它自行查询的过程,而保留了安全性。

Session Ticket 是一种加速 TLS 握手的技术,它将服务端加密的会话信息保存在客户端,于是客户端将该信息发送到服务端时,服务端能够解密并认可客户端,从而跳过证书交换过程。它的问题在于,多服务器需要使用相同的 Ticket Key 才能实现复用,而为了安全性它们又应该定时更换,一些人会忽略这一点,引入额外的安全隐患。这里我们已经有了缓存,证书交换频率不高,索性关闭这一特性。

让我们回顾一下要做的事情:

  1. 使用会话缓存提高性能
  2. 指定较新的 TLS 实现版本提高安全性
  3. 启用 OCSP Stapling 提高性能
  4. 关闭 Session Ticket 减小潜在风险(这不是必要的)

配置如下:

server {
    listen 80;
    listen 443 ssl;

    server_name your_domain.com;

    ssl_certificate /your_certificate_directory/your_certificate.cer;
    ssl_certificate_key /your_certificate_directory/your_private_key_of_the_certificate.key;

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

    ssl_protocols TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
    ssl_prefer_server_ciphers on;

    ssl_stapling on;
    ssl_stapling_verify on;

    return 301 https://$host$request_uri;
}

server {
    listen 80;
    server_name www.your_domain.com;

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;

    root /your_website_root_directory/;
    server_name www.your_domain.com;

    ssl_certificate /your_certificate_directory/your_certificate.cer;
    ssl_certificate_key /your_certificate_directory/your_private_key_of_the_certificate.key;

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

    ssl_protocols TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
    ssl_prefer_server_ciphers on;

    ssl_stapling on;
    ssl_stapling_verify on;
}

我们将相同的两段配置给了两处 TLS,这其中刚好没有包含路径、域名之类需要替换的东西,你也可以直接复制这一段,粘贴到对应位置。

注意:旧的浏览器或操作系统并不支持新的协议。使用 TLSv1.2,意味着网站最低只能支持到:Firefox 27、Chrome 30、IE 11(Windows 7)、Opera 17、Safari 9、Android 5.0、Java 8(还有 Edge)

在检查 Nginx 配置时,你可能会遇到这样一条警告:
nginx: [warn] "ssl_stapling" ignored, issuer certificate not found

这是你的证书没有正确配置。一般来说,你应该指定完整证书链(包括 CA 证书),或者另行指定上级信任链(使用 ssl_trusted_certificate 字段)。但你可能对证书机制了解得不多,而证书与证书看上去都是一样的,很容易在此出错,所以建议直接给 ssl_certificate 字段指定完整证书链文件。

为你的 TLS 设置 HSTS

HSTS(HTTP Strict Transport Security)策略约定,你可以设置一个 HTTP 响应头消息,告诉浏览器在一定时长内强制 HTTPS 方式访问。“强制”的意思是将所有 HTTP 方式的访问改换成 HTTPS,如果在此期间你的 TLS 配置错误,会导致无法访问网站。

显而易见,HSTS 可以减小中间人攻击的可能性,并降低从 HTTP 301 跳转到 HTTPS 的频率。

HSTS 的设置非常简单,只需增加一个 HTTP 类似这样的响应头消息:strict-transport-security: max-age=15768000; includeSubDomains。此处的 15768000 可以自定义(秒),我们选的这个数刚好是六个月。includeSubDomains 不是必须的,如果加上它,HSTS 策略也会启用在子域上。

注意:上面给出的是 HTTP 响应头消息,并不是 Nginx 配置。要在 Nginx 里设置这个响应头,应该增加下述行:
add_header Strict-Transport-Security max-age=15768000;

完整配置如下:

server {
    listen 80;
    listen 443 ssl;

    server_name your_domain.com;

    add_header Strict-Transport-Security max-age=15768000;

    ssl_certificate /your_certificate_directory/your_certificate.cer;
    ssl_certificate_key /your_certificate_directory/your_private_key_of_the_certificate.key;

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

    ssl_protocols TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
    ssl_prefer_server_ciphers on;

    ssl_stapling on;
    ssl_stapling_verify on;

    return 301 https://$host$request_uri;
}

server {
    listen 80;
    server_name www.your_domain.com;

    add_header Strict-Transport-Security max-age=15768000;

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;

    root /your_website_root_directory/;
    server_name www.your_domain.com;

    add_header Strict-Transport-Security max-age=15768000;

    ssl_certificate /your_certificate_directory/your_certificate.cer;
    ssl_certificate_key /your_certificate_directory/your_private_key_of_the_certificate.key;

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

    ssl_protocols TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
    ssl_prefer_server_ciphers on;

    ssl_stapling on;
    ssl_stapling_verify on;
}

注意:这个响应头消息应该出现在所有情况下,包括那个只监听 80 端口的 server。

如果你对信息安全足够敏锐,可能会意识到,HSTS 目前的机制需要在首次访问后生效,并不能杜绝降级可能。

这个顾虑是必要的。为此,Chrome(准确讲是 Chromium)维护了一个预载表,将启用 HSTS 的网站收录其中并硬编码给浏览器,以便浏览器在初次遇见你的域名时就能正确执行 HSTS 策略。

目前预载表的收录要求是:时间长达一年(max-age=31536000)、包含子域名(includeSubDomains)、包含一个标识(preload)。在 Nginx 配置文件中应该这样写:add_header Strict-Transport-Security "max-age=15768000;includeSubDomains;preload";

如果你已经准备好将自己的域名提交到这个列表中,请访问这里

你可以在Chromium 预载表源文件中查询到本站域名 yvesx.com还挺靠前

注意:被预载表收录以后,你的域名只有在正确配置 TLS 的情况下才能访问。由于这是浏览器内置的列表,相较于单纯的 HSTS 来说更加不可控,你可能会遇到因证书错误而无法访问网站、自己没有办法临时绕过的尴尬情况。

中场休息:跑个分试试

TLS 的配置到这里就基本结束了,为了检验成果,你可以在Qualys SSL Labs测评一下。

如果你完全按照本教程走,现在拥有一个高性能、安全、现代的 TLS 配置,必然能拿到 A+。

Hi,你也在这里吗?