Nginx 配合 CloudFlare 获取真实 IP 并限制访问

在最近一次网站部署中,考虑到安全因素,希望将源站完全隐藏在 CF 后,只允许 CF 访问,并尽可能的减少信息泄漏。

获取真实客户端 IP

由于经过 CloudFlare 代理后,Nginx 看到的 remote ip 其实是 CF 的 IP 地址。因此需要通过 Nginx 的 ngx_http_realip_module,还原出真实的客户端 IP。这一步在 CloudFlare 有详细的文档说明

简单来说,当发现请求 IP 为 CF 的 IP 段时,读取 CF-Connecting-IP 头,并将其中的 IP 设置为客户端 IP。CF 的 IP 段可以通过 API 获取。以下是一个脚本可以生成相关的配置:

#!/bin/bash

# 脚本:获取 Cloudflare IP 列表并生成 Nginx 配置

# 输出文件
IP_CONFIG_FILE="cf_ips.conf"

# 清空输出文件
> "$IP_CONFIG_FILE"

# Cloudflare IP 列表 API 端点
CLOUDFLARE_API_URL="https://api.cloudflare.com/client/v4/ips"

# 生成 set_real_ip_from 配置
echo "# Cloudflare IPv4 IPs" >> "$IP_CONFIG_FILE"
curl -s "$CLOUDFLARE_API_URL" | jq -r '.result.ipv4_cidrs[]' | while read -r ip; do
    echo "set_real_ip_from $ip;" >> "$IP_CONFIG_FILE"
done

echo -e "\n# Cloudflare IPv6 IPs" >> "$IP_CONFIG_FILE"
curl -s "$CLOUDFLARE_API_URL" | jq -r '.result.ipv6_cidrs[]' | while read -r ip; do
    echo "set_real_ip_from $ip;" >> "$IP_CONFIG_FILE"
done

# 添加 real_ip_header 配置
echo -e "\n# Set real IP header" >> "$IP_CONFIG_FILE"
echo "real_ip_header CF-Connecting-IP;" >> "$IP_CONFIG_FILE"

脚本运行后会生成 cf_ips.conf 文件,内含 set_real_ip_fromreal_ip_header 配置,在 httpinclude 该文件,即可自动为所有 server 自动设置真实客户端 IP。

限制仅 CloudFlare IP 访问

解决了真实客户端 IP 的问题后,接下来就是如何做到仅限 CF IP 访问。由于该 Nginx 托管了多个 server,因此并不希望直接从防火墙层面拒绝所有非 CF IP 访问。那么从 Nginx 侧,第一时间想到的就是用 ngx_http_access_module 的方式,仅放过 CF IP 段:

# Cloudflare IPv4 Allow
allow 173.245.48.0/20;
allow 103.21.244.0/22;
allow 103.22.200.0/22;
allow 103.31.4.0/22;
allow 141.101.64.0/18;
allow 108.162.192.0/18;
allow 190.93.240.0/20;
allow 188.114.96.0/20;
allow 197.234.240.0/22;
allow 198.41.128.0/17;
allow 162.158.0.0/15;
allow 104.16.0.0/13;
allow 104.24.0.0/14;
allow 172.64.0.0/13;
allow 131.0.72.0/22;

# Cloudflare IPv6 Allow
allow 2400:cb00::/32;
allow 2606:4700::/32;
allow 2803:f800::/32;
allow 2405:b500::/32;
allow 2405:8100::/32;
allow 2a06:98c0::/29;
allow 2c0f:f248::/32;

# Default deny
deny all;

但是添加了这段配置后,发现不论是否通过 CF 访问,所有请求均会被拦截,返回 403。搜索相关文章1后发现,这是因为 ngx_http_realip_module 已经先执行完毕了,此时 ngx_http_access_module 拿到的已经是被修改了的真实客户端 IP,自然无法通过检测,被拒绝访问。

为了解决该问题,我们需要使用其他手段来进行访问控制。查阅相关资料后发现,ngx_http_geo_module 是个非常好的选择。相关配置如下:

geo $realip_remote_addr $is_cf {
  default 0;
  # Cloudflare IPv4 Allow
  173.245.48.0/20 1;
  103.21.244.0/22 1;
  103.22.200.0/22 1;
  103.31.4.0/22 1;
  141.101.64.0/18 1;
  108.162.192.0/18 1;
  190.93.240.0/20 1;
  188.114.96.0/20 1;
  197.234.240.0/22 1;
  198.41.128.0/17 1;
  162.158.0.0/15 1;
  104.16.0.0/13 1;
  104.24.0.0/14 1;
  172.64.0.0/13 1;
  131.0.72.0/22 1;

  # Cloudflare IPv6 Allow
  2400:cb00::/32 1;
  2606:4700::/32 1;
  2803:f800::/32 1;
  2405:b500::/32 1;
  2405:8100::/32 1;
  2a06:98c0::/29 1;
  2c0f:f248::/32 1;
}

其中,$realip_remote_addrngx_http_realip_module 提供的变量,存储了原始的客户端地址。通过 CF 访问的情况下,这个变量存储的就是原始的 CF 地址。$is_cf 是用于储存中间结果的变量。如果原始客户端地址在 CF IP 段,则会被设置为 1,否则为 0。

有了这个变量之后,我们就可以使用 ngx_http_rewrite_module 来进行判断,并拒绝访问。在需要的地方增加如下代码:

if ($is_cf = 0) {
    return 444;
}

即可拒绝掉所有非 CF 访问的情况。

使用 OpenResty

如果在使用 OpenResty,那么有更简单的方式2可以做这个事情。借助 access_by_lua_block,我们可以直接在 lua 里访问相关变量进行判断,并拒绝非 CF 的访问:

access_by_lua_block {
    if ngx.var.remote_addr == ngx.var.realip_remote_addr then
            return ngx.exit(ngx.HTTP_FORBIDDEN)
    end
}

原理其实也很简单,如果 ngx.var.remote_addrngx.var.realip_remote_addr 是相同的,则证明 ngx_http_realip_module 没有起作用,那么说明原始访问 IP 一定不在 CF IP 段内。

