certbot+shell+阿里云api+k8s实现自动化更新SSL证书

certbot + shell + 阿里云 API + k8s 实现自动化更新 SSL 证书

背景

当业务所使用的域名越来越多时,使用 certbot renew 生成证书后,还需手动上传到阿里云控制台,并修改 Ingress 的注解,整个过程非常繁琐且重复。

对于一个成熟的运维工程师来说,这显然不够优雅。

话不多说,直接开搞。

参考项目 :https://github.com/justjavac/certbot-dns-aliyun


1. 安装 certbot 和 jq
# 复制代码
yum -y install certbot jq
2.创建 RAM 用户用于 API 调用

权限策略要求

  • 添加 DNS 解析记录
  • 上传证书到阿里云证书服务(CAS)

推荐策略(JSON)

shell 复制代码
{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "alidns:AddDomainRecord",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": "yundun-cert:UploadUserCertificate",
      "Resource": "*"
    }
  ]
}

🔐 安全性建议:为策略添加来源 IP 限制(将 1.1.1.1 替换为你的服务器公网 IP):

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "alidns:AddDomainRecord",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": "yundun-cert:UploadUserCertificate",
      "Resource": "*",
      "Condition": {
        "IpAddress": {
          "acs:SourceIp": [
            "1.1.1.1"
          ]
        }
      }
    }
  ]
}
3. 安装阿里云 CLI 工具
shell 复制代码
/bin/bash -c "$(curl -fsSL https://aliyuncli.alicdn.com/install.sh)"

配置凭证

shell 复制代码
aliyun configure --profile akProfile

Configuring profile 'akProfile' in '' authenticate mode...
Access Key Id []: 在这里输入 Access Key
Access Key Secret []: 在这里输入 Secret
Default Region Id []: cn-hangzhou
Default Output Format [json]: json
Default Language [zh|en] en:
Saving profile[akProfile] ...Done.
4. 安装 certbot-dns-aliyun 插件
shell 复制代码
wget https://cdn.jsdelivr.net/gh/justjavac/certbot-dns-aliyun@main/alidns.sh

移动并赋予执行权限

shell 复制代码
mv alidns.sh /usr/local/bin/
chmod +x /usr/local/bin/alidns.sh
ln -s /usr/local/bin/alidns.sh /usr/local/bin/alidns

💡 可选:为防止误删 DNS 记录,可注释掉脚本中删除记录的代码。

5. 测试插件是否能正常申请证书
shell 复制代码
certbot certonly \
  -d *.example.com \
  --manual \
  --preferred-challenges dns \
  --manual-auth-hook "alidns" \
  --email admin@example.com \
  --agree-tos \
  --non-interactive \
  --no-eff-email \
  --dry-run

✅ 测试成功后,去掉 --dry-run 即可正式申请。

6. 上传证书 + 动态更新脚本(UploadUserCertificate.sh)
shell 复制代码
#!/bin/bash
set -euo pipefail

# ========================
# 参数处理
# ========================
DOMAIN=${1:-}
if [ -z "$DOMAIN" ]; then
  cat << EOF
❌ 用法: $0 <domain>
   示例:
     $0 '*.test.example.com'        # 更新所有子域名
     $0 webapp.example.com     # 更新单个域名

   支持通配符格式: *.example.com
EOF
  exit 1
fi

echo "[`date`] 🔄 开始刷新证书: $DOMAIN"

# ========================
# 确定证书目录(支持泛域名)
# ========================
CERT_DIR=""

if [[ "$DOMAIN" == \*.* ]]; then
  # 是泛域名,例如: *.example.com
  BASE_DOMAIN="${DOMAIN#\*.}"  # 提取 test.mall.example.com

  echo "[`date`] 🔍 查找泛域名证书: 尝试以下路径..."
  for candidate in \
    "/etc/letsencrypt/live/_wildcard.${BASE_DOMAIN}" \
    "/etc/letsencrypt/live/wildcard.${BASE_DOMAIN}" \
    "/etc/letsencrypt/live/${BASE_DOMAIN}" \
    "/etc/letsencrypt/live/${DOMAIN//\*/wildcard}" \
    "/etc/letsencrypt/live/${DOMAIN}"; do

    echo "  → 检查: $candidate"
    if [[ -f "$candidate/fullchain.pem" ]] && [[ -f "$candidate/privkey.pem" ]]; then
      CERT_DIR="$candidate"
      echo "  ✅ 找到!"
      break
    fi
  done

