前言

 QuChao.com 的 Stapled OCSP Response

去年末“赶时髦”申请了 Let's Encrypt(下称 LE )的免费证书,
遗憾的是还不支持泛域名 ECC 证书(后者已被支持,中间证书则尚未提供),
不过一切都在进行之中。
有兴趣者请密切关注其论坛。

其中间证书虽已让被广泛支持的 IdenTrust 根证书 交叉签发,
然而它仍旧只是个刚“出道”的 CA ,
不少服务器和本地程序还未信任其证书,
一些朋友可能与我一样在配置 ssl_stapling_file 时遇到一些困难,
于是我将这部分配置过程从笔记里摘出来与大家分享、探讨。

关于 OCSP Stapling 的资料很多,
自己也还在学习之中,
这里仅给出一些链接:
简单地说它是一种的优化手段——
将原本需要客户端实时发起的 OCSP 请求转嫁给服务端;
利用 NginX 的 ssl_stapling_file 指令更可将业已缓存的查询结果直接返回,
使得缓存的更新时间更可加控。

环境

我使用的环境如下:

其中 NginX 版本最为关键,
它从 1.3.7+ 开始支持该特性。

  • CentOS/7.2.x
  • NginX/1.9.x
  • OpenSSL/1.0.1e

启用 OCSP Stapling

我们假定你已从 LE 申请到了证书,
并能正常提供服务。<!--more-->

推荐在服务器上使用 ACME-Tiny 来申请、续期证书。

我的配置参考了 H5BPNginX Boilerplate Configs
在其 /directive-only/ssl-stapling.conf 中推荐定义如下:

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /path/to/certs/quchao.com/chained.pem;
resolver 8.8.8.8 8.8.4.4 216.146.35.35 216.146.36.36 valid=60s;
resolver_timeout 2s;

ssl_certificate 指令指定了完整的证书链,
ssl_trusted_certificate 可省略。

上述配置即方便地开启了 OCSP Stapling 特性。
当客户端访问时 NginX 将去指定的证书中查找 OCSP 服务的地址,
获得响应内容后通过证书链下发给客户端。

可通过此工具来查看证书的有效期及撤销状态,
配置成功可看到 (STAPLED) 状态关键词。

启用 ssl_stapling_file

2016-12-27 特别提示
目前由于 LE 的 OCSP 不支持多证书合并响应,
此指令也无法多次使用,
因此多证书情形下无法工作!

接下来我们更进一步——
直接将 OCSP 响应存成文件,
NginX 将其随证书下发而不实时查询。

首先我们需要找到 OCSP 服务地址:

chained.pem 即网站完整的证书链。

openssl x509 -in /path/to/certs/quchao.com/chained.pem -text | grep "OCSP - URI:" | cut -d: -f2,3

得到的地址是 http://ocsp.int-x1.letsencrypt.org/
自 26th Mar, 2016 已启用 http://ocsp.int-x3.letsencrypt.org/已兼容 WinXP )。

接着获取 OCSP 响应并写入 ocsp.resp 文件:

openssl ocsp -no_nonce \
             -respout /path/to/certs/quchao.com/ocsp.resp \
             -issuer /path/to/certs/lets-encrypt-x3-cross-signed.pem \
             -cert /path/to/certs/quchao.com/quchao.com_chained.pem \
             -url http://ocsp.int-x3.letsencrypt.org/ \
             -header "HOST" "ocsp.int-x3.letsencrypt.org"

注意相关文件的权限;
lets-encrypt-x3-cross-signed.pemLE中间证书
有些 openssl 版本不支持指定 -header 参数,请选择合适的版本。

若运行时报错如下:

Response Verify Failure
140060623058848:error:27069076:OCSP routines:OCSP_basic_verify:signer certificate not found:ocsp_vfy.c:85:

则服务器可能并未信任 LE 的根证书或中间证书;
有多种途径解决:

  • 直接将这些证书加入系统的 ca-bundle.crt 里;
  • 生成一个专用的 bundle 。

下载相关根证书和中间证书

它们是:

  • 根证书:DST Root CA X3
  • 根证书:ISRG Root X1
  • 中间证书:Let’s Encrypt Authority X1 (分别被上述两个根证书签发)
  • X1 的候补中间证书:Let’s Encrypt Authority X2
  • 中间证书:Let’s Encrypt Authority X3 (兼容 WinXP)
  • X3 的候补中间证书:Let’s Encrypt Authority X4

不管采用哪种方式,
我们先将证书全部保存到本地。

mkdir -p /path/to/tmp/
cd /path/to/tmp/
curl -o ./DST_Root_CA_X3.pem -L https://ssl-tools.net/certificates/dac9024f54d8f6df94935fb1732638ca6ad77c13.pem

