新机器上要部署一个带域名的网站,Nginx 安装、证书签发、自动续签配置——这套流程我在 Amazon Linux 2023 上已经操作过不下十几次。
网上的教程大多是 CentOS 时代的写法,搬到 AL2023 上会碰到不少问题:EPEL 装不上,crontab 命令不存在,续签任务看起来配好了,实际一次都没跑过。
这篇按我自己的实际操作顺序写,从系统检查一直到续签监控,命令都可以直接复制。新手照着做,半小时左右能弄完。
先看下我这台机器:
[ec2-user@ALG ~]$ cat /etc/os-release
NAME="Amazon Linux"
VERSION="2023"
PRETTY_NAME="Amazon Linux 2023.12.20261102"
下文域名统一用 example.com 和 www.example.com,换成你自己的就行。
装软件之前,先把网络和域名弄对
我帮人排查证书签不下来,大部分问题出在网络和 DNS 上,跟 Nginx 本身关系不大。所以这一步宁可多花五分钟。
EC2 默认分配的公网 IP,实例停止再启动就会变。域名解析到这个 IP 上,过段时间停机升个配置,网站就打不开了。去控制台 EC2 → 弹性 IP,分配一个,关联到实例上。
安全组入站规则加两条:TCP 80 和 TCP 443,来源都是 0.0.0.0/0。SSH 的 22 端口最好只放自己的 IP。80 端口配完 HTTPS 以后也要一直开着,续签要用,后面会细讲。
AL2023 默认没开 firewalld,SELinux 默认也是 permissive,系统层一般不用额外处理,看一眼心里有数:
systemctl is-active firewalld
getenforce
然后去域名服务商那里加 A 记录,主域名和 www 都指向弹性 IP。回到服务器上核对:
sudo dnf install -y bind-utils
curl -s https://checkip.amazonaws.com
dig +short example.com
dig +short www.example.com
dig +short AAAA example.com
前三条 IP 一致就没问题。最后那条 AAAA 建议一定要查,有的服务商会默认给域名带一条 IPv6 记录,而你的 EC2 没配 IPv6,Let's Encrypt 验证时优先走 IPv6,连不上就失败,报错内容还看不出跟 IPv6 有关系。我同事就在这上面卡了一个多小时,删掉那条记录后一次就过了。
DNS 刚改完可能要等几分钟生效,dig 结果对了再往下走。
安装 Nginx
AL2023 官方仓库自带 Nginx,不用加第三方源:
sudo dnf update -y
sudo dnf install -y nginx
sudo systemctl enable --now nginx
nginx -v
curl -I http://127.0.0.1
返回 HTTP/1.1 200 OK 就是起来了。再用自己电脑的浏览器访问 http://弹性IP,能看到 Nginx 欢迎页,说明安全组也通了。
nginx -v 输出的版本号先记下来,后面写 HTTP/2 配置要用。
接着建网站目录。别放在 /home/ec2-user 下面,这个目录权限是 700,Nginx 的 worker 进程用 nginx 用户运行,读不到里面的文件,访问就是 403。我统一放 /var/www:
sudo mkdir -p /var/www/example.com /var/www/letsencrypt
echo '<h1>Hello HTTPS</h1>' | sudo tee /var/www/example.com/index.html
多建的 /var/www/letsencrypt 是给证书验证用的,具体原理下面讲 Certbot 的时候再说。这个路径我习惯单独给一个目录,跟业务文件分开,以后换网站代码也不会影响续签。
站点配置放在 /etc/nginx/conf.d/ 下面,主配置 nginx.conf 尽量不动,以后升级 Nginx 会省事很多:
sudo vim /etc/nginx/conf.d/example.com.conf
先写一个只有 80 端口的版本:
server {
listen 80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
}
location / {
root /var/www/example.com;
index index.html;
}
}
改完配置先测试,再重载:
sudo nginx -t
sudo systemctl reload nginx
nginx -t 显示 syntax is ok 和 test is successful 才能 reload。我以前改完配置直接 restart,少了个分号,Nginx 起不来,网站挂了好几分钟。reload 在配置有错时会拒绝加载,旧进程继续工作,比 restart 安全得多。
验证路径通不通,可以自己放个文件测一下:
sudo mkdir -p /var/www/letsencrypt/.well-known/acme-challenge
echo ok | sudo tee /var/www/letsencrypt/.well-known/acme-challenge/test
curl http://example.com/.well-known/acme-challenge/test
能返回 ok 就说明外部访问这个路径没问题,测完把 test 文件删掉。证书签不下来的时候,先用它排查最快。
Certbot 是什么,它替你干了哪些事
很多新手把 Certbot 和 Let's Encrypt 当成一回事,其实是两样东西。
Let's Encrypt 是发证书的机构,也就是 CA,由非营利组织 ISRG 运营,免费给网站签发证书。Certbot 是装在你服务器上的客户端,由 EFF(电子前哨基金会)开发维护,负责去 Let's Encrypt 申请证书、完成验证、把证书存到本地,以后再按时续签。打个比方,Let's Encrypt 是办证大厅,Certbot 是替你跑腿排队的人。
官网地址是 https://certbot.eff.org/ 。

