使用 acme.sh 手动 DNS 申请通配符证书并导出证书文件
一、背景
这次需要给一个域名申请 HTTPS 通配符证书,例如:
example.com
*.example.com
目标是让主域名和一级子域名都可以使用同一张证书:
example.com
www.example.com
api.example.com
admin.example.com
本教程使用:
acme.sh + Let's Encrypt + 手动 DNS 验证
最终只导出证书文件,不自动安装到 Nginx,也不绑定到任何具体平台。导出的证书可以用于:
Nginx
Apache
负载均衡
CDN
Rancher
Docker 服务
Kubernetes Ingress
其他需要上传证书的管理面板
本文以 example.com 为示例,实际操作时替换成自己的域名。
二、通配符证书说明
通配符证书一般是:
*.example.com
它可以覆盖:
www.example.com
api.example.com
admin.example.com
但需要注意:
*.example.com 不包含 example.com 本身
所以申请时建议同时申请:
-d example.com
-d '*.example.com'
最终一张证书里会同时包含:
DNS:*.example.com
DNS:example.com
这是一张证书,不是两张证书。
三、安装 acme.sh
安装 acme.sh:
curl https://get.acme.sh | sh -s email=your-email@example.com
重新加载环境变量:
source ~/.bashrc
设置默认 CA 为 Let’s Encrypt:
acme.sh --set-default-ca --server letsencrypt
确认安装成功:
acme.sh --version
四、准备申请脚本
这个脚本适合手动 DNS 验证场景。
执行流程是:
第一次执行脚本:生成 DNS TXT 验证值
手动去 DNS 后台添加 TXT 记录
第二次执行脚本:继续验证,签发证书,并导出证书文件
保存脚本:
vi /root/manual-wildcard-cert-export.sh
脚本内容如下:
#!/usr/bin/env bash
set -euo pipefail
# =========================
# 需要修改的配置
# =========================
# 主域名,不要带 http/https
DOMAIN="example.com"
# acme.sh 路径
ACME_SH="${HOME}/.acme.sh/acme.sh"
# 证书导出目录
# 注意:不要设置成 ~/.acme.sh/${DOMAIN}
# 避免覆盖 acme.sh 自己维护的证书文件
EXPORT_DIR="${HOME}/certs/${DOMAIN}"
# 证书签发机构
ACME_SERVER="letsencrypt"
# 使用 RSA 2048,兼容性较好
KEY_LENGTH="2048"
# 手动 DNS 模式确认参数
MANUAL_FLAG="--yes-I-know-dns-manual-mode-enough-go-ahead-please"
# 状态文件,用于判断是第一次执行还是第二次执行
STATE_DIR="${HOME}/.acme-manual-state"
STATE_FILE="${STATE_DIR}/${DOMAIN}.pending"
# acme.sh 内部证书目录,RSA 模式一般是这个路径
ACME_CERT_DIR="${HOME}/.acme.sh/${DOMAIN}"
# =========================
# 脚本逻辑
# =========================
if [ ! -x "$ACME_SH" ]; then
echo "找不到 acme.sh:${ACME_SH}"
echo "请确认 acme.sh 是否已经安装,或者修改脚本里的 ACME_SH 路径。"
exit 1
fi
mkdir -p "$STATE_DIR"
echo "当前域名:${DOMAIN}"
echo "申请范围:${DOMAIN} 和 *.${DOMAIN}"
echo "导出目录:${EXPORT_DIR}"
echo
export_cert_files() {
echo
echo "开始导出证书文件..."
if [ ! -d "$ACME_CERT_DIR" ]; then
echo "找不到 acme.sh 证书目录:${ACME_CERT_DIR}"
echo "可能证书还没有签发成功。"
exit 1
fi
mkdir -p "$EXPORT_DIR"
CERT_FILE="${ACME_CERT_DIR}/${DOMAIN}.cer"
KEY_FILE="${ACME_CERT_DIR}/${DOMAIN}.key"
CA_FILE="${ACME_CERT_DIR}/ca.cer"
FULLCHAIN_FILE="${ACME_CERT_DIR}/fullchain.cer"
if [ ! -f "$CERT_FILE" ] || [ ! -f "$KEY_FILE" ] || [ ! -f "$FULLCHAIN_FILE" ]; then
echo "证书文件不完整,请检查 acme.sh 输出。"
echo "预期文件:"
echo "$CERT_FILE"
echo "$KEY_FILE"
echo "$CA_FILE"
echo "$FULLCHAIN_FILE"
exit 1
fi
cp "$CERT_FILE" "${EXPORT_DIR}/cert.pem"
cp "$KEY_FILE" "${EXPORT_DIR}/key.pem"
cp "$FULLCHAIN_FILE" "${EXPORT_DIR}/fullchain.pem"
if [ -f "$CA_FILE" ]; then
cp "$CA_FILE" "${EXPORT_DIR}/ca.pem"
fi
chmod 600 "${EXPORT_DIR}/key.pem"
chmod 644 "${EXPORT_DIR}/cert.pem" "${EXPORT_DIR}/fullchain.pem" 2>/dev/null || true
[ -f "${EXPORT_DIR}/ca.pem" ] && chmod 644 "${EXPORT_DIR}/ca.pem"
echo
echo "证书已导出:"
echo "域名证书:${EXPORT_DIR}/cert.pem"
echo "私钥文件:${EXPORT_DIR}/key.pem"
echo "完整证书链:${EXPORT_DIR}/fullchain.pem"
[ -f "${EXPORT_DIR}/ca.pem" ] && echo "中间证书链:${EXPORT_DIR}/ca.pem"
echo
echo "常见使用方式:"
echo
echo "1. 如果服务只需要证书和私钥:"
echo " 证书文件使用:${EXPORT_DIR}/fullchain.pem"
echo " 私钥文件使用:${EXPORT_DIR}/key.pem"
echo
echo "2. 如果服务要求证书、私钥、证书链分开填写:"
echo " 证书文件使用:${EXPORT_DIR}/cert.pem"
echo " 私钥文件使用:${EXPORT_DIR}/key.pem"
echo " 证书链使用:${EXPORT_DIR}/ca.pem"
}
show_dns_check_tip() {
echo
echo "可以用下面命令检查 TXT 是否生效:"
echo "dig TXT _acme-challenge.${DOMAIN} +short"
echo
echo "也可以指定公共 DNS 检查:"
echo "dig TXT _acme-challenge.${DOMAIN} @8.8.8.8 +short"
}
# 第一次执行:生成 TXT 验证值
if [ ! -f "$STATE_FILE" ]; then
echo "这是第一次执行。"
echo "接下来 acme.sh 会输出 DNS TXT 记录。"
echo
set +e
"$ACME_SH" --issue --server "$ACME_SERVER" --dns \
-d "$DOMAIN" \
-d "*.${DOMAIN}" \
--keylength "$KEY_LENGTH" \
$MANUAL_FLAG
ACME_EXIT_CODE=$?
set -e
touch "$STATE_FILE"
echo
echo "=================================================="
echo "第一步完成。"
echo
echo "请去 DNS 后台添加 acme.sh 输出的 TXT 记录。"
echo
echo "通常主机记录是:"
echo "_acme-challenge"
echo
echo "注意:"
echo "如果 acme.sh 输出了两条 TXT value,就添加两条 TXT 记录。"
echo "不要只添加一条,也不要把第二条覆盖第一条。"
echo "=================================================="
show_dns_check_tip
echo
echo "DNS 生效后,再次执行本脚本:"
echo "$0"
echo
exit 0
fi
# 第二次执行:继续验证并签发
echo "检测到 pending 状态,这是第二次执行。"
echo "将继续验证 DNS TXT 并签发证书。"
echo
if command -v dig >/dev/null 2>&1; then
echo "当前 DNS TXT 查询结果:"
dig TXT "_acme-challenge.${DOMAIN}" +short || true
echo
fi
"$ACME_SH" --renew \
-d "$DOMAIN" \
-d "*.${DOMAIN}" \
--keylength "$KEY_LENGTH" \
$MANUAL_FLAG
export_cert_files
rm -f "$STATE_FILE"
echo
echo "=================================================="
echo "证书申请并导出完成。"
echo
echo "导出目录:${EXPORT_DIR}"
echo "=================================================="
五、修改脚本配置
把脚本里的:
DOMAIN="example.com"
改成自己的域名。
例如:
DOMAIN="mydomain.com"
导出目录默认是:
/root/certs/mydomain.com
注意不要把导出目录设置到:
/root/.acme.sh/mydomain.com
因为这是 acme.sh 自己维护证书的目录,导出文件和内部文件混在一起,容易误删或覆盖。
六、给脚本执行权限
chmod +x /root/manual-wildcard-cert-export.sh
七、第一次执行脚本
/root/manual-wildcard-cert-export.sh
第一次执行后,acme.sh 会输出 DNS TXT 验证信息,类似:
Domain: _acme-challenge.example.com
TXT value: xxxxxxxxxxxxxxxxxxxxx
如果同时申请主域名和通配符域名,可能会输出两条 TXT value。
去 DNS 管理后台添加 TXT 记录。
通常填写方式如下:
| 类型 | 主机记录 | 记录值 |
|---|---|---|
| TXT | _acme-challenge |
acme.sh 输出的 value |
如果 acme.sh 输出两条 TXT value,就添加两条 TXT 记录。
不要把第二条覆盖第一条。
正确状态应该类似:
_acme-challenge.example.com TXT value1
_acme-challenge.example.com TXT value2
八、检查 DNS 是否生效
添加 TXT 后,等待一会儿。
执行:
dig TXT _acme-challenge.example.com +short
把 example.com 换成自己的域名。
也可以指定公共 DNS 检查:
dig TXT _acme-challenge.example.com @8.8.8.8 +short
如果能看到刚才添加的 TXT value,说明 DNS 已经生效。
如果 acme.sh 要求两条 TXT value,这里最好能看到两条。
九、第二次执行脚本
DNS 生效后,再次执行脚本:
/root/manual-wildcard-cert-export.sh
这次脚本会继续验证 DNS,并签发证书。
成功后会导出到:
/root/certs/example.com/
目录里一般会有:
cert.pem
key.pem
fullchain.pem
ca.pem
文件含义如下:
| 文件 | 说明 |
|---|---|
cert.pem |
域名证书 |
key.pem |
私钥 |
fullchain.pem |
域名证书 + 中间证书链 |
ca.pem |
中间证书链 |
十、验证证书内容
签发完成后,建议检查证书里是否包含主域名和通配符域名。
执行:
openssl x509 -in /root/certs/example.com/fullchain.pem -noout -text | grep -A2 "Subject Alternative Name"
正常应该看到类似:
X509v3 Subject Alternative Name:
DNS:*.example.com, DNS:example.com
如果实际域名是 mydomain.com,正常结果类似:
X509v3 Subject Alternative Name:
DNS:*.mydomain.com, DNS:mydomain.com
看到这两个就说明证书是正确的。
十一、证书文件怎么用
不同平台对证书文件的要求不太一样。
1. 只要求证书和私钥
大多数面板或服务只需要两个文件:
Certificate / 证书 -> fullchain.pem
Private Key / 私钥 -> key.pem
推荐优先使用:
fullchain.pem
key.pem
因为 fullchain.pem 包含域名证书和中间证书链,兼容性更好。
2. 要求证书、私钥、证书链分开填写
有些平台会有三个输入项:
Certificate
Private Key
Chain Certificate
可以这样填:
Certificate -> cert.pem
Private Key -> key.pem
Chain Certificate -> ca.pem
如果填完后浏览器提示证书链不完整,可以改用:
Certificate -> fullchain.pem
Private Key -> key.pem
Chain Certificate -> 空
具体以平台实际校验结果为准。
3. Nginx 使用示例
如果要用于 Nginx,可以这样配置:
server {
listen 443 ssl http2;
server_name example.com *.example.com;
ssl_certificate /root/certs/example.com/fullchain.pem;
ssl_certificate_key /root/certs/example.com/key.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}
检查配置:
nginx -t
systemctl reload nginx
十二、线上验证证书
证书部署到服务后,可以用 openssl 测试线上站点是否使用了新证书。
例如:
echo | openssl s_client -connect www.example.com:443 -servername www.example.com 2>/dev/null | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
正常应该能看到:
DNS:*.example.com, DNS:example.com
这说明线上服务已经使用了这张通配符证书。
十三、常见问题
1. 只生成了一份证书,是不是少了?
不是。
通配符证书不是两张证书,而是一张证书里包含多个域名。
只要检查结果里有:
DNS:*.example.com, DNS:example.com
就说明证书是完整的。
2. 为什么证书管理页面显示 example.com + 1 other?
这是正常显示。
它表示证书里有两个域名:
example.com
*.example.com
页面只是把第二个域名折叠显示了。
3. DNS TXT 只剩一条会有问题吗?
要看 acme.sh 当时要求几条。
如果它要求两条 TXT value,但 DNS 后台只保留了一条,就会验证失败。
正确做法是添加两条 TXT 记录,主机记录都可以是:
_acme-challenge
但记录值不同。
4. 再次执行脚本为什么没有重新申请?
acme.sh 会判断证书是否还没到期。
如果证书还在有效期内,它可能会跳过续期。
如果只是想重新导出证书,不需要重新申请,直接从 acme.sh 目录复制即可。
5. 如何重新导出已有证书?
DOMAIN="example.com"
ACME_CERT_DIR="$HOME/.acme.sh/${DOMAIN}"
EXPORT_DIR="$HOME/certs/${DOMAIN}"
mkdir -p "$EXPORT_DIR"
cp "${ACME_CERT_DIR}/${DOMAIN}.cer" "${EXPORT_DIR}/cert.pem"
cp "${ACME_CERT_DIR}/${DOMAIN}.key" "${EXPORT_DIR}/key.pem"
cp "${ACME_CERT_DIR}/fullchain.cer" "${EXPORT_DIR}/fullchain.pem"
if [ -f "${ACME_CERT_DIR}/ca.cer" ]; then
cp "${ACME_CERT_DIR}/ca.cer" "${EXPORT_DIR}/ca.pem"
fi
chmod 600 "${EXPORT_DIR}/key.pem"
ls -lah "$EXPORT_DIR"
6. 如何强制重新申请?
先清理脚本状态:
DOMAIN="example.com"
rm -f "$HOME/.acme-manual-state/${DOMAIN}.pending"
然后强制重新发起申请:
DOMAIN="example.com"
acme.sh --issue --server letsencrypt --dns \
-d "$DOMAIN" \
-d "*.${DOMAIN}" \
--keylength 2048 \
--force \
--yes-I-know-dns-manual-mode-enough-go-ahead-please
添加新的 DNS TXT 后,再执行:
acme.sh --renew \
-d "$DOMAIN" \
-d "*.${DOMAIN}" \
--keylength 2048 \
--force \
--yes-I-know-dns-manual-mode-enough-go-ahead-please
然后重新导出证书。
十四、注意事项
这套方案是 手动 DNS 验证,适合临时申请、测试、或者不方便开放 DNS API 权限的场景。
缺点是:
不能真正自动续期
证书快过期时,需要重新执行:
第一次运行脚本
添加新的 DNS TXT
第二次运行脚本
重新部署或上传证书
如果是长期生产环境,建议后续改成 DNS API 自动续期,这样不需要每次手动添加 TXT 记录。
十五、本次验证结果示例
执行:
openssl x509 -in fullchain.pem -noout -text | grep -A2 "Subject Alternative Name"
输出:
X509v3 Subject Alternative Name:
DNS:*.mydomain.com, DNS:mydomain.com
说明证书已经同时覆盖:
mydomain.com
*.mydomain.com
证书申请成功,可以用于后续部署。