交代一下背景:

公司在AWS上用的是EKS+Fargate,还有一个跳板机,证书呢就不太想用aws收费的了,干脆用免费的证书好了。

那问题就来了,Let‘s encrypt的证书90天到期,已经手动更新过3次了,实在太麻烦了,能不能自动申请并更新到ACM呢?

答案是可以的,原理是:利用EC2可以携带IAM角色的原理,就可以无凭证更新免费证书到AWS Certificate Manager了

做法如下:

一、首先去ACM中拿到证书的ARN,要确保IAM权限不外泄,避免更新错证书

image-20250725144718592

二、生成一个IAM的policy,命名为AllowUpdateCertificate,只允许更新这两个特定的证书

具体策略内容如下:

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Action": [
				"acm:ImportCertificate"
			],
			"Resource": [
				"arn:aws:acm:ap-southeast-1:111111111111:certificate/arn01",
				"arn:aws:acm:ap-southeast-1:111111111111:certificate/arn02"
			]
		}
	]
}

image-20250725145030921

三、IAM生成一个role角色,命名为Ec2UpdateCertificateRole,把AllowUpdateCertificate策略给附上

image-20250725145132762

四、到EC2的实例详细信息中,操作–>安全–>修改IAM角色

image-20250725145419632

附上Ec2UpdateCertificateRole这个角色

image-20250725145533303

五、在EC2的机器上,装好AWS CLI,还有lego(申请Let’s encrypt证书的软件)

先写好获得证书的脚本 get_cert.sh,域名DNS解析是托管到cloudflare的,所以也直接用API TOKEN来处理

#!/bin/bash

CLOUDFLARE_DNS_API_TOKEN=######## /usr/local/bin/lego --path /usr/local/bin/certs --email zhangranrui@rendoumi.com --dns cloudflare --domains *.rendoumi.com --domains rendoumi.com renew --renew-hook="/usr/local/bin/update_aws_cert.sh"

参数--renew-hook如果有新证书,就调用后面的脚本。如果没有新证书,就无操作,避免无用功。

然后再写好 update_aws_cert.sh,这就是个千字文了,由gemini生成的:

由于lego生成的crt是证书+证书链,所以第一步要把证书部分给单独拆出来

#!/bin/bash

cd /usr/local/bin/certs/certificates

#首先把证书单独拆出来
awk '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/ {print} /-----END CERTIFICATE-----/ {exit}' _.rendoumi.com.crt > _.rendoumi.com.only

# --- 配置部分 ---
# 您要更新的现有 ACM 证书的 ARN
# 示例:CERTIFICATE_ARN="arn:aws:acm:ap-southeast-1:123456789012:certificate/abcdefab-1234-5678-abcd-1234567890ab"
CERTIFICATE_ARN="arn:aws:acm:ap-southeast-1:111111111111:certificate/11111111-1111-1111-1111-111111111111"

# 新证书文件的路径(例如:/opt/certs/new_certificate.crt)
# 确保这个路径在您的EC2实例上是正确的
CERTIFICATE_BODY_PATH="_.rendoumi.com.only"

# 新私钥文件的路径(例如:/opt/certs/private.key)
# 确保这个路径在您的EC2实例上是正确的
PRIVATE_KEY_PATH="_.rendoumi.com.key"

# 证书链文件的路径(可选)
# 如果没有证书链,请将此变量留空或注释掉
# 示例:CERTIFICATE_CHAIN_PATH="/opt/certs/ca_bundle.crt"
CERTIFICATE_CHAIN_PATH="_.rendoumi.com.issuer.crt" # 如果没有链,请将其留空 ""

# ACM 证书所在的 AWS 区域
# 这个区域应该与 CERTIFICATE_ARN 中的区域匹配
# 示例:AWS_REGION="ap-southeast-1"
AWS_REGION="ap-southeast-1"
# --- 配置部分结束 ---

echo "--- 开始使用实例角色重新导入 ACM 证书 ---"
echo "证书 ARN: ${CERTIFICATE_ARN}"
echo "区域: ${AWS_REGION}"

# 检查文件是否存在
if [ ! -f "${CERTIFICATE_BODY_PATH}" ]; then
    echo "错误:证书文件 '${CERTIFICATE_BODY_PATH}' 不存在。"
    exit 1
fi

if [ ! -f "${PRIVATE_KEY_PATH}" ]; then
    echo "错误:私钥文件 '${PRIVATE_KEY_PATH}' 不存在。"
    exit 1
fi

if [ -n "${CERTIFICATE_CHAIN_PATH}" ] && [ ! -f "${CERTIFICATE_CHAIN_PATH}" ]; then
    echo "警告:证书链文件 '${CERTIFICATE_CHAIN_PATH}' 已指定但不存在。将不包含证书链进行导入。"
    CERTIFICATE_CHAIN_PATH="" # 如果文件不存在,则清空路径
fi

# 构造 AWS CLI 命令。AWS CLI 会自动使用实例角色提供的凭证。
IMPORT_CMD="aws acm import-certificate \
  --certificate-arn ${CERTIFICATE_ARN} \
  --certificate fileb://${CERTIFICATE_BODY_PATH} \
  --private-key fileb://${PRIVATE_KEY_PATH} \
  --region \"${AWS_REGION}\""

# 如果有证书链,则添加到命令中
if [ -n "${CERTIFICATE_CHAIN_PATH}" ]; then
    IMPORT_CMD="${IMPORT_CMD} --certificate-chain fileb://${CERTIFICATE_CHAIN_PATH}"
fi

echo "正在执行命令:"
echo "$IMPORT_CMD"
echo "--------------------------"

# 执行命令
# 注意:这里我们不指定 --profile 或任何凭证,AWS CLI 会自动使用实例IAM角色。
eval $IMPORT_CMD

# 检查命令执行结果
if [ $? -eq 0 ]; then
    echo "--- 证书重新导入成功!---"
    echo "ACM 证书 ${CERTIFICATE_ARN} 已更新。"
else
    echo "--- 证书重新导入失败!---"
    echo "请检查错误消息,确保:"
    echo "1. EC2 实例已正确附加了具有 acm:ImportCertificate 权限的 IAM 角色。"
    echo "2. 证书、私钥和链文件路径正确且可读。"
    echo "3. 证书/私钥/链的格式正确。"
fi

echo "--- 脚本执行完毕 ---"

看起来真的是又臭又长,废话连篇。注意:gemini 生成的代码有问题,aws acm import-certificate 的参数中,证书文件的前缀是fileb://,gemini给的是file://,调试了半天,最后去aws看了文档才发现。

如上操作,脚本放入到crontb中,没有用到任何aws access id和secret,就能正确更新证书,善莫大焉。