else
  # 单域名
  CERT_DIR="/etc/letsencrypt/live/$DOMAIN"
fi

if [ ! -f "$CERT_DIR/fullchain.pem" ] || [ ! -f "$CERT_DIR/privkey.pem" ]; then
  echo "❌ 证书文件不存在: $CERT_DIR"
  echo "   请检查 Certbot 是否已正确签发该域名证书。"
  exit 1
fi

# ========================
# 上传证书到阿里云 CAS
# ========================
CERT_NAME="cert-${DOMAIN//\*/wildcard}-$(date +%Y%m%d%H%M)"
echo "[`date`] 📤 正在上传证书到阿里云 CAS: $CERT_NAME"

# ========================
# 加载证书内容并去除换行
# ========================
CERT_CONTENT=$(cat "$CERT_DIR/fullchain.pem")
KEY_CONTENT=$(cat "$CERT_DIR/privkey.pem")

response=$(aliyun cas UploadUserCertificate \
  --region cn-hangzhou \
  --Name="$CERT_NAME" \
  --Cert="$CERT_CONTENT" \
  --Key="$KEY_CONTENT")

NEW_CERT_ID=$(echo "$response" | jq -r '.ResourceId')
if [ -z "$NEW_CERT_ID" ] || [ "$NEW_CERT_ID" = "null" ]; then
  echo "❌ 上传失败,未返回 CertId"
  echo "💡 响应: $response"
  exit 1
fi

echo "[`date`] ✅ 证书上传成功,Cert ID: $NEW_CERT_ID"


# ========================
# 查找所有匹配 DOMAIN 的 Ingress
# ========================
echo "[`date`] 🔍 正在扫描所有命名空间中的 Ingress..."

# 获取所有 Ingress: namespace/name=host1 host2 ...
mapfile -t INGRESS_HOSTS < <(
kubectl get ingress -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}={.spec.rules[*].host}{"\n"}{end}'
)

MATCHED_INGRESSES=()

for record in "${INGRESS_HOSTS[@]}"; do
  # 跳过空行
  [[ -z "$record" ]] && continue

  # 分割为 INGRESS_FULLNAME 和 HOSTS_STR
  INGRESS_FULLNAME="${record%%=*}"
  HOSTS_STR="${record#*=}"

  # 去除首尾空白
  HOSTS_STR="$(echo "$HOSTS_STR" | xargs)"

  # 如果没有 hosts,跳过
  [[ -z "$HOSTS_STR" ]] && continue

  # 将 hosts 字符串转为数组
  IFS=' ' read -r -a HOSTS <<< "$HOSTS_STR"

  # 检查是否有任意 host 匹配 DOMAIN
  MATCH=false
for host in "${HOSTS[@]}"; do
  [[ -z "$host" ]] && continue

  # 情况1: 精确匹配
  if [[ "$host" == "$DOMAIN" ]]; then
    MATCH=true
    break
  fi

  # 情况2: 通配符匹配 *.test.example.com
  if [[ "$DOMAIN" == \*.* ]]; then
    suffix="${DOMAIN#\*.}"  # 如 test.example.com

    # 必须是以 .$suffix 结尾
    if [[ "$host" != *".$suffix" ]]; then
      continue
    fi

    # 提取前缀部分:如 xxl-job.test → 剩下 "xxl-job"
    prefix="${host%.$suffix}"

    # 检查 prefix 中是否还包含 "."
    # 如果包含,说明是 admin.cms.test → prefix=admin.cms → 多层,跳过
    if [[ "$prefix" == *.* ]]; then
      echo "[`date`] 🔇 跳过多层子域: $host (不匹配 $DOMAIN)" >&2
      continue
    fi

    # 合法的一级子域,匹配成功
    MATCH=true
    break
  fi
  done

  # 如果匹配,记录该 Ingress
  if [[ "$MATCH" == true ]]; then
    NS="${INGRESS_FULLNAME%%/*}"
    NAME="${INGRESS_FULLNAME##*/}"
    MATCHED_INGRESSES+=("$NS:$NAME")
    echo "[`date`] ✅ 匹配到: $NAME ($NS) -> $host"
  fi
