目前 https 是大势所趋,上周末成功把博客迁移到Halo
后,还未来得及配置 https,之前都是从阿里云申请的赛门铁克免费版 SSL 证书,很方便,申请是快捷,缺点是不能自动续费。了解到Certbot
是Let's Encrypt
官方推荐的证书生成客户端工具。
1.安装 Certbot
直接上命令,还可以安装 certbot-nginx 插件,笔者 Nginx 证书时自己配置,暂时未安装(如果需要可参考:[1])。
# 先安装 EPEL 仓库(因为 certbot 在这个源里,目前还没在默认的源里)
yum install epel-release
yum update
yum clean all
yum makecache
# 安装 certbot
yum install certbot -y
## 如果提示 urllib3 安装失败,可以先卸载掉 pip 安装的 urllib3
pip uninstall urllib3
## 然后通过 yum 安装 python-urllib3
yum install python-urllib3
## 更新 pip 版本方法
pip install --upgrade pip
# 如果提示 pyOpenSSL 安装失败,可先卸载掉 certbot 和 pyOpenSSL
yum remove certbot && pip uninstall pyOpenSSL
yum install -y python-devel
yum install -y openssl-devel
# 然后重新安装 certbot
yum install certbot
# 如果还是安装失败(提示:ImportError: ‘pyOpenSSL’ module missing required functionality. Try upgrading to v0.14 or newer.)
## 目前一个靠谱的解决办法
pip install --upgrade --force-reinstall 'requests==2.6.0'
# 测试是否安装成功
certbot --help
# 查看 certbot 版本,ACME v2 在 certbot 0.20.0+ 才支持
certbot --version
貌似还可以通过 pip 安装 certbot-auto,用法和 certbot 一致,可参考:[2]
2.申请证书前必要的配置
获取域名证书过程中, Let's Encrypt 会对域名发起访问,以确认申请者对域名的所有权。故需要配置 nginx,以便能够对 Let's Encrypt 的访问返回正确的响应。
假设我们要申请:a.xxx.com
,b.xxx.com
这两个域名的证书。在 nginx 中配置:
server {
listen 80;
server_name "a.xxx.com" "b.xxx.com";
location ^~ /.well-known/acme-challenge/ {
default_type "text/plain";
root /home/letsencrypt/;
}
}
然后,手动创建临时目录
mkdir /home/letsencrypt
重启 Nginx
# 重启前先检测下配置文件是否正确
nginx -t
nginx -s reload
同时还要把上述==域名解析==到服务器。
3.申请证书
Nginx 配置好之后,就可以正式开始申请域名证书了。首次申请证书成功后有90天有效期。
3.1 challenge 介绍
Let's Encrypt需要验证网站的所有权才能颁发证书, 官方称之为challenge(挑战).
有三种方式可以实现验证: (官方文档在此)
- 在网站上的指定位置发布指定文件(HTTP-01)
- 在网站上提供指定的临时证书(TLS-SNI-01)
- 在域名系统中发布指定的DNS记录(DNS-01)
3.2 普通子域名申请
3.2.1 HTTP-01 方式 challenge
执行命令:
certbot certonly --email xxx@126.com --webroot -w /home/letsencrypt -d a.xxx.com -d b.xxx.com
certonly
是 certbot 众多插件之一,代表安装证书,还可以选择其他插件。-d
指定域名,可以指定多个
上述方法还需要将子域名指定到当前服务器,并在服务器上配置web访问,以支持 HTTP-01 challenge.
我使用的是 Nginx 的形式,添加如下配置:
## SSL cert auto generate, verify need
server {
listen 80;
server_name "a.xxx.com";
location ^~ /.well-known/acme-challenge/ {
default_type "text/plain";
root /home/letsencrypt/;
}
}
3.2.2 DNS challenge 方式(推荐)
执行命令:
certbot certonly --email xxx@126.com --manual --preferred-challenges dns -d a.xxx.com
--manual
表示手动交互模式,Certbot 有很多插件,不同的插件都可以申请证书,用户可以根据需要自行选择--preferred-challenges
使用 DNS 方式校验域名所有权,域名验证 dns 解析需要增加 TXT 类型的解析配置,子域名为_acme-challenge
,记录值为交互模式(命令行)所给出的值。
申请时需要手动在域名解析中添加一条 TXT 记录,执行命令时会有提示。
例如:
_acme-challenge.a.xxx.com -- TXT -- xxxxxxxxxxxxxxxxxxxxxxx-4ukpnxfmy-io
3.3 泛域名(通配符)申请
还支持申请通配符域名证书
certbot certonly --email xxx@126.com -d *.xxx.com --manual --preferred-challenges dns --server https://acme-v02.api.letsencrypt.org/directory
--manual
表示手动交互模式,Certbot 有很多插件,不同的插件都可以申请证书,用户可以根据需要自行选择--preferred-challenges
使用 DNS 方式校验域名所有权,域名验证 dns 解析需要增加 TXT 类型的解析配置,子域名为_acme-challenge
,记录值为交互模式(命令行)所给出的值。--server
Let's Encrypt ACME v2 版本使用的服务器不同于 v1 版本,需要显式指定。
3.4 申请过程后续
执行过程中会有几次命令确认,根据提示 确认/不同意 操作即可,申请成功后会有对应提示。
创建成功后 /etc/letsencrypt/live/xxx.com/
下会生成 4 个文件,请勿更改 ssl 文件位置,这样可以减少自动续期时的操作
cert.pem
- Apache服务器端证书chain.pem
- Apache根证书和中继证书fullchain.pem
- Nginx所需要ssl_certificate文件privkey.pem
- 安全证书KEY文件
Nginx环境,就只需要用到
fullchain.pem
和privkey.pem
两个证书文件
4.证书的更新
由于默认申请的证书只有90天,所以需要配置自动更新。
4.1 普通子域名证书更新
普通子域名自动更新测试,使用--dry-run
选项表示测试,非真正执行更新
certbot renew --dry-run
测试成功会看到类似如下提示:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
** (The test certificates below have not been saved.)
Congratulations, all renewals succeeded. The following certs have been renewed:
/etc/letsencrypt/live/a.xxx.com/fullchain.pem (success)
** DRY RUN: simulating 'certbot renew' close to cert expiry
** (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
到期后手动更新证书
certbot renew -v
还可以配置自动更新
certbot renew --quiet --no-self-upgrade
如果是 DNS challenges 方式,更新时不能使用采用上述方法了,需要继续往下看。
需要使用命令(具体原因可以参考 4.2 节):
# 测试更新(测试成功会有提示信息,并不会真的生效,只是测试)
certbot renew --dry-run --cert-name a.xxx.com --manual-auth-hook /usr/bin/certbot-alidns
# 手动更新--可配置到 crontab 中,定时执行
certbot renew --cert-name a.xxx.com --manual-public-ip-logging-ok --manual-auth-hook /usr/bin/certbot-alidns --deploy-hook "python3 /opt/bin/qiniu_cdn_ssl_cert_auto_renew.py halo.cdn.xx.com"
# 手动更新(并通过 deploy-hook 指定自动上传新证书到七牛)
certbot renew --cert-name a.xxx.com --manual-public-ip-logging-ok --manual-auth-hook /usr/bin/certbot-alidns --deploy-hook "python3 qiniu_cdn_ssl_cert_auto_renew.py xxx.yyy.com"
关于自动上传证书到七牛云可参考博文:使用七牛云 API 上传 letsencrypt SSL 证书并绑定到 CDN
4.2 泛域名证书更新
如果使用普通子域名的方式,会报一个一个错误:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Could not choose appropriate plugin: The manual plugin is not working; there may be problems with your existing configuration.
The error was: PluginError('An authentication script must be provided with --manual-auth-hook when using the manual plugin non-interactively.',)
Attempting to renew cert (xxx.com) from /etc/letsencrypt/renewal/xxx.com.conf produced an unexpected error: The manual plugin is not working; there may be problems with your existing configuration.
The error was: PluginError('An authentication script must be provided with --manual-auth-hook when using the manual plugin non-interactively.',). Skipping.
All renewal attempts failed. The following certs could not be renewed:
/etc/letsencrypt/live/xxx.com/fullchain.pem (failure)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
4.2.1 问题分析
这是因为Let’s Encrypt颁发的通配符证书每次续期需要验证一下DNS的TXT记录,这个TXT记录的值是会发生变化的。所以续期时候还需要更改下DNS的TXT记录。
而且,还需要指定--manual-auth-hook
参数,意思就是当我们自动使用插件的时候必须提供一个身份验证脚本,这样就可以使用 cerbot 提供的--manual-auth-hook
来进行证书的更新,因为要录入DNS记录,所以会失败。
大致解决思路:
可以使用 certbot 插件,比如 certbot-dns-cloudflare 插件(还有其他的),就是自动更新 cloudflare 等 DNS 的记录。或者手动编写脚本,cerbot 官方提供了 cloudflare 的样例,笔者的域名是阿里云的,所以就寻找到了网友写好的阿里云可用的脚本。
4.2.2 准备 Aliyun DNS 更新脚本
可以使用GitHub网友提供的python脚本(还支持腾讯云和Godaddy),或者使用CSDN网友提供的阿里云Go版小程序certbot-alidns
,这里我使用的是已经编译好的阿里云Go版小程序certbot-alidns
(源码贴在文末了),在此表示感谢。
2022-02-10 更新:推荐使用第 5 节所示方法。
4.2.3 配置更新脚本所需信息
配置两个环境变量,可以写在/etc/profile
,或者.bashrc
里面,然后执行source生效。
# 阿里云的access key和secret(登录阿里云控制台获取)
export CERTBOT_ALI_KEY=""
export CERTBOT_ALI_SECRET=""
下载certbot-alidns
,并添加可执行权限。
wget https://ghost.oss.sherlocky.com/certbot/certbot-alidns -O /usr/bin/certbot-alidns
chmod +x /usr/bin/certbot-alidns
4.2.4 更新泛域名证书
# 测试更新(测试成功会有提示信息,并不会真的生效,只是测试)
certbot renew --dry-run --cert-name xxx.com --manual-auth-hook /usr/bin/certbot-alidns
# 手动更新
certbot renew --cert-name xxx.com --manual-auth-hook /usr/bin/certbot-alidns
还可以在更新后,自动执行命令,比如重启Nginx:
###### 手动更新后自动重启nginx(前提:Nginx已service化)
certbot renew --cert-name xxx.com --manual-auth-hook /usr/bin/certbot-alidns --deploy-hook "systemctl restart nginx"
4.2.5 重新加载下ssl证书
配合Nginx使用具体可参见下一章节。
/usr/local/nginx/sbin/nginx -t
/usr/local/nginx/sbin/nginx -s reload
4.2.6 配置自动更新
添加 crontab 计划,使用命令crontab -e
,添加以下内容。
## 泛域名证书的更新
##### crontab 任务自动更新(每月 15 号,因为证书有效期<30天才会renew)
##### 只有成功renew证书,才会重新启动nginx
0 0 15 * * certbot renew --cert-name xxx.com --manual-auth-hook /usr/bin/certbot-alidns --deploy-hook "systemctl restart nginx" >> /var/log/certbot.log
## 子域名证书的更新
0 0 15 * * certbot renew --cert-name a.xxx.com --manual-auth-hook /usr/bin/certbot-alidns --deploy-hook "python3 qiniu_cdn_ssl_cert_auto_renew.py xxx.yyy.com" >> /var/log/certbot.log
5.三级子域名泛域名的特殊处理
第 4 节中提到的 Go 版 dns challenge 更新程序有 bug,不支持自动添加三级子域名的DNS _acme-challenge。需要使用GitHub网友提供的python脚本
5.1 脚本安装&配置
git clone https://github.com/ywdblog/certbot-letencrypt-wildcardcertificates-alydns-au /opt/certbot-alydns/
cd /opt/certbot-alydns
vi au.sh
$填入阿里云密钥
chmod +x au.sh
5.3 证书申请方法
申请时是手动创建的DNS解析记录,故和4节一致即可
certbot certonly --email @126.com -d *.xx.yy.com --manual --preferred-challenges dns --server https://acme-v02.api.letsencrypt.org/directory
5.4 证书续期方法
certbot renew --dry-run --cert-name xx.yy.com --manual-auth-hook "/opt/certbot-alydns/au.sh python aly add" --manual-cleanup-hook "/opt/certbot-alydns/au.sh python aly clean"
如果是测试续期追加--dry-run
参数即可。
定时任务示例:
0 0 1 * * certbot renew --cert-name xx.yy.com--manual-auth-hook "/opt/certbot-alydns/au.sh python aly add" --manual-cleanup-hook "/opt/certbot-alydns/au.sh python aly clean" --deploy-hook "systemctl restart nginx" >> /var/log/certbot.log
6.证书的使用
笔者的证书主要配合 Nginx 使用,当然也可以作其他用途。
直接上 Nginx 配置:
server {
listen 80;
server_name "a.xxx.com";
# 实现自动跳转
return 301 https://$server_name$request_uri;
}
server {
listen 443;
server_name "a.xxx.com";
ssl on;
root html;
index index.html index.htm;
# SSL 10 MB共享SSL会话缓存配置
ssl_session_tickets off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 3600s;
# 也可以写到http{}中,server{}可以直接继承使用
ssl_certificate /xxx/fullchain.pem;
ssl_certificate_key /xxx/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
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_prefer_server_ciphers on;
# 开启HSTS
add_header Strict-Transport-Security "max-age=15768000; includeSubdomains; preload" always;
location / {
root html;
index index.html index.htm;
proxy_pass http://halo_blog;
client_max_body_size 3000m;
include proxy.conf;
access_log off;
proxy_set_header X-Forwarded-Proto $scheme;
# 点击劫持:无X-Frame-Options头信息
add_header X-Frame-Options SAMEORIGIN;
}
# 禁止访问以下类型文件(防一部分恶意攻击)
location ~* \.(ini|cfg|dwt|lbi|php|action)$ {
deny all;
}
}
Go版阿里云DNS更新小程序源码贴一下:
package main
import (
"fmt"
"github.com/denverdino/aliyungo/dns"
"os"
"time"
)
var certbot_ali_key string
var certbot_ali_secret string
func init() {
// 定义阿里云的访问key和secret
certbot_ali_key = os.Getenv("CERTBOT_ALI_KEY")
certbot_ali_secret = os.Getenv("CERTBOT_ALI_SECRET")
// 判断阿里云的key和secret是否存在
if certbot_ali_key == "" || certbot_ali_secret == "" {
fmt.Println("请设置环境变量CERTBOT_ALI_KEY和CERTBOT_ALI_SECRET")
os.Exit(1)
}
}
func main() {
client := dns.NewClient(certbot_ali_key, certbot_ali_secret)
var args = new(dns.DescribeDomainRecordsArgs)
args.DomainName = os.Getenv("CERTBOT_DOMAIN")
args.RRKeyWord = "_acme-challenge"
args.TypeKeyWord = "TXT"
res, err := client.DescribeDomainRecords(args)
if err == nil {
records := res.DomainRecords.Record
// 记录大于1执行更新,小于1执行创建
if len(records) > 0 {
for i := 0; i < len(records); i++ {
var update_args = new(dns.UpdateDomainRecordArgs)
update_args.RecordId = records[i].RecordId
update_args.RR = "_acme-challenge"
update_args.Value = os.Getenv("CERTBOT_VALIDATION")
update_args.Type = "TXT"
res, err := client.UpdateDomainRecord(update_args)
if err == nil {
fmt.Println("更新成功:", res.RecordId)
time.Sleep(time.Duration(20) * time.Second)
} else {
fmt.Println("更新失败:", err.Error())
os.Exit(2)
}
}
} else {
// 执行创建操作
var add_args = new(dns.AddDomainRecordArgs)
add_args.DomainName = os.Getenv("CERTBOT_VALIDATION")
add_args.RR="_acme-challenge"
add_args.Value=os.Getenv("CERTBOT_VALIDATION")
add_args.Type="TXT"
res,err:=client.AddDomainRecord(add_args)
if err == nil {
fmt.Println("创建成功:", res.RecordId)
time.Sleep(time.Duration(20) * time.Second)
} else {
fmt.Println("创建失败:", err.Error())
os.Exit(2)
}
}
}
}
评论区