两边沟通用的是 ACME 协议,标准编号 RFC 8555。所以 Certbot 并不是唯一选择,acme.sh、lego 也都是 ACME 客户端,原理一样。我用 Certbot 主要是因为它是 Let's Encrypt 官方推荐的,文档全,出了问题搜得到答案。acme.sh 是纯 shell 写的,支持的 DNS 服务商多,用惯了也挺顺手。
签一张证书,Certbot 在后台大致做了这些事:第一次运行时生成账号密钥,在 Let's Encrypt 注册一个账号。然后告诉 CA 要给哪几个域名申请证书,CA 会出一道“考题”,叫 challenge。Certbot 按要求把答案放好,CA 从外网来检查,检查通过就说明这个域名确实归你管。最后 Certbot 在本地生成证书私钥,提交证书请求,把签好的证书拿回来存到 /etc/letsencrypt 下面。私钥从头到尾都留在你自己的服务器上,不会发给 CA。
考题常见的有两种。
HTTP-01 是最常用的验证方式,CA 会访问 http://你的域名/.well-known/acme-challenge/一串随机文件名,能读到 Certbot 放的内容就算通过。它只走 80 端口,这也是 80 不能关的根本原因。
DNS-01 是让你在域名下加一条 _acme-challenge 开头的 TXT 记录。它不需要开任何端口,也只有它能签通配符证书 *.example.com。代价是续签时要能自动改 DNS,得装对应的 DNS 插件,比如域名托管在 Route 53 上,可以用 certbot-dns-route53 配合 IAM 角色。
至于 Certbot 怎么交这份答卷,它有几种插件可选:
--webroot:往已经在运行的网站目录里放验证文件,Nginx 不用停,配置也不会被改动,本文用的就是这个
--nginx:自动读取并修改 Nginx 配置,临时加验证规则,签完还能顺手帮你写好 443 配置,省事,但配置会被它改得比较乱
--standalone:Certbot 自己临时起一个 Web 服务占用 80 端口,机器上已经有 Nginx 就得先停掉,续签时也一样,不适合线上
--manual:手动放文件或者手动加 DNS 记录,偶尔签一张可以,没办法自动续签
想清楚这几种方式的区别,后面配置为什么这么写,是不是就好理解多了?
安装 Certbot
从 CentOS 过来的人,第一反应是 yum install epel-release。AL2023 不支持 EPEL,这条路走不通。
可以先 dnf search certbot 看看官方仓库有没有,不同版本的仓库内容会有变化。我自己用的是 Certbot 官方推荐的 pip 虚拟环境方式,版本新,也不会跟系统 Python 包冲突。系统自带的 python3 是 3.9,我另外装个 3.11:
sudo dnf install -y python3.11 python3.11-pip
sudo python3.11 -m venv /opt/certbot
sudo /opt/certbot/bin/pip install --upgrade pip
sudo /opt/certbot/bin/pip install certbot
sudo ln -sf /opt/certbot/bin/certbot /usr/bin/certbot
certbot --version
能打印版本号就装好了。用的是 webroot 模式,所以 certbot-nginx 插件就没装,Nginx 配置自己写,出了问题也知道该查哪里。
签发证书
签正式证书之前,先用 --dry-run 演练一次。Let's Encrypt 有频率限制,同一组域名 7 天内最多签 5 张一样的证书,反复删了重签很容易被限住,只能等。dry-run 走测试环境,不占次数,也不会保存证书:
sudo certbot certonly --webroot -w /var/www/letsencrypt \
-d example.com -d www.example.com --dry-run
看到 The dry run was successful 再签正式的:
sudo certbot certonly --webroot -w /var/www/letsencrypt \
-d example.com -d www.example.com \
-m 你的邮箱 --agree-tos --no-eff-email
成功后证书路径是:
/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem
现在把站点配置改成完整版:
server {
listen 80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
80 端口那段特意保留了验证路径,其他请求一律跳转 HTTPS。这样续签照常走 80,用户访问 http 也会被带到 https。
http2 on 这种写法要 Nginx 1.25.1 及以上。前面记下的版本号如果比这个低,就删掉这一行,把 listen 改成 listen 443 ssl http2。
sudo nginx -t && sudo systemctl reload nginx
curl -I https://example.com
curl -I http://example.com
第一条返回 200,第二条返回 301,浏览器地址栏有小锁,证书部分就完成了。
签完之后,/etc/letsencrypt 里都有什么
这个目录建议花两分钟看一下,以后排查续签问题基本都在这里:
sudo ls /etc/letsencrypt
几个关键目录:
accounts:Let's Encrypt 账号信息和账号密钥
archive:证书的真实文件,每续签一次多一组,名字是 cert1.pem、cert2.pem 这样递增
live:指向 archive 里最新一组文件的软链接,Nginx 配置引用的就是这里
renewal:每张证书一个续签配置文件
renewal-hooks:放续签前后要执行的脚本,下面有 pre、deploy、post 三个子目录
Nginx 配置里一定要写 live 目录。续签后 live 下的软链接会自动指向新文件。有人把证书复制到 /etc/nginx/ssl/ 下面再引用,续签明明成功了,Nginx 读的还是旧证书,到期照样出问题。
renewal 目录里的文件也值得打开看看:
sudo cat /etc/letsencrypt/renewal/example.com.conf
里面大概是这样:
[renewalparams]
account = 一串账号ID
authenticator = webroot
server = https://acme-v02.api.letsencrypt.org/directory
key_type = ecdsa
[[webroot_map]]
example.com = /var/www/letsencrypt
www.example.com = /var/www/letsencrypt
签证书时用了什么验证方式、webroot 目录在哪,Certbot 都记在这个文件里。所以后面执行 certbot renew 不用带任何参数,它会照着这份记录重新走一遍。反过来说,以后要是改了验证目录,这里没跟着改,续签就会失败。key_type = ecdsa 是 Certbot 2.0 之后的默认值,证书用的是 ECDSA 密钥,比 RSA 更短,握手更快,现在的浏览器都支持。
日常用得最多的几条命令,我整理在这:
# 查看所有证书、包含的域名和到期时间
sudo certbot certificates
# 演练续签
sudo certbot renew --dry-run
# 给现有证书加一个域名,所有域名要一起写上
sudo certbot certonly --webroot -w /var/www/letsencrypt \
--cert-name example.com \
-d example.com -d www.example.com -d api.example.com
# 不再使用的证书,删掉后续签任务也会跳过它
sudo certbot delete --cert-name example.com
# 私钥泄露时吊销证书
sudo certbot revoke --cert-name example.com
还有一个 --force-renewal 参数可以强制续签,它会计入频率限制,平时别随手加。
自动续签
这里是 AL2023 上最容易漏掉的地方。pip 装的 certbot 不会自动建定时任务,AL2023 默认又没有安装 cron。照着网上的教程执行 crontab -e,要么提示命令不存在,要么装了 cronie 却没启动 crond,任务写进去了但不会执行。
系统本身就是 systemd,我直接用 systemd timer,不用再装别的东西。
先写 service:
sudo vim /etc/systemd/system/certbot-renew.service
[Unit]
Description=Certbot Renew
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/opt/certbot/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
再写 timer:
sudo vim /etc/systemd/system/certbot-renew.timer
[Unit]
Description=Run certbot renew twice daily
[Timer]
OnCalendar=*-*-* 03,15:00:00
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
一天检查两次是 certbot 官方的建议。证书离到期还远的时候,renew 检查完就直接退出,默认在到期前 30 天左右才真正续签。RandomizedDelaySec 加一点随机延迟,避免大量机器同一时间请求 Let's Encrypt。Persistent=true 的意思是到点时机器正好关机,开机后会补跑一次。
Certbot 的钩子有三种。pre-hook 在续签前执行,post-hook 在续签尝试后执行,deploy-hook 只在某张证书真正续签成功后才执行。reload Nginx 放在 deploy-hook 里最合适,证书没换就不会去动 Nginx。钩子也可以写成脚本放进 /etc/letsencrypt/renewal-hooks/deploy/,加上执行权限,Certbot 续签成功后会自动调用。证书要同步到别的机器,或者顺带发个通知,都可以写在这里面。
启用定时器:
sudo systemctl daemon-reload
sudo systemctl enable --now certbot-renew.timer
systemctl list-timers | grep certbot
能看到下次执行时间,说明定时器生效了。不过这只能说明任务排上了,续签能不能成功还得手动跑一遍:
sudo certbot renew --dry-run
sudo systemctl start certbot-renew.service
journalctl -u certbot-renew.service -n 50 --no-pager
dry-run 显示 all simulated renewals succeeded,service 日志里也没有报错,这样才算配完。如果失败,更详细的日志在 /var/log/letsencrypt/letsencrypt.log。
certbot 本身也要定期升级,我一般每个季度升一次:
sudo /opt/certbot/bin/pip install --upgrade certbot
还有个变化要知道:Let's Encrypt 从 2025 年起已经不再发送证书到期提醒邮件,证书有效期以后也会从 90 天逐步缩短到 45 天。续签失败后不会再有邮件提醒,只能靠自己做监控。
80 端口为什么不能关
我之前遇到过一次。机器上的定时任务一直正常运行,后来做安全整改,有同事觉得全站都上 HTTPS 了,80 端口没用,就在安全组里删掉了。这个想法乍一听挺合理,我当时也没拦。
前面说过,webroot 走的是 HTTP-01 验证,Let's Encrypt 必须从外网访问 80 端口才能读到验证文件。80 一关,每次续签都会失败。从到期前 30 天开始,它一天失败两次,日志里全是 Timeout during connect,因为加了 --quiet,也没人去看。最后证书过期,用户打开网站看到一整页安全警告,我们才发现。
处理起来很快,安全组加回 80,手动 certbot renew,一分钟就恢复了。可网站已经有好几个小时不能正常访问。
所以 80 端口要一直开着,除了验证路径什么内容都不提供,只负责跳转。确实要关 80 的话,就改用 DNS-01 验证,那是另一套配置,这里先不展开。
这些坑我和身边的人都踩过
- 安全组里的 80 端口不要关
- 证书路径写 live 目录,别写 archive,也别复制到别的地方
- 调试时先用
--dry-run,别拿正式环境反复试
- 改了 webroot 目录,记得同步 renewal 配置文件
- AL2023 默认没有 cron,别直接照抄 crontab 教程
- 改完配置先
nginx -t,再 reload
- 域名的 AAAA 记录要检查
- 记得绑定弹性 IP
- 网站文件别放
/home/ec2-user
- 配了自动续签,也要加到期监控
到期监控脚本
这个脚本从外部连接 443 端口获取证书,检查的是 Nginx 实际返回给用户的那张,比检查本地文件可靠:
#!/bin/bash
# check_cert.sh 用法: ./check_cert.sh example.com
DOMAIN=${1:-example.com}
WARN_DAYS=20
END=$(echo | openssl s_client -connect "${DOMAIN}:443" -servername "${DOMAIN}" 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
if [ -z "$END" ]; then
echo "[ERROR] ${DOMAIN} 获取证书失败"
exit 2
fi
DAYS=$(( ( $(date -d "$END" +%s) - $(date +%s) ) / 86400 ))
if [ "$DAYS" -lt "$WARN_DAYS" ]; then
echo "[WARN] ${DOMAIN} 证书还剩 ${DAYS} 天,检查续签日志"
exit 1
fi
echo "[OK] ${DOMAIN} 证书还剩 ${DAYS} 天"
续签正常时,剩余天数一般会保持在 30 天上下。降到 20 天以下还没恢复,说明续签已经卡住了。这时离过期还有将近三周,有足够时间排查。告警可以接企业微信或钉钉机器人,在脚本里加一行 curl 就行。脚本最好放在另一台机器上跑,网站所在的机器出了问题,它也能照常告警。
整套流程下来,Nginx 和证书本身都不复杂。把 Certbot 的工作方式弄明白以后,大部分问题都能顺着查下去:验证失败就看 80 端口和 DNS,续签没跑就看 timer,续签成功网站没变就看证书路径和 deploy-hook。
我现在每次配完 HTTPS,都会跑 nginx -t、certbot renew --dry-run、systemctl list-timers 这三条命令,结果都正常,再把监控脚本挂上,这件事才算真正做完。
如果对这类实操内容感兴趣,欢迎到云栈社区和更多同行一起交流。