注意事项

  1. 有条件的情况下(即 Nginx 所有 server 都托管在 CF),应使用防火墙直接拒绝所有非 CF IP 的 TCP 链接
  2. 由于 if 指令只能使用在 serverlocation 使用,因此该方法在多个 server 内,需要重复配置
  3. 由于 ssl_reject_handshake 无法在 if 内使用,因此如果攻击者猜对了 server_name,那么证书信息依然会泄漏,存在被探测到源站的可能

参考文档

  1. https://serverfault.com/questions/601339/how-do-i-deny-all-requests-not-from-cloudflare/826428#826428 ↩︎
  2. https://stackoverflow.com/questions/39176931/nginx-allowdeny-realip-remote-addr/39183303#39183303 ↩︎

OpenResty AES 函数密钥生成原理

最近遇到一个场景,需要在 OpenResty 里使用 AES 对数据进行加密。但是看了一下官方提供的 AES 库,如果数据密码的话,并没有返回实际使用的 key 和 IV,因此查看了一下源码,研究了一下 key 生成的方式,方便其他语言使用。

密钥生成逻辑的核心代码在这:

function _M.new(self, key, salt, _cipher, _hash, hash_rounds, iv_len, enable_padding)

...

    local _hash = _hash or hash.md5
    local hash_rounds = hash_rounds or 1

...

        if C.EVP_BytesToKey(_cipher.method, _hash, salt, key, #key,
                            hash_rounds, gen_key, gen_iv)
            ~= _cipherLength
        then
            return nil, "failed to generate key and iv"
        end
end

其中最核心的部分就是使用了 EVP_BytesToKey 这个函数,进行密钥扩展。这个函数是 OpenSSL 库提供的函数,相关说明在这:

https://www.openssl.org/docs/man3.1/man3/EVP_BytesToKey.html

注意!该函数并不安全!一般不建议在生产中使用该函数!应当使用安全的密钥衍生函数(KDF)!

阅读文档,该函数的主要做法就是对提供的密码进行 hash,如果长度不够 key 或 iv 的要求,则继续进行 hash,直到长足足够为止。

第一轮 hash 直接拼接给定的密码和 salt,从第二轮开始,则将上一轮的结果附加在密码和 salt 前,然后再次进行 hash 操作。注意这里从第二轮开始附加的上一轮结果是二进制结果,如果希望手动使用工具计算的话,需要将 hex 编码或 base64 编码后的结果先进行解码,再附加在输入前。

用 Go 模拟这个过程的代码如下:

package main

import (
    "crypto/sha256"
    "fmt"
)

// EVPBytesToKey 模拟 OpenSSL EVP_BytesToKey 方法
func EVPBytesToKey(password, salt []byte, keyLen, ivLen int) (key, iv []byte) {
    var (
        concatenatedHashes []byte
        previousHash       []byte
        currentHash        []byte
        hash               = sha256.New()
    )

    for len(concatenatedHashes) < keyLen+ivLen {
        hash.Reset()

        // 如果不是第一轮,将上一轮的哈希值加到当前哈希的输入中
        if previousHash != nil {
            hash.Write(previousHash)
        }

        // 加入密码和盐值
        hash.Write(password)
        hash.Write(salt)

        // 当前哈希
        currentHash = hash.Sum(nil)

        // 添加到累积哈希中
        concatenatedHashes = append(concatenatedHashes, currentHash...)

        // 为下一轮准备
        previousHash = currentHash
    }

    // 提取密钥和 IV
    key = concatenatedHashes[:keyLen]
    iv = concatenatedHashes[keyLen : keyLen+ivLen]
    return key, iv
}

func main() {
    password := []byte("password")
    salt := []byte("salt")
    keyLen := 32 // AES-256 需要的密钥长度
    ivLen := 16  // AES 的 IV 长度

    key, iv := EVPBytesToKey(password, salt, keyLen, ivLen)
    fmt.Printf("Key: %x\nIV: %x\n", key, iv)
}

无痛解决 TeslaMate 访问 OpenStreetMap API 的问题

TeslaMate 是一款特斯拉汽车专用的增强工具,使用了破解的官方 App 的 API,将车机记录的大量信息进行了导出和展示,极大的增强了特斯拉车主的使用体验。

但是由于一些原因,中国特斯拉的汽车的数据都是存放在中国大陆,且不允许境外 IP 访问。因此,中国车主想要使用 TeslaMate,则必须在境内部署相关的服务。

不过,境内部署虽然可以解决访问 Tesla API 的问题,却也带来了新的问题,就是调用境外 API 的时候会出现错误,导致部分功能无法使用。

具体来说,TeslaMate 使用了 OpenStreetMap 提供的 API,用来将经纬度坐标转换为地名。然而,OpenStreetMap 的域名在境内是无法访问的,因此会导致解析坐标点的时候出现失败,导致在 Grafana 的 Drives(行程) 面板不显示行程的起止地址。

为了解决这个问题,我首先想到了设置 http_proxy 等环境变量的方式。但是搜索之后发现,似乎 TeslaMate 并不会读区这几个环境变量,因此无法使用这种方式强制走代理访问相关 API。而且,由于 Tesla 本身的 API 不能走代理访问,必须使用中国 IP 访问,因此如果要使用这种方式的话,还需要配置 Proxy 的分流策略,非常麻烦。

综合考虑各种方案,我最终选择了 sniproxy 的方式。sniproxy 是允许在不解密流量本身的情况下,进行 SSL/TLS 流量转发的方案。使用 sniproxy 时,只需要将目标域名的 IP 强制指向 sniproxy 的 IP 地址(如修改 /etc/hosts),既可以完成代理。

方案一:直接在本地运行 sniproxy

  • 优点:不占用母鸡的 443 端口,不影响现有服务
  • 缺点:配置较为繁琐,需要两个额外的容器互相配合

考虑到搭建 sniproxy 需要占用一个 IP 的 443 端口,因此我决定在 docker compose 内增加 sniproxy 的相关容器,来做到不占用母鸡的 443 端口的目的。由于 sniproxy 是运行在 TeslaMate 同一台机器上的,因此还需要一个额外的代理来将流量转发出去。这一步有很多方案,使用 warp+、ss、v2 等均可实现,我选择使用 v2 的方案。

最终修改后的 docker-compose.yaml 新增的内容如下:

  sniproxy:
    image: sniproxy
    restart: always
    command: ./sniproxy
    depends_on:
      - xray
    networks:
      default:
        aliases:
          - nominatim.openstreetmap.org

  xray:
    image: teddysun/xray:latest
    restart: always
    volumes:
      - ./xray:/etc/xray