done

# ========================
# 统计并更新
# ========================
TOTAL_MATCHED=${#MATCHED_INGRESSES[@]}
if [ $TOTAL_MATCHED -eq 0 ]; then
  echo "[`date`] ⚠️  没有找到匹配 '$DOMAIN' 的 Ingress"
  echo "   请确认域名拼写或 Ingress 配置。"
  exit 0
fi

echo "[`date`] 🛠️  准备更新 $TOTAL_MATCHED 个 Ingress 的证书..."

for item in "${MATCHED_INGRESSES[@]}"; do
  NS="${item%:*}"
  NAME="${item#*:}"
  
  echo "[`date`] 📌 更新 Ingress: $NAME ($NS)"
  kubectl patch ingress "$NAME" -n "$NS" \
    -p "{\"metadata\":{\"annotations\":{\"alb.ingress.kubernetes.io/cert-id\":\"$NEW_CERT_ID\"}}}" \
    || echo "⚠️  更新失败或无变化: $NAME ($NS)"
done

echo "[`date`] 🚀 证书更新完成!共更新 $TOTAL_MATCHED 个 Ingress"
echo "[`date`] 💡 提示: ALB Controller 会自动同步证书,通常 1-2 分钟生效"
7. 执行脚本 + 验证

正式申请证书并部署

shell 复制代码
certbot certonly \
  -d api.example.com \
  --manual \
  --preferred-challenges dns \
  --manual-auth-hook "alidns" \
  --email y***@163.com \
  --agree-tos \
  --non-interactive \
  --no-eff-email \
  --deploy-hook "/root/UploadUserCertificate.sh api.example.com"

证书续期

shell 复制代码
certbot renew \
  --cert-name api.example.com \
  --manual \
  --preferred-challenges dns \
  --manual-auth-hook "alidns" \
  --email y****@163.com \
  --agree-tos \
  --non-interactive \
  --no-eff-email \
  --deploy-hook "/root/UploadUserCertificate.sh api.example.com"

定时续期

将续期命令加入 crontab:

相关推荐
天远Date Lab38 分钟前
零信任架构实战:基于天远车辆估值构建自动化品牌二手车置换智能定价与补贴测算网关
运维·人工智能·架构·自动化
qq_284274051 小时前
电气控制笔记:常用低压电器符号原理与异步电动机点动、自锁、多地控制电路(含考点)
笔记·学习·自动化·制造
乐橙开放平台2 小时前
不用换摄像头,如何用乐橙把海康、大华、宇视统一到一个界面
笔记·物联网·自动化·音视频·智能家居
zyseo82 小时前
谷歌SEO 抓取日志分析脚本:解析访问日志,看穿 Googlebot 的抓取行为
大数据·数据库·搜索引擎·自动化
FII工业富联科技服务2 小时前
AI Factory 技术架构解析:数字孪生、自动化、机器人与 AI Agent 如何协同?
人工智能·架构·自动化
P1Browser3 小时前
Puppeteer 与 Selenium 技术对比:多账号自动化管理如何做环境隔离?
selenium·测试工具·自动化
你看我名字3 小时前
LAMP 架构演练:从云主机准备到 Apache、PHP、MySQL 上线验证
运维·ci/cd·架构·kubernetes·jenkins
海姐软件测试3 小时前
Python 自动化与 AI 结合:基于 Jenkins 的回归验证实践
人工智能·python·自动化
AI_Cloud_推荐4 小时前
DeepSeek-V4-Pro 的 4.5 和 12:不是加价,是同名不同版本
阿里云·语言模型·代理模式·ai编程·百度云
余槐i4 小时前
yt-dlp 线程开到 16 反而更慢:1000 个视频的实测与限速原因
性能优化·ffmpeg·自动化·开发工具·yt-dlp