Nginx + SSL 的配置优化

近期,博客服务器由阿里云华北区迁移至阿里云华南区,并启用了阿里云的 OSS 对象存储和 CDN 节点加速,系统由Ubuntu 14.04 升级至 Ubuntu 16.04 并打上所有安全补丁,采用全站 HTTPS 安全连接。因此在迁移前,SSL 评估时得分为 B 以下,因为OpenSSL版本低,导致安全性不足,此次迁移后索性通过 SSL LabsMySSL 两个 SSL 安全评估站点对自己的网站进行了检测,发现得分为 A。但仍有一些配置可以优化,本篇文章就记录一下我是如何配置 Nginx 中的 SSL,提升其安全性。本文需要一定的 SSL 配置基础,在此之前请确认你的 Web 服务器已经开启了 HTTPS。

简要介绍

HTTPS:我们简单地可以认为是 HTTP + TLS。HTTP 协议目前已经广泛地运用在大部分的 Web 应用和网站,随着线上支付、移动支付越来越流行,对网络传输安全要求也提升了不少。

TLS:是一种传输层加密协议,它的前身是 SSL 协议,于 1999 年正式更名为 TLS 。TLS 协议主要有五部分:应用数据层协议,握手协议,报警协议,加密消息确认协议,心跳协议。

目前常用的 HTTP 协议是 HTTP 1.1,前几年还有 HTTP 1.0,如今 HTTP 2 已经推出,但目前设备支持还需要点时间。

常用的 TLS 协议版本有:TLS1.2,TLS1.1,TLS1.0 和 SSL3.0。其中 SSL3.0 由于 POODLE 攻击已经被证实不安全。TLS1.0 也存在部分安全漏洞,比如 RC4 和 BEAST 攻击。

配置 SSL 证书

SSL 证书许多地方都能申请,包括免费的和收费的。这里介绍一些可以申请免费 SSL 证书的服务提供商。本站所使用的证书是来自阿里云的 Symantec Basic DV SSL 证书,年限为 1 年。使用的比较多的还有腾讯云,Let’s EncryptStart SSLAWS 等。

申请成功后,部分提供商会提供各种适配版本的压缩包。如“for Nginx”,“for IIS”,“for Apache”等等。由于本站使用的 Web 服务为 Nginx,因此下载为 Nginx 提供的压缩包可以得到两个证书文件,一个以“.key”为后缀的文件和一个以“.pem”为后缀的文件。

在 nginx 的配置文件中 server 中添加,并重启 Nginx 即可使用 HTTPS:

ssl on;
ssl_certificate /root/ssl/214262594******.pem;
ssl_certificate_key /root/ssl/214262594******.key;

HSTS (HTTP Strict Transport Security)

我们简单地可以将 HSTS 理解为服务器告诉浏览器这个网站支持 HTTPS,从而强制客户端(如浏览器)使用 HTTPS 与服务器创建连接,不允许使用 HTTP。实现原理是当客户端发出请求时,在服务器返回的超文本传输协议响应头中包含 Strict-Transport-Security 字段,浏览器获取到 HSTS 头部之后,在一段时间内,不管用户是以 HTTP 请求还是以 HTTPS 请求,浏览器都会默认将请求跳转到 HTTPS。

Strict-Transport-Security: max-age=expireTime ; includeSubDomains ; preload

max-age:HSTS 在客户端的生效时间,单位是秒。浏览器缓存后在生效时间内,回自动通过 HTTPS 访问站点。

includeSubDomains:可选参数,如果指定这个参数,表明这个网站所有子域名也必须通过HTTPS协议来访问。

preload:可选参数,让站点进入浏览器内置使用 的 HTTPS 域名列表,使得第一次访问时就会启用 HSTS。

需要注意的是,HSTS 策略只能在 HTTPS 响应中进行设置,网站必须使用 443 端口,且必须使用域名,不可使用 IP 地址。如果在 HTTP 明文响应中允许设置 HSTS 头,中间人攻击者就可以通过在普通站点中注入 HSTS 信息来执行 DoS 攻击。

