Nginx关于https的相关配置


web安全的话题是绕不开https的,即使站点的安全配置很完备,也无法阻止中间人对header和html文档的更改。https用于web消息传输的加密,使得中间人查看明文信息的成本大大提高,相应的降低了各种攻击的可能性。但并不是拥有https就万无一失了,无论是软件漏洞还是密钥强度或是配置的疏忽,都依然会成为短板的存在。持续了解web的各种配置,也是对自已知识范围的扩展,本篇文章,列举一些关于本站的https相关配置。

本站是从Let's Encrypt机构申请的https证书,它是一个免费的证书申请机构,并且被主流浏览器所支持。每一次证书的发行有效期为90天,续订支持自动化脚本。官方推荐使用Certbot进行证书的自动申请及续订。本着折腾的想法,本站使用acme-tiny管理续订。

申请证书

  1. 创建私钥

    # mkdir -p <path>/https/
    # cd <path>/https
    
    openssl genrsa 4096 > account.key #生成私钥
    
    openssl genrsa 4096 > domain.key  #生成证书签名请求文件
    
    # openssl版本1.1.1及以上
    openssl req -new -sha256 -key domain.key -subj "/" -addext "subjectAltName = DNS:<example>.com, DNS:www.<example>.com" > domain.csr
    
    # openssl1.1.1以下版本
    # openssl req -new -sha256 -key domain.key -subj "/" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:<example>.com,DNS:www.<example>.com")) > domain.csr
    
    # openssl.conf的位置可能不同,可使用以下命令查看路径
    # openssl version -a | grep OPENSSLDIR
    
  2. 申请证书前的准备

    申请过程中需要生成标识文件,Let's Encrypt会通过http://<域名>/.well-known/acme-challenge/<文件名>访问该文件,以此来证明申请者对域名的拥有权,因此需要配置nginx以使文件可以访问

    server {
        listen              80;
        listen              [::]:80;
        server_name         <example>.com www.<example>.com;
    
        location /.well-known/acme-challenge/ {
            alias <path>/challenge/;
            try_files $uri =404;
        }
    }
    
  3. 申请证书

    # mkdir -p <path>/challenge/
    # cd <path>/https
    
    wget -O acme_tiny.py https://raw.githubusercontent.com/diafygi/acme-tiny/master/acme_tiny.py
    
    python acme_tiny.py --account-key ./account.key --csr ./domain.csr --acme-dir <path>/challenge/ > ./signed_chain.crt
    

至此满足了开启https的最基本条件。可以通过以下配置开启https服务

server {
    listen                      443 ssl;
    listen                      [::]:443 ssl;
    server_name                 <example>.com www.<example>.com;

    ssl_certificate             <path>/https/signed_chain.crt;
    ssl_certificate_key         <path>/https/domain.key;

    # ...其他配置
}

server {
    listen                      80;
    listen                      [::]:80;
    server_name                 <example>.com www.<example>.com;

    location /.well-known/acme-challenge/ {
        alias <path>/challenge/;
        try_files $uri =404;
    }

    location / {
        rewrite       ^/(.*)$ https://$http_host/$1 permanent;
    }
}

Diffie–Hellman密钥交换

https中客户端与服务端在证书协商完成后才会发送请求,证书协商要求服务器发送公钥到客户端再由双方确认用于加密后续信息的密钥。diffie-hellman密钥交换算法用于证书协商,以保证密钥生成过程的安全性,openssl默认使用1024位强度,应该至少保证强度在2048以上。如果服务器计算力足够,可以提升到4096。

# cd <path>/https

openssl dhparam -out dhparam.pem 2048

之后在nginx增加配置

ssl_dhparam         <path>/https/dhparam.pem;

禁用低版本的TLS

低版本的TLS已经存在太多安全相关的漏洞,随着时间的推移,TLS1和1.1版本会逐步被服务商和浏览器禁用。从安全角度考虑,服务端应该只支持TLS1.2及以上版本。但是高版本号也就存在着与旧版浏览器的兼容问题,是否要启用对低版本的支持,需要在兼容和安全方面有所取舍。