其中,sniproxy 镜像是对 https://github.com/XIU2/SNIProxy 项目的简单包装,将配置文件写在了镜像内。配置内容如下:

# 监听端口(注意需要引号)
listen_addr: ":443"

# 可选:启用 Socks5 前置代理
enable_socks5: true
# 可选:配置 Socks5 代理地址
socks_addr: xray:40000

# 可选:允许所有域名(会忽略下面的 rules 列表)
#allow_all_hosts: true

# 可选:仅允许指定域名
rules:
  - openstreetmap.org

这里的 socks_addr 为下方 xray 容器内的 socks 代理监听地址。

这段配置的重点在于,sniproxy 容器配置了 network alias。这个选项的作用是,告诉 docker compose,将指定的域名解析到当前容器。因此,加上了这段配置后,docker 容器内解析 nominatim.openstreetmap.org 这个域名,会自动得到 sniproxy 容器的 docker 网络的 IP,达到将 OpenStreetMap 的 API 请求自动导向 sniproxy,进而导向 xray 代理的目的。

方案二:使用单独的服务器部署 sniproxy

  • 优点:配置简单,无需额外容器
  • 缺点:需要额外的服务器部署 sniproxy,需要占用一个 443 端口

当然,如果有多余的服务器,可以提供一个 443 端口的话,就不用这么麻烦了。直接在服务器上搭建好 sniproxy,然后在 docker-compose.yaml 内的 teslamate 容器配置里增加下面这段就可以了:

extra_hosts:
  nominatim.openstreetmap.org: sniproxy_server_ip

这段配置等同于在 teslamate 的 docker 容器的 /etc/hosts 文件内,写入了域名与 IP 的映射关系,因此所有的 OpenStreetMap 相关的 API 请求,都会被导向 sniproxy 的 IP 地址。

P.S. 其实使用 nginx 的 stream 模块,配合 stream_ssl stream_ssl_preread 两个模块,可以做到不影响现有的网站服务的前提下,与 sniproxy 兼容。但是此方法需要对现有的 nginx 配置进行较大的改动,而我本人的 nginx 配置有十几个文件,实在是不想改,因此就放弃了这种方式。

Nginx 诡异 SSL_PROTOCOL_ERROR 问题排查

这两天在检查一台 Nginx 配置的时候,遇到了一个极端诡异的问题。一段很通用的配置,配在这个服务器上,就会 100% 导致 Chrome 报 ERR_SSL_PROTOCOL_ERROR 。但是这段配置非常的通用,是用 Mozilla 提供的工具生成的。

而且在 iPhone 的 Safari 上访问又是完全正常的,服务器日志也看不到任何错误。看到的请求相应码也是完全正确的 200 。

先贴出配置:

# https://mozilla.github.io/server-side-tls/ssl-config-generator/
    listen 443 ssl http2;

    # certs sent to the client in SERVER HELLO are concatenated in ssl_certificate
    ssl_certificate /path/to/signed_cert_plus_intermediates;
    ssl_certificate_key /path/to/private_key;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;


    # modern configuration. tweak to your needs.
    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;

可以看到是在 Mozilla 网站上选择  和 Modern 生成出来的配置。

在测试过程中,排查了各种问题,包括但不限于 SSL 证书问题,HTTP Basic Auth 问题,http2 问题等等,然而都没有解决这个现象。

一次偶然的尝试,发现只要注释掉我给这个 server 特殊配置的这段逻辑,使用服务器通用的 ssl.ngx 文件中的 SSL 配置,就不会出现问题。于是开始先使用 ssl.ngx 文件中的配置,然后逐行替换,来查找具体出现问题的配置。

终于,当我将配置中的这行加上时,问题出现了:

ssl_session_tickets off;

于是以这个配置作为关键字搜索,找到了这么一篇文章:

https://community.letsencrypt.org/t/errors-from-browsers-with-ssl-session-tickets-off-nginx/18124/5

I’m posting this here both because this question was recently asked and because it took me many hours of troubleshooting to figure out the issue as while I found several references to the problem on Google, no one seemed to have a real solution. So here it is:

ssl_session_tokens off breaks if it’s not set the same for all ssl-enabled server{} blocks. So if you have 2 server configurations and and you have ssl_server_tokens set to on in one (which is the default so it counts even if you omit it) and set to off in another, it will break the one where it’s set to off in certain browsers. The easiest way to resolve this, unless you have multiple http{} blocks, is to just set it to off in the http{} block. I have not tested to see if you you can have different settings in different http{} blocks as I haven’t had need to set up more than one http{} block.

For others looking for this issue, I want to add that Chrome will respond with: ERR_SSL_PROTOCOL_ERROR while Firefox responds with: SSL_ERROR_RX_UNEXPECTED_NEW_SESSION_TICKET and curl responds with: gnutls_handshake() failed: An unexpected TLS packet was received. IE seemed to work, surprisingly.

简单翻译一下,这里是说,如果你的 nginx 开了多个 https 的 server,其中某些 server 没有配置 ssl_server_tokens off; ,而有些 server 配置了这个选项,那么就会导致没有手动 off 的 server 采用默认值 on,而手动 off 掉的 server 采用 off。这种情况会导致 nginx 和浏览器之间的握手出现问题,从而导致 Chrome 报出 ERR_SSL_PROTOCOL_ERROR ,FireFox 则会报出 SSL_ERROR_RX_UNEXPECTED_NEW_SESSION_TICKET 。

那么解决方法也很简单,只要在所有的 server 块统一这个配置就好了。要么都设置为 on,要么都设置为 off,问题解决。目前没有尝试多个 http 块隔离两个 server,建议还是将这个配置统一一下。

 

 

nginx rewrite 的一个小坑

今天在配置 Nginx 的时候写了这么一个 location

location /a {
    rewrite /a/(.*) /$1 break;
    ...
}

然后发现当我直接访问 /a 的时候,rewrite 并没有生效,后端收到的还是 /a 而不是我想象中的 / 。想了想可能是结尾 / 的问题,于是这样改:

location /a {
    rewrite /a(.*) $1 break;
    ...
}

结果新的问题来了,由于这样匹配到的 $1 是空的,所以 Nginx 报错了,the rewritten URI has a zero length

