使用 acme.sh 手动 DNS 申请通配符免费证书并导出证书文件

阿白
发布于 2026-07-07 / 20 阅读
0

使用 acme.sh 手动 DNS 申请通配符免费证书并导出证书文件

使用 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

证书申请成功,可以用于后续部署。