Nginx 上启用 HSTS 只需要将 HTTP 强制跳转到 HTTPS,并在 HTTPS 中设置 HSTS 的头字段。

server {
   listen 443 ssl;
   listen [::]:443 ssl;
   root /var/******;
   index index.php index.html index.htm;
   server_name laijingwu.com www.laijingwu.com pcie.xyz;
   ssl on;
   ssl_certificate /root/ssl/214262594******.pem;
   ssl_certificate_key /root/ssl/214262594******.key;
   add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
...
}

server {
   listen 80;
   server_name laijingwu.com www.laijingwu.com pcie.xyz;
   location / {
        rewrite ^/(.*)$ https://www.laijingwu.com/$1 permanent;
    }
...
}

OCSP Stapling

OCSP,在线证书状态检查协议 (RFC6960),用于向 CA 站点查询证书状态,例如证书是否被吊销。通常情况下,浏览器使用 OCSP 协议发起查询请求,CA 返回证书状态内容,然后浏览器接受证书是否可信的状态。而开启 OCSP Stapling 后,服务器会将证书有效状态的信息缓存到服务器,浏览器请求时候直接通过自己的服务器发送回去,防止验证服务器出问题,提高 TLS 握手速度。Nginx 中其中几个参数:

ssl_trusted_certificate /root/ssl/******.pem;
# OCSP Stapling 的证书位置。证书链,用于验证 OCSP 响应的各个 CA 证书和中级证书,和信任的 CA 根证书列表。当用于验证 OCSP 响应的时候,应该配置为你的 CA 根证书和中级 CA 证书的列表,此处可以简单和ssl_certificate 使用同一个证书列表文件。如果使用阿里云云盾服务可以通过证书的“其他”下载完整的证书文件。
ssl_stapling on;
# OCSP Stapling 开启
ssl_stapling_verify on;
# OCSP Stapling 验证开启
resolver 223.5.5.5 114.114.114.114 valid=300s;
# 用于查询 OCSP 服务器的 DNS
resolver_timeout 5s;
# 查询域名超时时间

Nginx 上启用 OSCP:

server {
   listen 443 ssl;
   listen [::]:443 ssl;
   root /var/******;
   index index.php index.html index.htm;
   server_name laijingwu.com www.laijingwu.com pcie.xyz;
   ssl on;
   ssl_certificate /root/ssl/214262594******.pem;
   ssl_certificate_key /root/ssl/214262594******.key;
   ssl_trusted_certificate /root/ssl/******.pem;
   ssl_stapling on;
   ssl_stapling_verify on;
   resolver 223.5.5.5 114.114.114.114 valid=300s;
   resolver_timeout 5s;
   add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
...
}

Session Cache

Session Cache 原理是使用请求中的 Session ID 查询服务端的 Session Cache,如果服务端有对应的缓存,则直接使用已有的 Session 信息提前完成握手,称为简化握手。SSL 握手是非常消耗 CPU 资源的 SSL 操作之一,通过在并发连接或后续连接中重用 SSL 会话参数,可以避免 SSL 握手的操作,从而将每个客户端的握手操作数量降到最低。

Session Cache 缺点:需要消耗服务器内存来存储 Session 内容,且目前有些开源软件只支持单机多进程间共享缓存,不支持多机间分布式缓存。对于大型互联网公司而言,单机的 Session Cache 作用很小。

Nginx 中,我们可以使用 ssl_session_cache 和 ssl_session_timeout 配置缓存大小和缓存超时时间。根据 Nginx 官方文档,1M 内存大约能缓存 4000 个会话,默认缓存超时时间为5分钟。我们可以将其配置在 nginx.conf 中。

http {
...
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
...
}

协议支持

SSL 协议默认支持的是 SSLv3,TLSv1,TLSv1.1,TLSv1.2。因为 SSLv3 的一些安全问题,所以注意禁用 SSLv3。

Nginx 中通过 ssl_protocols 配置 TLS 版本。

http {
...
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
...
}

HTTP 2

为了提高 HTTPS 网站性能,可以选择启用 HTTP 2,尽管现在 HTTP 2 普及度还不够,但相信不远的将来会慢慢普及开。

Nginx 要启用 HTTP 2,首先要查看安装的 Nginx 版本是否支持 HTTP2,可以通过 nginx -V 查看安装的模块,检查其中是否包括 –with-http_v2_module。如果已经包含,可以在 HTTPS 的站点配置文件中,listen 后面加上 http2。示例如下:

server {
   listen 443 ssl http2;
   listen [::]:443 ssl http2;
   root /var/******;
   index index.php index.html index.htm;
   server_name laijingwu.com www.laijingwu.com pcie.xyz;
   ssl on;
   ssl_certificate /root/ssl/214262594******.pem;
   ssl_certificate_key /root/ssl/214262594******.key;
   add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
...
}

X-Frame-Options

简单地说,X-Frame-Options 是应用于响应头中用于控制当前页面是否可以被其他页面以 frame 方式引入。其可设置的值有(大小写不敏感):

deny:禁止其他所有页面引用,主要针对不适用 frame 的情景。

sameorigin:仅对同源可加载,对不同源则不能加载,主要针对需要使用 frame 的情景。

Nginx 配置时需要修改站点配置文件:

server {
   listen 443 ssl http2;
   listen [::]:443 ssl http2;
   root /var/******;
   index index.php index.html index.htm;
   server_name laijingwu.com www.laijingwu.com pcie.xyz;
   ssl on;
   ssl_certificate /root/ssl/214262594******.pem;
   ssl_certificate_key /root/ssl/214262594******.key;
   add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
   add_header X-Frame-Options deny;
...
}

X-Content-Type-Options

加载互联网上的资源,通常浏览器会根据响应头的 Content-Type 字段来分辨它们的类型。例如:”text/html” 代表 HTML 文档,”image/jpeg” 是 JPG 图片等等。然而,有些资源的 Content-Type 是错的或者未定义的。遇到这些资源,某些浏览器会启用 MIME-sniffing 来猜测该资源的类型,解析内容并执行。 例如,我们即使给一个 HTML 文档指定 Content-Type 为 ”text/plain”,在 IE8 中这个文档依然会被当做 HTML 来解析。利用浏览器的这个特性,攻击者可以使原本应解析为图片的请求被解析为 JavaScript 代码执行。通过设置 X-Content-Type-Options 头可以禁用浏览器的类型猜测行为。Nginx 配置如下:

server {
   listen 443 ssl http2;
   listen [::]:443 ssl http2;
   root /var/******;
   index index.php index.html index.htm;
   server_name laijingwu.com www.laijingwu.com pcie.xyz;
   ssl on;
   ssl_certificate /root/ssl/214262594******.pem;
   ssl_certificate_key /root/ssl/214262594******.key;
   add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
   add_header X-Frame-Options deny;
   add_header X-Content-Type-Options nosniff;
...
}

X-XSS-Protection

如果尝试在页面搜索框插入 JS 脚本,将会被 Chrome 拦截, 而在 Firefox 上则成功执行。 这是由于没有显式指定 X-XSS-Protection 头造成的不同浏览器的不同处理。为了解决这一情况,我们需要显式指定 X-XSS-Protection 头来开启 XSS 保护,禁止 JS 代码执行,防止 XSS。

server {
   listen 443 ssl http2;
   listen [::]:443 ssl http2;
   root /var/******;
   index index.php index.html index.htm;
   server_name laijingwu.com www.laijingwu.com pcie.xyz;
   ssl on;
   ssl_certificate /root/ssl/214262594******.pem;
   ssl_certificate_key /root/ssl/214262594******.key;
   add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
   add_header X-Frame-Options deny;
   add_header X-Content-Type-Options nosniff;
   add_header X-XSS-Protection 1;
...
}

CSP (Content Security Policy)

现如今,跨域脚本攻击 XSS 是最常见、危害最大的网页安全漏洞。 CSP 是一种由开发者定义的安全性政策性申明,通过CSP 所约束的的规则指定可信的内容来源(可以是脚本、图片、iframe、front、style 等等可能的远程资源)。通过 CSP 协定,让 Web 处于一个安全的运行环境中。CSP 的实质就是白名单制度,开发者明确告诉客户端,哪些外部资源可以加载和执行。它的实现和执行全部由浏览器完成。

启用 CSP 有两种方式,一种是通过 HTTP 头中的 Content-Security-Policy 字段。

Content-Security-Policy: default-src https:;

另一种是通过网页的 标签。

<meta http-equiv="Content-Security-Policy" content="default-src https:">

详细介绍我们可以看看知乎的文章 Content Security Policy (CSP) 是什么?为什么它能抵御 XSS 攻击?

如果是 Nginx 上配置,也是和前面一样,只需要加一条头字段即可。

server {
   listen 443 ssl http2;
   listen [::]:443 ssl http2;
   root /var/******;
   index index.php index.html index.htm;
   server_name laijingwu.com www.laijingwu.com pcie.xyz;
   ssl on;
   ssl_certificate /root/ssl/214262594******.pem;
   ssl_certificate_key /root/ssl/214262594******.key;
   add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
   add_header X-Frame-Options deny;
   add_header X-XSS-Protection 1;
   add_header Content-Security-Policy "default-src https:; connect-src https:; font-src *.laijingwu.com https: data:; frame-src https:; img-src *.laijingwu.com https: data:; media-src *.laijingwu.com https:; object-src https:; script-src *.laijingwu.com 'unsafe-inline' 'unsafe-eval' https:; style-src *.laijingwu.com 'unsafe-inline' https:;";
...
}

DH-Key(证书密钥交换密钥)

一般网站使用的 SSL 证书都是 RSA 证书,这种证书基本都是 2048 位的密钥,但是证书密钥交换密钥必须要比证书密钥更长才能安全,而默认的只有 1024 位,所以我们需要手动生成一个更强的密钥。

openssl dhparam -out dhparam.pem 4096

Nginx 中的 nginx.conf 配置文件中的 http 中引入该密钥。

http {
...
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_dhparam /root/ssl/******;
...
}

通过 SSL 的配置优化后,使用文章开头说的 SSL LabsMySSL 等工具测试,得到了一个不错的分数。

附上一份完整的站点配置文件供参考。

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    root /var/******;
    index index.php index.html index.htm;
    server_name laijingwu.com www.laijingwu.com pcie.xyz;
    ssl on;
    ssl_certificate /root/ssl/214262594******.pem;
    ssl_certificate_key /root/ssl/214262594******.key;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
    add_header X-Frame-Options deny;
    add_header X-XSS-Protection 1;
    add_header Content-Security-Policy "default-src https:; connect-src https:; font-src *.laijingwu.com https: data:; frame-src https:; img-src *.laijingwu.com https: data:; media-src *.laijingwu.com https:; object-src https:; script-src *.laijingwu.com 'unsafe-inline' 'unsafe-eval' https:; style-src *.laijingwu.com 'unsafe-inline' https:;";

    if ($host ~* www.laijingwu.com) { rewrite ^/(.*)$ https://laijingwu.com/$1 permanent; }

    access_log /var/log/nginx/***.access.log;
    error_log /var/log/nginx/***.error.log;

    error_page 404 /404.html;
    error_page 500 502 503 504 /50x.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        try_files $uri $uri/ =404;
        fastcgi_pass unix:/run/php/php7.1-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ ^/(images|javascript|js|css|flash|media|static)/ {
        expires 6h;
    }

    location ~ /\.ht {
        deny all;
    }
}