curl -O -L https://letsencrypt.org/certs/isrgrootx1.pem

curl -O -L https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem
curl -O -L https://letsencrypt.org/certs/letsencryptauthorityx3.pem
curl -O -L https://letsencrypt.org/certs/lets-encrypt-x4-cross-signed.pem
curl -O -L https://letsencrypt.org/certs/letsencryptauthorityx4.pem

curl -O -L https://letsencrypt.org/certs/lets-encrypt-x1-cross-signed.pem
curl -O -L https://letsencrypt.org/certs/letsencryptauthorityx1.pem
curl -O -L https://letsencrypt.org/certs/lets-encrypt-x2-cross-signed.pem
curl -O -L https://letsencrypt.org/certs/letsencryptauthorityx2.pem

方法一:将它们加入系统的 ca-bundle.crt

以 CentOS/7 为例。

mv /path/to/tmp/*.pem /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust

执行完毕可看看 /etc/pki/ca-trust/extracted/openssl/ca-bundle.trust.crt 是否已包含上述证书。

方法二:生成专用的 ca-bundle.pem

我使用该方式。

cat /path/to/tmp/*.pem > /path/to/certs/ca-bundle.pem

特别注意
letsencryptauthorityx2.pem 文件为 Dos 格式,
会导致 concat 后 PEM 格式错误,
请尝试将其转换为 Unix 格式(末尾增加换行)后重新生成 Bundle 。

再次尝试缓存 ocsp.resp 文件

此时再执行:

openssl ocsp -no_nonce \
             -respout /path/to/certs/quchao.com/ocsp.resp \
             -issuer /path/to/certs/lets-encrypt-x3-cross-signed.pem \
             -cert /path/to/certs/quchao.com/quchao.com_chained.pem \
             -CAfile /path/to/certs/ca-bundle.pem \
             -VAfile /path/to/certs/ca-bundle.pem \
             -url http://ocsp.int-x3.letsencrypt.org/ \
             -header "HOST" "ocsp.int-x3.letsencrypt.org"

注意新增的 -CAfile-VAfile 参数。

若无其它意外,
你会看到如下结果:

Response verify OK
/path/to/certs/quchao.com/chained.pem: good
    This Update: Jan 17 06:00:00 2016 GMT
    Next Update: Jan 24 06:00:00 2016 GMT

证明 OCSP 请求缓存成功,
且告知了过期时间。

开启特性

接下来放心大胆地在 NginX 配置中增加一行:

ssl_stapling_file /path/to/certs/quchao.com/ocsp.resp;

重载配置后,
执行如下命令测试:

echo QUIT | openssl s_client -connect quchao.com:443 -status 2> /dev/null | grep -A 17 'OCSP response:' | grep -B 17 'Next Update'

若得到类似 OCSP Response Status: successful 的答复即算成功。(亦可通过之前的工具进行验证)

定期更新

我们注意到响应内容中有一行 Next Update: Jan 24 06:00:00 2016 GMT
表示我们所缓存的响应内容将于这个时间点过期。
为了避免过期的情形,
我们可以编写脚本并设定 CronJob 来一劳永逸地解决问题。

脚本

如下是我所使用的脚本(参考),
使用时请注意相关文件的路径权限问题。

#!/bin/bash
#############################
#
# OCSP Cache Updater v0.2.1
#   by Qu Chao
#
# ---------------------------
#
# Usage:
#   % update_ocsp_cache "example.com"
#
# ---------------------------
#
# v0.1.0 - Initialization.
# v0.2.0 - Use X3 intermediate certs by default.
# v0.2.1 - Add generation of intermediates as an option.
#
#############################

if [ -z $1 ]; then
    echo -e "No domain is specified!" >&2
    exit 1
fi

############################
# CONFIGS
############################
#LE_DIR="/usr/local/etc/nginx/ssl"

LE_DIR="/etc/nginx/ssl"
GEN_NUM="x3"

############################
# DEFAULTS
############################
DOMAIN=$1
CERT_DIR="${LE_DIR}/${DOMAIN}"
CHAINED_CERT="${CERT_DIR}/chained.pem"

ISSUER_CERT="${LE_DIR}/lets-encrypt-${GEN_NUM}-cross-signed.pem"
CA_FILE="${LE_DIR}/ca-bundle.pem"
OCSP_RESP_FILE="${CERT_DIR}/ocsp.resp"
OCSP_REPLY_FILE="${CERT_DIR}/ocsp.reply"

############################
# MAIN
############################
# Functions
existence_pattern_check(){
    [ -n "$1" ] && [ -e "$1" ] && ( [ "${2:-file}" = "file" ] && ( [ -f "$1" ] || [ -L "$1" ] ) || [ -d "$1" ] ) && [ -r "$1" ]
}

# Params Validation
if ! existence_pattern_check "${LE_DIR}" "dir"; then
    echo -e "Let's Encrypt folder is missing!" >&2
    exit 1
elif ! existence_pattern_check "${CERT_DIR}" "dir"; then
    echo -e "Domain cert folder is missing!" >&2
    exit 1
elif ! existence_pattern_check "${CHAINED_CERT}" || ! existence_pattern_check "${ISSUER_CERT}" || ! existence_pattern_check "${CA_FILE}"; then
    echo -e "Required certs file is missing!" >&2
    exit 1
fi

# Get OCSP URI & HOST
OCSP_URL=$(openssl x509 -in "${CHAINED_CERT}"  -text | grep "OCSP - URI:" | cut -d: -f2,3)
OCSP_HOST=$(echo "${OCSP_URL}" | awk -F/ '{print $3}')

# Output OCSP response
openssl ocsp -no_nonce \
             -respout "${OCSP_RESP_FILE}.new" \
             -issuer "${ISSUER_CERT}" \
             -cert "${CHAINED_CERT}" \
             -CAfile "${CA_FILE}" \
             -VAfile "${CA_FILE}" \
             -url "${OCSP_URL}" \
             -header HOST "${OCSP_HOST}" > "${OCSP_REPLY_FILE}" 2>&1

# Check if it's all okay?
if  grep -q "Response verify OK" "${OCSP_REPLY_FILE}" && grep -q "${CHAINED_CERT}: good" "${OCSP_REPLY_FILE}" ; then
    if  cmp -s "${OCSP_RESP_FILE}.new" "${OCSP_RESP_FILE}" ; then
        # No news is good news
        rm "${OCSP_RESP_FILE}.new"

        echo -e "OCSP cache is up-to-date!"
    else
        # Update the cache file
        mv "${OCSP_RESP_FILE}.new" "${OCSP_RESP_FILE}"

        # reload nginx's config
        /usr/sbin/nginx -s reload

        echo -e "OCSP cache is updated!"
    fi
else
    # Bad things happen all the time
    cat "${OCSP_REPLY_FILE}" >&2

    echo -e "Failed to update OCSP cache!" >&2
fi

# Make a backup
mv "${OCSP_REPLY_FILE}" "${OCSP_REPLY_FILE}.old"
echo -e "Detailed log located at ${OCSP_REPLY_FILE}.old"

将脚本保存为 /path/to/certs/scripts/update_ocsp_cache.sh 并赋予执行权限。

计划任务

接下来新增一个 Hourly CronJob :

1 * * * * /path/to/certs/scripts/update_ocsp_cache.sh quchao.com 2>&1 >/dev/null | (test -s /dev/stdin && /usr/bin/mail -s "$HOSTNAME - Hourly OCSP Cache Update" mail@to.me || echo 'OCSP Cache is Up-to-date!')

这样一来,
服务器每小时都会尝试缓存新的 OCSP 响应,
当响应内容有变化便会自动更新缓存重载配置
若过程中发生异常则会通过 mailx 邮件通知我们上线处理。

更新日志

20160327 - LE 中间证书已改用 X3 (兼容 WinXP ),OCSP 服务器亦有调整;本文已更新。

标签: Security, NginX, Performance

已有 10 条评论

  1. J J

    大佬,这个Cron后面的脚本能解释下吗?这个逻辑是怎么判断捕获到标准错误后发送邮件的?

    1. Chao QU Chao QU

      先 2>&1 把标准错误定向到标准输出,
      然后通过管道给 mailx

  2. google voice google voice

    Let's Encrypt 的OCSP现在被墙了吗?

    1. Chao QU Chao QU

      遗憾,应该是。

  3. ChaoYu ChaoYu

    这个脚本后面的一些实在看不懂? 楼主有QQ吗??

    1. Chao QU Chao QU

      具体哪一段呢?我很少使用 QQ 了。

  4. MikeLiao MikeLiao

    怒赞。感谢分享,写的非常详细。成功开启ssl_stapling_file

    1. Chao QU Chao QU

      不客气哦,欢迎交流。

  5. Duke Duke

    这两天在配置自己网站的ssl,用的comodo的,参考Jerry Qu的文章,在ssl_stapling_file一直都是验证出错,根据Chao Qu的配置,总算成功获取了OCSP响应文件。非常感谢。

    1. Chao QU Chao QU

      @Duke
      不客气。
      或许会整理一篇更完整的。

添加新评论