让我们自动化 Let's Encrypt
HTTPS 在这个不安全的世界中是一个小小的安全岛屿,而且在当今时代,绝对没有理由不在您托管的每个网站上都使用它。直到去年,只有一个最后的借口:购买证书有点贵。对于企业来说,这可能没什么大不了的;但是,如果您经常托管十几个网站,每个网站都有多个子域名,并且必须自掏腰包为每个证书付费——那么这很快就会成为一种负担。
现在您没有更多的借口了。 请看 Let's Encrypt,这是一个免费的证书颁发机构,于 2016 年 4 月正式脱离 Beta 状态。
除了完全免费之外,Let's Encrypt 证书还有另一个特别之处:它们的有效期不长。目前,Let's Encrypt 颁发的所有证书都仅有效期 90 天,您应该预料到有一天这个期限会变得更短。虽然这种短寿命肯定会创建更高的安全级别,但许多人认为这是一种不便,而且我见过有人因此从使用 Let's Encrypt 转回从商业证书颁发机构购买证书。
当然,如果您运行多个网站,则必须每三个月手动续订几个证书,至少可以说很快就会变得很烦人。 有一天您甚至可能会忘记(并且您会后悔自己的健忘)。 让我们把例行公事留给计算机,对吧?
如果您在基于 Debian 的发行版下使用 Apache,Let's Encrypt 已经通过 libaugeas0 软件包为您提供了支持,并且它能够颁发和续订证书。 如果您像我一样,更喜欢 nginx,并且希望获得零停机时间的自动证书更新以及工业级加密,请继续阅读。 我将向您展示如何实现目标。
首先要说明的是——一些假设和要求
-
您正在运行 nginx Web 服务器/负载均衡器,并且您将使用它进行 TLS 终端处理(这是一种花哨但技术上正确的说法,意思是“nginx 将处理所有这些 HTTPS 内容”)。
-
nginx 为多个网站提供服务,并且您希望所有网站都启用 HTTPS,而且您不打算花一分钱。
-
您还希望在 SSL 测试的行业标准 SSL Labs 的 SSL 服务器测试 中获得最高评级。
-
您不喜欢在您的服务器上运行一些并非完全沙箱化的第三方代码的想法,并且您宁愿将此代码放在 Docker 容器中。
-
自然,您足够懒惰(或经验丰富),因此您想编写一些脚本,以便在所有证书到期之前重新颁发它们。
-
我在运行 nginx 1.6.2 和 Docker 1.9.1 的 Debian Jessie 上测试了此代码;它也应该适用于所有其他版本。 如果您没有安装 docker-engine,请按照此处的说明进行操作。
现在,检查您的 nginx 是否支持 TLS
sudo nginx -V
通常默认情况下支持它,并且应产生以下结果
TLS SNI support enabled
您还需要一个存储证书的位置
sudo mkdir -m 755 /etc/letsencrypt
不要担心此目录的权限; 证书本身不会公开访问。 现在您需要在 nginx 配置中进行一些小的更改。 创建一个新文件 /etc/nginx/letsencrypt.inc,内容如下
location ^~ /.well-known/acme-challenge/ {
root /tmp/letsencrypt/www;
break;
}
然后在 nginx 配置文件中找到您的“server”部分,并将以下行添加到您托管的每个网站
include /etc/nginx/letsencrypt.inc;
因此,最终结果将如下所示
server {
listen 80;
server_name example.com www.example.com;
...
include /etc/nginx/letsencrypt.inc;
...
}
保存这两个文件后,要求 nginx 重新加载配置
sudo /usr/sbin/nginx -t && sudo service nginx reload
请注意,您只是重新加载 nginx 配置——nginx 非常清楚如何在不丢弃连接的情况下执行此操作。
现在,让我们获取一些证书! 不用说,您要为其颁发证书的所有域名都应解析到您的服务器 IP 地址;否则,可能会为别人的域名颁发证书,并将这些证书用于中间人攻击。
以下命令将拉取并启动一个新的 Docker 镜像,其中包含官方 Let's Encrypt 客户端
mkdir -p /tmp/letsencrypt/www
# make sure you have the latest version of this image,
# and not some pre-beta - those used to be notoriously buggy
docker pull quay.io/letsencrypt/letsencrypt:latest
docker run --rm -it --name letsencrypt \
-v /etc/letsencrypt:/etc/letsencrypt \
-v /tmp/letsencrypt/www:/var/www \
quay.io/letsencrypt/letsencrypt:latest \
auth --authenticator webroot \
--webroot-path /var/www \
--domain=example.com --domain=www.example.com \
--email=admin@example.com
如您所见,您在主机和容器之间共享两个数据卷
-
/etc/letsencrypt 用于存储 Let's Encrypt 配置、所有证书和链。
-
/tmp/letsencrypt/www 用于您的服务器与 Let's Encrypt 服务器之间的通信。
在容器内运行的 webroot 插件将为您的每个域名创建一个临时质询文件,然后 Let's Encrypt 验证服务器将发送 HTTP 请求以确保您确实控制着此域名和此服务器。 这些文件是临时的,仅在颁发或续订证书期间需要。
您需要按下按钮同意 TOS,几秒钟后,您的证书就准备好了。 如果您有多个子域名,如本例所示,您可以枚举所有子域名,这将导致为一个共享证书颁发给所有这些子域名。 但是,如果您有多个域名,则最好为每个域名单独拥有一个证书——只需为您拥有的每个域名重复最后一条 docker run ...
命令即可(如果您有一天决定将您的某个域名移动到不同的服务器,请稍后感谢我)。
如您所见,获取证书的过程是轻松且安全的。 几乎所有繁重的工作都在幕后为您完成,如果您曾经使用其他传统的证书颁发机构处理过证书,您就会确切地知道我的意思。 容器内运行的任何程序都只能访问服务器上的两个目录,并且仅在运行时访问。
在您获得所有证书后,可以安全地删除临时目录
rm -rf /tmp/letsencrypt
让我们回到 nginx 配置。 从 SSLLabs 获得 A+ 评级需要一些额外的努力。 创建一个新的 Ephemeral Diffie-Hellman 素数(如果这是您第一次看到此术语,请转到 此处 获取更多信息)
sudo openssl dhparam -out /etc/pki/tls/private/dhparam.pem 4096
注意:如果您绝对需要支持旧版本的客户端软件,例如 Java 6 客户端,则需要跳过此步骤并在下一步中注释掉 ssl_dhparam
行。 这些旧客户端不支持长度超过 1024 字节的 Diffie-Hellman 参数,因此您需要在支持这些客户端和安全之间做出选择。
现在,喝一杯热饮; 这将需要一些时间来生成。 将以下行添加到 /etc/nginx/nginx.conf 的“http”部分
http {
...
ssl_dhparam /etc/pki/tls/private/dhparam.pem;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 60m;
...
}
创建一个新文件 /etc/nginx/ssl_options.inc
ssl on;
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers "ECDH+AESGCM DH+AESGCM ECDH+AES256 DH+AES256
↪ECDH+AES128 DH+AES ECDH+3DES DH+3DES RSA+AESGCM
↪RSA+AES RSA+3DES !aNULL !MD5 !DSS";
# Enable HSTS (HTTP Strict Transport Security) for half a year
add_header Strict-Transport-Security
↪"max-age=15768000;includeSubDomains";
并创建一个新的“server”部分
server {
listen 443;
server_name example.com www.example.com;
include /etc/nginx/letsencrypt.inc;
include /etc/nginx/ssl_options.inc;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# enable OCSP stapling to speed up first connect
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate
↪/etc/letsencrypt/live/example.com/chain.pem;
...
}
警告:Strict-Transport-Security 标头将告诉每位访问者您承诺将来始终使用 HTTPS。 这是一条单行道,一旦您设置它,就无法回头——您访问者的浏览器将记住您的承诺并坚持使用 HTTPS。 另请注意:有关 OCSP stapling 的更多信息,请参阅 此 Wikipedia 页面。
完成所有这些更改后,再次重新加载 nginx 配置
sudo /usr/sbin/nginx -t && sudo service nginx reload
此时,您的网站应该已启动并运行 HTTPS。 尝试在浏览器中打开 https://www.example.com/,并在地址栏中享受绿色锁标志。 要验证加密质量,请转到 SSL Labs SSL 服务器测试 并提交您的主机名进行检查(通常需要几分钟)。
因此,既然您已经拥有 HTTPS,那么如何禁用 HTTP 呢? 返回到 HTTP “server”部分并进行以下改进
server {
listen 80;
server_name example.com www.example.com;
include /etc/nginx/letsencrypt.inc;
...
if ($scheme = "http") {
rewrite ^/(.*)$ https://$host/$1 permanent;
}
...
}
这将把所有来自 HTTP 的流量重定向到 HTTPS,自动将所有客户端带到您网站的安全版本。 重新加载 nginx 配置以激活更改。
现在是时候自动化证书续订了。 Let's Encrypt 当前的策略允许您在七天内为一个域名请求五次证书续订。 这意味着每天尝试续订证书是不明智的(也没有多大意义)。 另一方面,将其留到到期前的最后一刻也很危险。 幸运的是,有一种简单的方法可以仅在这些证书到期前不到 30 天时才续订它们。 对我来说,30 天听起来刚刚好。 这意味着我的证书平均每 60 天重新颁发一次,如果之后出现问题,我将有一个月的时间来修复任何损坏的东西。
创建一个用于续订的脚本(我将其放在 /root/update_keys.sh 中),内容如下
#!/bin/bash
mkdir -p /tmp/letsencrypt/www
ADMIN_EMAIL=admin@example.com
HOSTNAME=$(hostname)
OUTPUT="$((docker run --rm -i --name letsencrypt \
-v /etc/letsencrypt:/etc/letsencrypt \
-v /tmp/letsencrypt/www:/var/www \
quay.io/letsencrypt/letsencrypt:latest renew) 2>&1)"
if [[ $? -eq 0 ]]; then
echo "${OUTPUT}" | grep -q "No renewals were attempted"
if [[ $? -eq 0 ]]; then
# all certificates have more than 30 days left -
# nothing to do
exit 0
fi
echo "${OUTPUT}" | tr -Cd '[:print:]\n' \
| mail -s "${HOSTNAME}: Let's Encrypt keys renewal -
↪success" "${ADMIN_EMAIL}"
else
echo "${OUTPUT}" | tr -Cd '[:print:]\n' \
| mail -s "${HOSTNAME}: Let's Encrypt keys renewal -
↪failed, exit code $?!" "${ADMIN_EMAIL}"
exit 1
fi
# test config, reload if successful
/usr/sbin/nginx -t &> /dev/null
if [[ $? -ne 0 ]]; then
echo 'please fix configfile problem' \
| mail -s "${HOSTNAME}: nginx unable to reload"
↪"${ADMIN_EMAIL}"
logger "nginx has errors - not reloaded"
else
service nginx reload
logger "nginx reloaded"
fi
rm -rf /tmp/letsencrypt
记住分配正确的访问权限
sudo chmod u+x /root/update_keys.sh
并创建一个 crontab 条目
sudo crontab -e
包含这样的一行
17 2 * * * /root/update_keys.sh
这将每天凌晨 2:17 触发此更新脚本的执行。 更新脚本将检查您的证书是否剩余超过 30 天,如果不足 30 天,它将尝试续订所有即将到期的证书。 您是否想知道我为什么使用凌晨 2:17? 嗯,有一个简单的解释:几乎所有其他人都没有这样做。 大多数人在创建 cron 作业时,都使用一些简单的值,例如凌晨 1:00、凌晨 2:00、凌晨 3:30、下午 4:15 等等,如果您的 cron 作业应该与外部服务通信,这是一个非常非常糟糕的选择,因为这意味着该服务将不时地遇到最大负载。 这对服务不利,对您也不利; 如果您在这些高峰负载期间发送请求,则获得超时的机会会大大增加。
因此,对于这项工作,请,请不要使用偶数值,也不要使用我的值; 请使用一些随机值代替,一切都会好起来的。
如您所见,Let's Encrypt 设法实现了证书维护的完全自动化。 如果您正确使用它,它就可以正常工作——而且是免费的。