所以这种情况下只好这么写:

location /a/ {
    rewrite /a(.*) $1 break;
    ...
}

注意第一行的末尾 / 。这种情况下,访问 /a 会被 Nginx 自动重定向到 /a/ ,然后重写之后的 uri 就是 /,问题解决。

Nginx 配置 ECC RSA 双证书

Config Nginx for parallel ECC and RSA Certificate

先决条件

Nginx 1.11.0 以上

OpenSSL 1.0.2 以上

申请证书

首先申请 ECC 证书,这个不多说,很多方法都可以,大部分 CA 现在也都可以签署。生成 CSR 的命令是:

openssl ecparam -out 证书名.key -name prime256v1 -genkey && openssl req -new -key 证书名.key -nodes -out 证书名.csr

拿到证书之后,还是像之前一样将中级 CA 拼接在证书后面,得到给 Nginx 使用的 domain-cert.crt

配置 Nginx

首先是将两个证书链都加入 Nginx 的配置文件:

ssl_certificate     example.com.rsa.crt;
ssl_certificate_key example.com.rsa.key;

ssl_certificate     example.com.ecdsa.crt;
ssl_certificate_key example.com.ecdsa.key;

如果要使用 CT 的话有两种方法:

1. 将两个证书的 CT 信息放到同一目录,并做如下设置:

ssl_ct on;
ssl_ct_static_scts /path/to/sct/dir;

这样 Nginx CT 模块会自动在这个目录下查找相应证书的 CT 信息并发送

2. 可以单独配置每个证书的 CT 文件:

ssl_ct on;

ssl_certificate example.com.rsa.crt;
ssl_certificate_key example.com.rsa.key;
ssl_ct_static_scts xample.com.rsa.scts;

ssl_certificate example.com.ecdsa.crt;
ssl_certificate_key example.com.ecdsa.key;
ssl_ct_static_scts example.com.ecdsa.scts;

然后问题来了。很多盆友这么配置之后可能发现用 Chrome 之类的明明支持 ECC 的浏览器却并没有用 ECC 证书。这是为什么呢?

问题就出在  ssl_ciphers  这个配置项上面。

如果我们用各种网上推荐的配置,需要注意顺序问题。以 Cloud Flare 的配置为例:

#https://github.com/cloudflare/sslconfig/blob/master/conf
ssl_protocols               TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers                 EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers   on;

注意到这里面的算法都是优先使用 RSA 的,所以服务器和浏览器协商出来的一定是 RSA ,这就导致 Nginx 会自动发送 RSA 证书链给浏览器。

这里可以用 openssl 验证一下:

# openssl ciphers -V 'EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5' | column -t
0xC0,0x2F  -  ECDHE-RSA-AES128-GCM-SHA256    TLSv1.2  Kx=ECDH  Au=RSA    Enc=AESGCM(128)  Mac=AEAD
0xC0,0x2B  -  ECDHE-ECDSA-AES128-GCM-SHA256  TLSv1.2  Kx=ECDH  Au=ECDSA  Enc=AESGCM(128)  Mac=AEAD
0xC0,0x27  -  ECDHE-RSA-AES128-SHA256        TLSv1.2  Kx=ECDH  Au=RSA    Enc=AES(128)     Mac=SHA256
0xC0,0x23  -  ECDHE-ECDSA-AES128-SHA256      TLSv1.2  Kx=ECDH  Au=ECDSA  Enc=AES(128)     Mac=SHA256
0xC0,0x13  -  ECDHE-RSA-AES128-SHA           SSLv3    Kx=ECDH  Au=RSA    Enc=AES(128)     Mac=SHA1
0xC0,0x09  -  ECDHE-ECDSA-AES128-SHA         SSLv3    Kx=ECDH  Au=ECDSA  Enc=AES(128)     Mac=SHA1
0x00,0x9C  -  AES128-GCM-SHA256              TLSv1.2  Kx=RSA   Au=RSA    Enc=AESGCM(128)  Mac=AEAD
0x00,0x3C  -  AES128-SHA256                  TLSv1.2  Kx=RSA   Au=RSA    Enc=AES(128)     Mac=SHA256
0x00,0x2F  -  AES128-SHA                     SSLv3    Kx=RSA   Au=RSA    Enc=AES(128)     Mac=SHA1
0xC0,0x30  -  ECDHE-RSA-AES256-GCM-SHA384    TLSv1.2  Kx=ECDH  Au=RSA    Enc=AESGCM(256)  Mac=AEAD
0xC0,0x2C  -  ECDHE-ECDSA-AES256-GCM-SHA384  TLSv1.2  Kx=ECDH  Au=ECDSA  Enc=AESGCM(256)  Mac=AEAD
0xC0,0x28  -  ECDHE-RSA-AES256-SHA384        TLSv1.2  Kx=ECDH  Au=RSA    Enc=AES(256)     Mac=SHA384
0xC0,0x24  -  ECDHE-ECDSA-AES256-SHA384      TLSv1.2  Kx=ECDH  Au=ECDSA  Enc=AES(256)     Mac=SHA384
0xC0,0x14  -  ECDHE-RSA-AES256-SHA           SSLv3    Kx=ECDH  Au=RSA    Enc=AES(256)     Mac=SHA1
0xC0,0x0A  -  ECDHE-ECDSA-AES256-SHA         SSLv3    Kx=ECDH  Au=ECDSA  Enc=AES(256)     Mac=SHA1
0x00,0x9D  -  AES256-GCM-SHA384              TLSv1.2  Kx=RSA   Au=RSA    Enc=AESGCM(256)  Mac=AEAD
0x00,0x3D  -  AES256-SHA256                  TLSv1.2  Kx=RSA   Au=RSA    Enc=AES(256)     Mac=SHA256
0x00,0x35  -  AES256-SHA                     SSLv3    Kx=RSA   Au=RSA    Enc=AES(256)     Mac=SHA1
0xC0,0x12  -  ECDHE-RSA-DES-CBC3-SHA         SSLv3    Kx=ECDH  Au=RSA    Enc=3DES(168)    Mac=SHA1
0xC0,0x08  -  ECDHE-ECDSA-DES-CBC3-SHA       SSLv3    Kx=ECDH  Au=ECDSA  Enc=3DES(168)    Mac=SHA1
0x00,0x0A  -  DES-CBC3-SHA                   SSLv3    Kx=RSA   Au=RSA    Enc=3DES(168)    Mac=SHA1