ssl_protocols               TLSv1.2 TLSv1.3;
ssl_ciphers                 ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers   off;
  • ssl_ciphers 客户端与服务端在TLS握手阶段会通过双方加密套件的交集确定要使用的加密算法,顺序越靠前优先级越高。如果不存在交集,握手会失败

  • ssl_prefer_server_ciphers 加密算法协商时,服务器的算法优先级会高于客户端,以防止客户端使用不安全的算法作为高优先级算法。

    如果服务器支持TLS1.2以下版本,应该开启ssl_prefer_server_ciphers。在1.2及以上版本弱密码的套件已经被弃用,所以这个选项不是必须开启的。

Online Certificate Status Protocol(在线证书状态协议)

服务器的证书是否是合法的,客户端实现会向证书所属CA发送请求,以验证服务端的证书是否可以被信任。然而它使认证过程变得更长。OCSP Stapling由服务端主动查询OCSP并把结果发送给客户端,以尽量提升网站的访问速度。在开启OCSP Stapling之前,要先制作证书链,需要证书CA的中间证书和根证书。因此本站需要下载Let's Encrypt的中间证书以及根证书

# cd <path>/https

wget -O trustid-x3-root.pem https://letsencrypt.org/certs/trustid-x3-root.pem.txt
wget -O x3-cross-ocsp.pem https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt

cat trustid-x3-root.pem >> x3-cross-ocsp.pem

之后在nginx中开启OCSP

ssl_stapling                on;
ssl_stapling_verify         on;
ssl_trusted_certificate     <path>/https/x3-cross-ocsp.pem;

resolver                    8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout            10s;

resolver 服务端获取OCSP需要向目标域名发送请求,因此需要能够解析其主机名

重用TLS会话

访问https站点,TLS需要握手,验证,协商等一系列操作。将之前的session缓存起来并且重复使用,达到优化访问速度的目的。

ssl_session_cache           shared:SSL:10m;
ssl_session_timeout         1d;
ssl_session_tickets         off;
  • ssl_session_cache 服务端缓存session,缓存大小10M。nginx官方文档有提到1M约可以缓存4000个session的说明

  • ssl_session_timeout 缓存超时时间为1天

  • ssl_session_tickets 关闭ticket缓存,ticket缓存是由服务端将session信息加密发送给客户端,由客户端存储,之后客户端向服务器提交ticket,服务器重用解密后的session。但这可能会破坏Forward Secrecy(前向保密)Why do you recommend disable ssl_session_tickets in NGINX?

HTTP Strict Transport Security(HTTP严格传输安全)

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

用户第一次访问站点可能是通过http访问的,服务端通常的做法是把用户重定向到https。但这一次的交互使用的是http,如果中间人更改了重定向的地址,用户就会被重定向到中间人希望他们访问的地址。HSTS用于强制浏览器在有效期内通过https访问当前域名,即使手动输入http://浏览器也会把http替换为https再进行访问,如果目标网站的证书过期,用户不能忽略浏览器的警告选择继续访问

  • max-age 过期时间,在此时间内访问该域名都自动替换https

  • includeSubDomains(可选) 表示所有子域名全都应用此规则

HSTS规则只有通过https返回的响应头,才会被浏览器认为是有效的,每次收到这个头部都会刷新过期时间。如果第一次http请求被劫持的话。HSTS是无能为力的,它只能在首次接收头部后,保证后续被劫持的风险。以下是nginx配置

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

总结

SSL Server Test用于测试https的强度和是否存在漏洞而广为人知的在线测试服务。

SSL Configuration GeneratorMozilla提供的主流代理Server的推荐配置

下面给出上述配置的完整版

server {
    listen                      443 ssl;
    listen                      [::]:443 ssl;
    server_name                 <example>.com www.<example>.com;

    ssl_certificate             <path>/https/signed_chain.crt;
    ssl_certificate_key         <path>/https/domain.key;

    ssl_dhparam                 <path>/https/dhparam.pem;

    ssl_protocols               TLSv1.2 TLSv1.3;
    ssl_ciphers                 ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers   off;

    ssl_stapling                on;
    ssl_stapling_verify         on;
    ssl_trusted_certificate     <path>/https/x3-cross-ocsp.pem;

    resolver                    8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout            10s;

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

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

    # ...其他配置
}

server {
    listen                      80;
    listen                      [::]:80;
    server_name                 <example>.com www.<example>.com;

    location /.well-known/acme-challenge/ {
        alias <path>/challenge/;
        try_files $uri =404;
    }

    location / {
        rewrite       ^/(.*)$ https://$http_host/$1 permanent;
    }
}