会看到第一个选择就是使用 RSA,而不是椭圆曲线 ECDSA。

再来验证一下 Mozilla 给出的配置:

# openssl ciphers -V 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS' | column -t 
0xC0,0x2B  -  ECDHE-ECDSA-AES128-GCM-SHA256  TLSv1.2  Kx=ECDH  Au=ECDSA  Enc=AESGCM(128)  Mac=AEAD
0xC0,0x2F  -  ECDHE-RSA-AES128-GCM-SHA256    TLSv1.2  Kx=ECDH  Au=RSA    Enc=AESGCM(128)  Mac=AEAD
0xC0,0x2C  -  ECDHE-ECDSA-AES256-GCM-SHA384  TLSv1.2  Kx=ECDH  Au=ECDSA  Enc=AESGCM(256)  Mac=AEAD
0xC0,0x30  -  ECDHE-RSA-AES256-GCM-SHA384    TLSv1.2  Kx=ECDH  Au=RSA    Enc=AESGCM(256)  Mac=AEAD
0x00,0x9E  -  DHE-RSA-AES128-GCM-SHA256      TLSv1.2  Kx=DH    Au=RSA    Enc=AESGCM(128)  Mac=AEAD
0x00,0x9F  -  DHE-RSA-AES256-GCM-SHA384      TLSv1.2  Kx=DH    Au=RSA    Enc=AESGCM(256)  Mac=AEAD
0xC0,0x23  -  ECDHE-ECDSA-AES128-SHA256      TLSv1.2  Kx=ECDH  Au=ECDSA  Enc=AES(128)     Mac=SHA256
0xC0,0x27  -  ECDHE-RSA-AES128-SHA256        TLSv1.2  Kx=ECDH  Au=RSA    Enc=AES(128)     Mac=SHA256
0xC0,0x09  -  ECDHE-ECDSA-AES128-SHA         SSLv3    Kx=ECDH  Au=ECDSA  Enc=AES(128)     Mac=SHA1
0xC0,0x28  -  ECDHE-RSA-AES256-SHA384        TLSv1.2  Kx=ECDH  Au=RSA    Enc=AES(256)     Mac=SHA384
0xC0,0x13  -  ECDHE-RSA-AES128-SHA           SSLv3    Kx=ECDH  Au=RSA    Enc=AES(128)     Mac=SHA1
0xC0,0x24  -  ECDHE-ECDSA-AES256-SHA384      TLSv1.2  Kx=ECDH  Au=ECDSA  Enc=AES(256)     Mac=SHA384
0xC0,0x0A  -  ECDHE-ECDSA-AES256-SHA         SSLv3    Kx=ECDH  Au=ECDSA  Enc=AES(256)     Mac=SHA1
0xC0,0x14  -  ECDHE-RSA-AES256-SHA           SSLv3    Kx=ECDH  Au=RSA    Enc=AES(256)     Mac=SHA1
0x00,0x67  -  DHE-RSA-AES128-SHA256          TLSv1.2  Kx=DH    Au=RSA    Enc=AES(128)     Mac=SHA256
0x00,0x33  -  DHE-RSA-AES128-SHA             SSLv3    Kx=DH    Au=RSA    Enc=AES(128)     Mac=SHA1
0x00,0x6B  -  DHE-RSA-AES256-SHA256          TLSv1.2  Kx=DH    Au=RSA    Enc=AES(256)     Mac=SHA256
0x00,0x39  -  DHE-RSA-AES256-SHA             SSLv3    Kx=DH    Au=RSA    Enc=AES(256)     Mac=SHA1
0xC0,0x08  -  ECDHE-ECDSA-DES-CBC3-SHA       SSLv3    Kx=ECDH  Au=ECDSA  Enc=3DES(168)    Mac=SHA1
0xC0,0x12  -  ECDHE-RSA-DES-CBC3-SHA         SSLv3    Kx=ECDH  Au=RSA    Enc=3DES(168)    Mac=SHA1
0x00,0x16  -  EDH-RSA-DES-CBC3-SHA           SSLv3    Kx=DH    Au=RSA    Enc=3DES(168)    Mac=SHA1
0x00,0x9C  -  AES128-GCM-SHA256              TLSv1.2  Kx=RSA   Au=RSA    Enc=AESGCM(128)  Mac=AEAD
0x00,0x9D  -  AES256-GCM-SHA384              TLSv1.2  Kx=RSA   Au=RSA    Enc=AESGCM(256)  Mac=AEAD
0x00,0x3C  -  AES128-SHA256                  TLSv1.2  Kx=RSA   Au=RSA    Enc=AES(128)     Mac=SHA256
0x00,0x3D  -  AES256-SHA256                  TLSv1.2  Kx=RSA   Au=RSA    Enc=AES(256)     Mac=SHA256
0x00,0x2F  -  AES128-SHA                     SSLv3    Kx=RSA   Au=RSA    Enc=AES(128)     Mac=SHA1
0x00,0x35  -  AES256-SHA                     SSLv3    Kx=RSA   Au=RSA    Enc=AES(256)     Mac=SHA1
0x00,0x0A  -  DES-CBC3-SHA                   SSLv3    Kx=RSA   Au=RSA    Enc=3DES(168)    Mac=SHA1

在这份配置中可以看到,每种套件中使用椭圆曲线部分都被排在了 RSA 前面,所以能尽量协商出支持椭圆曲线的算法。

或者我们还可以更激进一些:

# openssl ciphers -V 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-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:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK' | column -t
0xC0,0x2B  -  ECDHE-ECDSA-AES128-GCM-SHA256  TLSv1.2  Kx=ECDH        Au=ECDSA  Enc=AESGCM(128)    Mac=AEAD
0xC0,0x2C  -  ECDHE-ECDSA-AES256-GCM-SHA384  TLSv1.2  Kx=ECDH        Au=ECDSA  Enc=AESGCM(256)    Mac=AEAD
0xC0,0x23  -  ECDHE-ECDSA-AES128-SHA256      TLSv1.2  Kx=ECDH        Au=ECDSA  Enc=AES(128)       Mac=SHA256
0xC0,0x09  -  ECDHE-ECDSA-AES128-SHA         SSLv3    Kx=ECDH        Au=ECDSA  Enc=AES(128)       Mac=SHA1
0xC0,0x24  -  ECDHE-ECDSA-AES256-SHA384      TLSv1.2  Kx=ECDH        Au=ECDSA  Enc=AES(256)       Mac=SHA384
0xC0,0x0A  -  ECDHE-ECDSA-AES256-SHA         SSLv3    Kx=ECDH        Au=ECDSA  Enc=AES(256)       Mac=SHA1
0xC0,0x2F  -  ECDHE-RSA-AES128-GCM-SHA256    TLSv1.2  Kx=ECDH        Au=RSA    Enc=AESGCM(128)    Mac=AEAD
0xC0,0x30  -  ECDHE-RSA-AES256-GCM-SHA384    TLSv1.2  Kx=ECDH        Au=RSA    Enc=AESGCM(256)    Mac=AEAD
0x00,0x9E  -  DHE-RSA-AES128-GCM-SHA256      TLSv1.2  Kx=DH          Au=RSA    Enc=AESGCM(128)    Mac=AEAD
0x00,0xA2  -  DHE-DSS-AES128-GCM-SHA256      TLSv1.2  Kx=DH          Au=DSS    Enc=AESGCM(128)    Mac=AEAD
0x00,0xA3  -  DHE-DSS-AES256-GCM-SHA384      TLSv1.2  Kx=DH          Au=DSS    Enc=AESGCM(256)    Mac=AEAD
0x00,0x9F  -  DHE-RSA-AES256-GCM-SHA384      TLSv1.2  Kx=DH          Au=RSA    Enc=AESGCM(256)    Mac=AEAD
0xC0,0x27  -  ECDHE-RSA-AES128-SHA256        TLSv1.2  Kx=ECDH        Au=RSA    Enc=AES(128)       Mac=SHA256
0xC0,0x13  -  ECDHE-RSA-AES128-SHA           SSLv3    Kx=ECDH        Au=RSA    Enc=AES(128)       Mac=SHA1
0xC0,0x28  -  ECDHE-RSA-AES256-SHA384        TLSv1.2  Kx=ECDH        Au=RSA    Enc=AES(256)       Mac=SHA384
0xC0,0x14  -  ECDHE-RSA-AES256-SHA           SSLv3    Kx=ECDH        Au=RSA    Enc=AES(256)       Mac=SHA1
0x00,0x67  -  DHE-RSA-AES128-SHA256          TLSv1.2  Kx=DH          Au=RSA    Enc=AES(128)       Mac=SHA256
0x00,0x33  -  DHE-RSA-AES128-SHA             SSLv3    Kx=DH          Au=RSA    Enc=AES(128)       Mac=SHA1
0x00,0x40  -  DHE-DSS-AES128-SHA256          TLSv1.2  Kx=DH          Au=DSS    Enc=AES(128)       Mac=SHA256
0x00,0x6B  -  DHE-RSA-AES256-SHA256          TLSv1.2  Kx=DH          Au=RSA    Enc=AES(256)       Mac=SHA256
0x00,0x38  -  DHE-DSS-AES256-SHA             SSLv3    Kx=DH          Au=DSS    Enc=AES(256)       Mac=SHA1
0x00,0x39  -  DHE-RSA-AES256-SHA             SSLv3    Kx=DH          Au=RSA    Enc=AES(256)       Mac=SHA1
0x00,0x9C  -  AES128-GCM-SHA256              TLSv1.2  Kx=RSA         Au=RSA    Enc=AESGCM(128)    Mac=AEAD
0x00,0x9D  -  AES256-GCM-SHA384              TLSv1.2  Kx=RSA         Au=RSA    Enc=AESGCM(256)    Mac=AEAD
0x00,0x3C  -  AES128-SHA256                  TLSv1.2  Kx=RSA         Au=RSA    Enc=AES(128)       Mac=SHA256
0x00,0x3D  -  AES256-SHA256                  TLSv1.2  Kx=RSA         Au=RSA    Enc=AES(256)       Mac=SHA256
0x00,0x2F  -  AES128-SHA                     SSLv3    Kx=RSA         Au=RSA    Enc=AES(128)       Mac=SHA1
0x00,0x35  -  AES256-SHA                     SSLv3    Kx=RSA         Au=RSA    Enc=AES(256)       Mac=SHA1
0x00,0x6A  -  DHE-DSS-AES256-SHA256          TLSv1.2  Kx=DH          Au=DSS    Enc=AES(256)       Mac=SHA256
0xC0,0x32  -  ECDH-RSA-AES256-GCM-SHA384     TLSv1.2  Kx=ECDH/RSA    Au=ECDH   Enc=AESGCM(256)    Mac=AEAD
0xC0,0x2E  -  ECDH-ECDSA-AES256-GCM-SHA384   TLSv1.2  Kx=ECDH/ECDSA  Au=ECDH   Enc=AESGCM(256)    Mac=AEAD
0xC0,0x2A  -  ECDH-RSA-AES256-SHA384         TLSv1.2  Kx=ECDH/RSA    Au=ECDH   Enc=AES(256)       Mac=SHA384
0xC0,0x26  -  ECDH-ECDSA-AES256-SHA384       TLSv1.2  Kx=ECDH/ECDSA  Au=ECDH   Enc=AES(256)       Mac=SHA384
0xC0,0x0F  -  ECDH-RSA-AES256-SHA            SSLv3    Kx=ECDH/RSA    Au=ECDH   Enc=AES(256)       Mac=SHA1
0xC0,0x05  -  ECDH-ECDSA-AES256-SHA          SSLv3    Kx=ECDH/ECDSA  Au=ECDH   Enc=AES(256)       Mac=SHA1
0x00,0x32  -  DHE-DSS-AES128-SHA             SSLv3    Kx=DH          Au=DSS    Enc=AES(128)       Mac=SHA1
0xC0,0x31  -  ECDH-RSA-AES128-GCM-SHA256     TLSv1.2  Kx=ECDH/RSA    Au=ECDH   Enc=AESGCM(128)    Mac=AEAD
0xC0,0x2D  -  ECDH-ECDSA-AES128-GCM-SHA256   TLSv1.2  Kx=ECDH/ECDSA  Au=ECDH   Enc=AESGCM(128)    Mac=AEAD
0xC0,0x29  -  ECDH-RSA-AES128-SHA256         TLSv1.2  Kx=ECDH/RSA    Au=ECDH   Enc=AES(128)       Mac=SHA256
0xC0,0x25  -  ECDH-ECDSA-AES128-SHA256       TLSv1.2  Kx=ECDH/ECDSA  Au=ECDH   Enc=AES(128)       Mac=SHA256
0xC0,0x0E  -  ECDH-RSA-AES128-SHA            SSLv3    Kx=ECDH/RSA    Au=ECDH   Enc=AES(128)       Mac=SHA1
0xC0,0x04  -  ECDH-ECDSA-AES128-SHA          SSLv3    Kx=ECDH/ECDSA  Au=ECDH   Enc=AES(128)       Mac=SHA1
0x00,0x88  -  DHE-RSA-CAMELLIA256-SHA        SSLv3    Kx=DH          Au=RSA    Enc=Camellia(256)  Mac=SHA1
0x00,0x87  -  DHE-DSS-CAMELLIA256-SHA        SSLv3    Kx=DH          Au=DSS    Enc=Camellia(256)  Mac=SHA1
0x00,0x84  -  CAMELLIA256-SHA                SSLv3    Kx=RSA         Au=RSA    Enc=Camellia(256)  Mac=SHA1
0x00,0x45  -  DHE-RSA-CAMELLIA128-SHA        SSLv3    Kx=DH          Au=RSA    Enc=Camellia(128)  Mac=SHA1
0x00,0x44  -  DHE-DSS-CAMELLIA128-SHA        SSLv3    Kx=DH          Au=DSS    Enc=Camellia(128)  Mac=SHA1
0x00,0x41  -  CAMELLIA128-SHA                SSLv3    Kx=RSA         Au=RSA    Enc=Camellia(128)  Mac=SHA1

可以看到在这份配置中,我几乎将所有的支持椭圆曲线的套件都提到了最前面,来加大协商出支持椭圆曲线算法的套件的可能性。

综上,推荐的配置如下:

ssl_ciphers 'EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+ECDSA+AES128:EECDH+aRSA+AES128:RSA+AES128:EECDH+ECDSA+AES256:EECDH+aRSA+AES256:RSA+AES256:EECDH+ECDSA+3DES:EECDH+aRSA+3DES:RSA+3DES:!MD5';
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-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:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';

三者任选其一即可。

 

参考:

https://imququ.com/post/ecc-certificate.html

https://www.zeroling.com/nginx-kai-qi-https-shuang-zheng-shu-zhi-nan/

https://n4l.pw/nginx-dual-certificates.html

https://imququ.com/post/certificate-transparency.html

https://seryo.net/use-ecc-algorithms-issued-a-certificate-request-file.Seryo

Nginx 配置 403 Error Page

今天遇到一个很有意思的问题,就是给 Nginx 配置 403 时候的错误页面。因为这台 Nginx 针对访问 IP 做了一些限制,所以需要给无权限访问的用户展示一个友好的界面。

一开始我的设置是这么写的:

...
allow 10.0.0.0/24;
allow 192.168.0.0/24;
deny 1.2.3.4;
deny 2.3.4.5;

error_page 403 /403.html;
...

可是实际使用发现,无论如何 Nginx 展示的都是内置 Hard Code 进去的那个 404 页面,并不是我想让他展示的友好的界面。

在排除了各种文件权限之类的错误之后,我 Google 了一下发现,这个真正的原因在于前面配置的 deny 不仅 deny 掉了正常页面的访问,同时也将对 /404.html 页面的访问也 deny 掉了。

所以正确的配置是加上下面几句:

location = /403.html {
    root /path/to/403/page/;
    allow all;
    internal;
}

通过强制允许访问 /403.html 的方式来避免这个错误。下面加上 internal 的意思是这个页面不能被正常的访问到,只能因为 error_page 等内部原因而被访问到。详见这里

使用 Nginx 反代 Apache 安装 WordPress

最近由于原先博客主机极度不稳定,所以准备了很久,准备进行主机迁移。由于迁移前的环境和迁移后还是有较大的区别,整体架构也不太一样,所以在这里说说迁移过程中遇到的问题。

环境对比

原主机 新主机
操作系统 CentOS 6 CentOS 7
Web 服务器 Apache 2.2 Openresty 1.9.7 + Apache 2.4
PHP 版本 5.5 5.6
其他 SELinux

SELinux

很多人为了省事,在拿到主机的第一时间就直接禁用了 SELinux。不过在学习了一段时间之后,我发现其实 SELinux 是一个很好的保护机器的手段。

这里简单列举几个需要注意的 SELinux 的配置:

 

httpd_can_network_connect_db boolean 类型,控制 Apache 是否可以连接 DB
http_port_t port 类型,控制 Apache 可以监听的端口
mysqld_port_t port 类型,控制 mysql 可以监听的端口和 Apache 可以连接的 DB 端口

由于我是将 Apache 作为只解析后端 PHP 请求使用,所以需要修改 http_port_t 加入我需要的端口。添加方法类似于:

semanage port -a -t http_port_t  -p tcp 8090

另外由于我没有在本机安装 Mysql 而是使用的远程 Mysql 实例,并且开放的端口并不是标准的 3306,所以需要将端口号添加到 mysqld_port_t 中。

Apache

这玩意是主要的坑所在。下面来一一列举。

配置格式变更

Apache 2.2 和 2.4 的配置文件区别还是比较大的,加了很多新的参数,同时修改了很多配置的方法。最明显的是:

Options -Indexes

Allow from all
Deny from all

这三条配置已经完全被改掉了。如果在配置中出现第一种,会直接起不来。后面的 Allow Deny 的写法虽然不会有问题,但是已经不是官方推荐的了,建议改掉。

配置无效

在配置 Apache 2.4 的 Log Format 的时候,我发现了一个很蛋疼的问题,就是 CentOS yum 安装的版本(2.4.6)有些配置是使用不了的。如:

http://httpd.apache.org/docs/2.4/mod/mod_log_config.html

这个文档中的:

%{UNIT}T	The time taken to serve the request, in a time unit given by UNIT. Valid units are ms for milliseconds, us for microseconds, and s for seconds. Using s gives the same result as %T without any format; using us gives the same result as %D. Combining %T with a unit is available in 2.4.13 and later.

很多参数都有类似的标注(available in 2.4.13 and later),告诉你在旧版本中不能使用。如果不仔细看的话很容易忽略。所以配置之前一定要仔细阅读。

HTTPS

WordPress

由于我的博客是全站 HTTPS 的,因此在 WordPress 上我是有做一些强制 HTTPS 的措施。但是!由于 WordPress 默认的检测 HTTPS 的方法是这样的:

/**
 * Determine if SSL is used.
 *
 * @since 2.6.0
 *
 * @return bool True if SSL, false if not used.
 */
function is_ssl() {
        if ( isset($_SERVER['HTTPS']) ) {
                if ( 'on' == strtolower($_SERVER['HTTPS']) )
                        return true;
                if ( '1' == $_SERVER['HTTPS'] )
                        return true;
        } elseif ( isset($_SERVER['SERVER_PORT']) && ( '443' == $_SERVER['SERVER_PORT'] ) ) {
                return true;
        }
        return false;
}

而我的 HTTPS 是在 Nginx 层做的,所以导致这两个条件均不满足,因此会遇到重定向循环(Redirect Loop)的问题。解决方法有两种:

修改 wp-config.php 文件:

if ( isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && ( 'https' == $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) {
    $_SERVER['HTTPS'] = 'on';
}

这里的修改需要配合修改 Nginx 的 Proxy 设置,增加下面这行:

proxy_set_header X-Request-Protocol $scheme; #http or https

修改 wp-includes/functions.php 文件(4025行左右):

找到上面说的那段代码,将其替换为:

/**
 * Determine if SSL is used.
 *
 * @since 2.6.0
 *
 * @return bool True if SSL, false if not used.
 */
function is_ssl() {
        if ( isset($_SERVER['HTTPS']) ) {
                if ( 'on' == strtolower($_SERVER['HTTPS']) )
                        return true;
                if ( '1' == $_SERVER['HTTPS'] )
                        return true;
        } elseif ( isset($_SERVER['SERVER_PORT']) && ( '443' == $_SERVER['SERVER_PORT'] ) ) {
                return true;
        } elseif ( isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && ( 'https' == $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) {
                return true;
        }
        return false;
}

同时这个修改方法也要配合修改 Nginx 的 Proxy 设置。

注意,虽然可以不做判断直接无脑 $_SERVER[‘HTTPS’] = ‘on’; 或者在那个 is_ssl() 方法中无脑返回 true,但是不建议这样修改。因为这样可能会导致安全上面的问题。

Apache .htaccess

由于我在某些目录中通过 .htaccess 的方式强制重定向了非 HTTPS 请求,因此在将 SSL 交给 Nginx 之后相关的跳转判断也要修改。原先的跳转逻辑是:

RewriteEngine on
RewriteCond   %{HTTPS} !=on
RewriteRule   ^(.*)  https://%{SERVER_NAME}/$1 [L,R]

由于 %{HTTPS} 这个变量判断的是 Apache 传进来的内部参数,我们无法控制,所以需要将这段修改为:

RewriteEngine on
RewriteCond %{HTTP:X-Request-Protocol} ^http$
RewriteRule   ^(.*)  https://%{SERVER_NAME}/$1 [L,R]

这里判断的就是 Nginx 传进来的 X-Request-Protocol 头了。不过这种写法并不能在单独使用 Apache 作为 Web 服务器的时候使用,需要注意。

客户端证书

目前只针对某目录或文件的客户端证书配置还没有测试完成,将在后面更新。

Compiling Nginx with HTTP/2 and ALPN

To have HTTP/2 fully supported in Nginx, you will need OpenSSL 1.0.2+ to have APLN enabled (What is APLN)

Unfortunately, nowadays most linux distributions are shipping with older version of OpenSSL. For example Ubuntu 14.04 is using openssl 1.0.1f

I can also use 3rd party repositories but I couldn’t find out something I think it is trusted

So it leaves me with only one option: compiling from source

Good thing is, it is pretty easy to compile nginx with a custom OpenSSL. You don’t even have to compile the OpenSSL and install into the system (which could break your system dependences)

All you have to do are:

  1. Download the latest stable OpenSSL from https://www.openssl.org/source/ and extract the tar
  2. Download the latest stable nginx from http://nginx.org/en/download.html and extract the tar
  3. Go into nginx source folder and
    ./configure --with-openssl=/path/to/openssl-1.0.2f --with-http_ssl_module --with-http_v2_module
    make && make install
    • Note: –with-openssl points to the openssl source folder instead of the installation folder
  4. Enable HTTP/2 in nginx configure file

That’s it

And here is how you can verify if your website is now supporting HTTP/2 and ALPN

echo |  /usr/local/ssl/bin/openssl s_client -alpn h2 -connect c11e.wodemo.com:443 | grep ALPN

Which will report

  • “ALPN protocol: h2”
  • or “No ALPN negotiated”

什么是 ALPN(应用层协议协商) What is ALPN (Application-Layer Protocol Negotiation)

在没有启用 ALPN 的时候,加载一个 HTTP/2 页面的步骤是:

Without ALPN, the steps to load a HTTP/2 page would be like:

  1. TLS 握手 (TLS handshake)
  2. 浏览器/客户端 发送带有 “Upgrade: h2c” 头的 HTTP/1.1 请求 (Browser/Client speaks HTTP/1.1 to server with “Upgrade: h2c” Header)
  3. 服务器回复 101 Switching 并将链接升级至 HTTP2 (Server responds with 101 Switching to upgrade to HTTP2)
  4. 现在服务器和客户端采用 HTTP2 协议交流 (Now they talks via HTTP2)

在启用了 ALPN 的情况下:

With ALPN, the steps would be:

  1. TLS握手,并且在握手过程中,客户端告诉服务器一个客户端支持的协议列表,然后服务器回复客户端支持 HTTP2 协议 (TLS handshake and in the handshake client tells the server the list of protocol it supports and server respond in handshake saying that it supports HTTP2 as well)
  2. 现在服务器和客户端采用 HTTP2 协议交流 (Now they talks via HTTP2)

可以看出,使用 ALPN 之后,客户端和服务器的交互少了一轮握手(不需要 Upgrade 和 101 Switching)

As you can see there is one less round trip with ALPN (No step of Upgrade and 101 Switching)

RFC: http://tools.ietf.org/html/rfc7301