Integrations

aliyun-oidc-cert-renew

Try it

用 GitHub Actions + 阿里云 RAM OIDC 自动续期 Let's Encrypt 通配证书并分发到 OSS/FC/CDN,仓库零长期 AK。当用户说「自动续期证书」「不想存 AK」「GitHub Actions 操作阿里云」「SSL 证书过期」「新增子域自动上 HTTPS」「免密钥访问阿里云」时使用。

What it does

阿里云证书自动续期(GitHub Actions + RAM OIDC,免长期 AK)

The skill document

阿里云证书自动续期(GitHub Actions + RAM OIDC,免长期 AK)

何时用

  • 想让定时任务自动续期 Let's Encrypt 证书并分发到阿里云(OSS / FC / CDN),但不想把长期 AK/SK 塞进仓库 Secrets。
  • 想让新子域加一条 CNAME 就自动上 HTTPS,不用改代码。
  • 更广义:任何「GitHub Actions(或别的 OIDC IdP)免密钥操作阿里云」的场景,本文第一、二节可直接复用。

架构

GitHub Actions (id-token: write)
   │ 1. 请求 OIDC JWT(显式 audience)
   ▼
RAM OIDC 提供商 (token.actions.githubusercontent.com)
   │ 2. AssumeRoleWithOIDC
   ▼
RAM 角色 → 临时 STS 凭证(1h)
   │ 3. 写入 $GITHUB_ENV
   ▼
cert_manager.py(ACME DNS-01 签发 → CAS 上传 → OSS/FC 分发)
   │ 4. 状态回写
   ▼
OSS 私有桶(account.key / privkey.pem / state.json)

仓库里只需要两个非敏感 Secret:ALI_ROLE_ARN、ALI_OIDC_ARN。

一、RAM 侧搭建(一次性,用永久 AK 调 API,AK 不落盘)

账号 ID 用 sts.GetCallerIdentity 拿,无需用户名。

1. OIDC 提供商 —— 属 IMS API,不是 ram20150501

pip install alibabacloud_ims20190815
  • 方法 create_oidcprovider(小写 p),模型 CreateOIDCProviderRequest
  • 字段全部 snake_case:oidc_provider_name、issuer_url、client_ids、fingerprints、description
  • issuer_url = https://token.actions.githubusercontent.com
  • client_ids = audience,如 https://github.com//
  • 指纹必须纯 hex、无冒号:
openssl s_client -connect token.actions.githubusercontent.com:443 /dev/null \
  | openssl x509 -pubkey -noout \
  | openssl rsa -pubin -outform der 2>/dev/null \
  | openssl dgst -sha1        # 取输出里的 hex,去掉冒号

2. 角色(alibabacloud_ram20150501)

ram.create_role(CreateRoleRequest(
    role_name=ROLE, assume_role_policy_document=json.dumps(trust), description=...))

信任策略 —— oidc:sub 一定要 StringLike + 通配(原因见「坑 3」):

{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow",
  "Principal":{"Federated":"acs:ram:::oidc-provider/github"},
  "Condition":{"StringEquals":{"oidc:iss":"https://token.actions.githubusercontent.com",
                                "oidc:aud":"https://github.com//"},
               "StringLike":{"oidc:sub":"repo:*/*:ref:refs/heads/main"}}}],
 "Version":"1"}

改信任范围用 update_role(UpdateRoleRequest(role_name=..., new_assume_role_policy_document=...)) —— 字段名是 new_assume_role_policy_document。

3. 权限策略

create_policy + attach_policy_to_role(snake_case:policy_name、policy_document、policy_type、role_name)。

Allow Action 至少:alidns:*、oss:*、fc:*、yundun-cert:*(不是 cas:*,见坑 5)。

改已有策略:create_policy_version(CreatePolicyVersionRequest(policy_name, policy_document, set_as_default=True)); 版本上限 5,超了先 delete_policy_version 删最旧的非默认版。

直接跑 scripts/setup_oidc.py 可一步建完上述三者(AK 从环境变量读)。

二、workflow:手动 OIDC 兑换(不要依赖官方 action)

不要用 aliyun/configure-aliyun-credentials-action —— RAM 配置完全正确时它仍会报 AuthenticationFail.NoPermission / ImplicitDeny / sts:AssumeRole / PolicyType: AssumeRolePolicy,且难排查。 改为自己请求 JWT + AssumeRoleWithOIDC,并把 iss/aud/sub 打出来对照信任策略。

permissions: { contents: read, id-token: write }
steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-python@v5
    with: { python-version: "3.13" }
  - run: pip install alibabacloud_sts20150401 alibabacloud_alidns20150109 \
             alibabacloud_cas20200407 alibabacloud_fc_open20210406 oss2 acme cryptography
  - name: OIDC -> STS 临时凭证
    env:
      # ⚠️ 下面两行的美元符号与左花括号之间「故意留了一个空格」,是为规避上传通道的 WAF
      #    (它会把「美元符号 + 双花括号」连写当成模板注入特征拦截)。
      #    复制到 workflow 时请删掉那个空格,恢复标准表达式写法。
      ALI_ROLE_ARN: $ {{ secrets.ALI_ROLE_ARN }}
      ALI_OIDC_ARN: $ {{ secrets.ALI_OIDC_ARN }}
      AUDIENCE: https://github.com//
    run: python scripts/oidc_sts_env.py     # 本 skill 自带,等价于下面的内联版

内联等价实现(要点全在注释里):

# 1) 请求 OIDC JWT:URL 已带 ?job_id=...,必须「追加」&audience=
url = os.environ['ACTIONS_ID_TOKEN_REQUEST_URL'] + '&audience=' + os.environ['AUDIENCE']
req = urllib.request.Request(url, headers={
    'Authorization': 'Bearer ' + os.environ['ACTIONS_ID_TOKEN_REQUEST_TOKEN']})
oidc = json.load(urllib.request.urlopen(req, timeout=30))['value']

# 2) 解码 payload 打出 RAM 实际看到的 claim(定位 ImplicitDeny 的神器)
p = oidc.split('.')[1]; p += '=' * (-len(p) % 4)
cl = json.loads(base64.urlsafe_b64decode(p))
print('OIDC claims ->', 'iss =', cl.get('iss'), 'aud =', cl.get('aud'), 'sub =', cl.get('sub'))

# 3) AssumeRoleWithOIDC(请求字段全 snake_case,且要重试吸收偶发 500)
sts = StsClient(openapi_models.Config(endpoint='sts.aliyuncs.com'))
r = sts.assume_role_with_oidc(sts_models.AssumeRoleWithOIDCRequest(
    oidcprovider_arn=os.environ['ALI_OIDC_ARN'], role_arn=os.environ['ALI_ROLE_ARN'],
    oidctoken=oidc, role_session_name='cert-renew'))
c = r.body.credentials      # access_key_id / access_key_secret / security_token / expiration
with open(os.environ['GITHUB_ENV'], 'a') as f:
    f.write('ALIBABA_CLOUD_ACCESS_KEY_ID=%s\n' % c.access_key_id)
    f.write('ALIBABA_CLOUD_ACCESS_KEY_SECRET=%s\n' % c.access_key_secret)
    f.write('ALIBABA_CLOUD_SECURITY_TOKEN=%s\n' % c.security_token)

三、cert_manager.py 的三条硬要求

STOKEN = os.environ.get('ALIBABA_CLOUD_SECURITY_TOKEN', '')

# 1) oss2 必须按有无 token 选 Auth 实现(坑 6)
auth = oss2.StsAuth(AK, SK, STOKEN) if STOKEN else oss2.Auth(AK, SK)

# 2) 阿里云 SDK Config 透传 token
cfg.security_token = STOKEN          # 长期 AK 场景为空,可忽略

# 3) 签发与分发解耦 + 指纹守卫(坑 7、8)
need_issue = DO('cert') and (left <= THRESHOLD or FORCE)   # 证书有效只跳过签发,仍跑分发
cert_fp = hashlib.sha256(pem).hexdigest()
if cert_fp != state.get('cert_fp'):                        # 指纹变了才重传 CAS
    old = state.get('cert_id')
    if old:
        cas.delete_user_certificate(DeleteUserCertificateRequest(cert_id=old))  # 注意是 cert_id
    cert_id = cas.upload_user_certificate(UploadUserCertificateRequest(
        name=CAS_NAME, cert=pem, key=privkey_pem)).body.cert_id

四、状态持久化(不做就会撞 LE 限额)

runner 是临时容器,account.key / privkey.pem / state.json 必须存外部。 用 OSS 私有桶:跑前 get_object_to_file、跑后 put_object_from_file,桶不存在自动建(建桶写法见坑 9)。 否则每次运行都当首次签发,撞 Let's Encrypt「每域名每周 5 张重复证书」限额。

五、keepalive:别让 GitHub 停掉 schedule

GitHub 对连续 60 天无活动的仓库会禁用 schedule,且 workflow 自己跑不算活动。 专用仓加 keepalive.yml,每月 1 日用 GITHUB_TOKEN(permissions: contents: write)改一个时间戳文件并 push:

on:
  schedule: [{ cron: "0 3 1 * *" }]
  workflow_dispatch:
permissions: { contents: write }

六、新子域「无感」接入(DNS 自动发现)

  • 证书侧本来就无感:签的是通配 example.com + *.example.com,*. 覆盖任意一级子域, 新建子域无需重新签发、不消耗配额。注意 *. 不覆盖多级子域(a.b.example.com)。
  • 真正的手工成本在「绑定」。加 discover_targets() 扫根域 CNAME(分页 100/页):
    • .oss-.aliyuncs.com → 正则反解 bucket + region,自动生成 OSS target
    • ..fcapp.run → FC target(service/function 无法从 DNS 推导,取 FC_TARGETS[0] 兜底)
    • 非阿里云 CNAME(Vercel / 其他 CDN)与 _ 开头记录(_dnsauth / _acme-challenge)跳过
  • merge_targets(manual, auto):手工优先,自动发现按 domain 去重追加;分发循环遍历合并结果; 提供 --no-discover 退回纯手工模式。 ⚠️ 去重集合里必须放 domain 字符串,不能放 target 字典本身:
    seen = {t['domain'] for t in manual}
    for t in auto:
        if t['domain'] not in seen:
            out.append(t)
            seen.add(t['domain'])     # 写成 seen.add(t) -> TypeError: unhashable type: 'dict'
    
    写成 seen.add(t) 在生产上真炸过:整个分发 stage 抛异常,续期任务失败。
  • 效果:新建子域只需加一条 CNAME,下次定时任务自动写 _dnsauth TXT 验归属 + 绑 cert_id + 上 HTTPS。

七、必坑清单(按踩中概率排序)

  1. 迁移仓库 / 改 audience 必须三处同步,否则 AuthenticationFail.OIDCToken.AudienceNotMatch / Invalid audience.: ① workflow 的 audience;② 角色信任策略的 oidc:aud(update_role); ③ OIDC 提供商的 ClientIds(IMS add_client_id_to_oidcprovider / remove_client_id_from_oidcprovider)。 只改前两处是典型漏网。

  2. 官方 action 莫名 ImplicitDeny:aliyun/configure-aliyun-credentials-action 在 RAM 全对时仍报 NoPermission / ImplicitDeny / PolicyType: AssumeRolePolicy。改用第二节的手动兑换。 get_role 返回的 Principal.Federated 是数组,那是 RAM 规范化结果,不是 bug,别去改它。

  3. oidc:sub 别用 StringEquals 写死 repo:/:*:GitHub 在 fork / 重跑 / 某些事件下会把 repo ID 与 owner ID 嵌进 sub,变成 repo:oracis@4110185/myrepo@1368066874:ref:refs/heads/main。 用 StringLike + repo:*/*:ref:refs/heads/main 一套覆盖两种格式。 打印 JWT 的 sub 是定位此类问题的唯一快路径。

  4. STS 偶发 InternalError 500:凭证指纹核对无误仍 500 时属瞬时故障,AssumeRoleWithOIDC 加 3~5 次指数退避重试即可。

  5. CAS 的 RAM 命名空间是 yundun-cert,不是 cas:策略写 cas:* 覆盖不到 UploadUserCertificate,报 NoPermission / 403 / yundun-cert:UploadUserCertificate / AccountLevelIdentityBasedPolicy。 必须加 yundun-cert:*。

  6. oss2 用 STS 必须 oss2.StsAuth,不能把 token 当 oss2.Auth 第 3 个参数: oss2.Auth.__init__(access_key_id, access_key_secret) 只收 2 个参数,传第 3 个直接 TypeError: Auth.__init__() takes 3 positional arguments but 4 were given。 正确:oss2.StsAuth(ak, sk, stoken) if stoken else oss2.Auth(ak, sk)。

  7. CAS 删除参数是 cert_id,不是 certificate_id;且 SDK 查不到上传型证书: 写成 certificate_id= 会静默不删(被 except 吞掉),旧证书长期占用固定名称, 下次上传必报 NameRepeat (code:400, 名称重复)。cas20200407 只有 list_user_certificate_order(仅购买订单),没有 DescribeUserCertificateList, 所以 state.json 里的 cert_id 是定位旧证书的唯一途径。 稳健写法:上传前按 state 的 cert_id 删除(失败仅告警);若仍 NameRepeat,用「固定名+时间戳」重传兜底。

  8. 签发与分发必须解耦,证书有效不能整体 sys.exit(0):否则上次分发失败后, 有效证书会永远卡在「已签发未分发」。用 need_issue 只控制签发,CAS 上传加 sha256(pem) 指纹守卫, 指纹一致就复用旧 cert_id,避免每天重复创建 CAS 证书。

  9. oss2 create_bucket 第一个位置参数是 permission(ACL 字符串),不是 input(config 对象): 签名 create_bucket(self, permission=None, input=None, headers=None)。 写 create_bucket(oss2.models.BucketCreateConfig(oss2.BUCKET_ACL_PRIVATE)) 会把 config 塞进 x-oss-acl header,签名时 TypeError: can't concat BucketCreateConfig to bytes。 正确:b.create_bucket(oss2.BUCKET_ACL_PRIVATE)(默认即私有桶)。 BucketCreateConfig 构造器也按 snake_case:storage_class / data_redundancy_type / acl。

  10. cron 时区是 UTC:北京 03:30 = 30 19 * * *。schedule 有 ±15 分钟漂移,对续期无影响。 wait_txt() 等 DNS 轮询在 Linux runner 用 dig(需 dnsutils),Windows 无 dig 回退 nslookup。

  11. 定时跑全绿 ≠ 管线健康:自动发现这类"只有出现新目标才会执行"的分支,平时根本走不到, 于是 bug 能在每天 success 的 run 里潜伏几个月。真实案例:seen.add(t)(见第六节) 在新增第 3 个子域的当天才第一次崩(此前定时跑连续 5 天 success)。 任何一次改动域名/CNAME/目标清单后,都要手动 workflow_dispatch 跑一次, 别只看最近几次定时 run 的绿勾。日志里出现 证书仍有效(剩 N 天),无需重新签发 是正常路径,不是失败。

  12. 判断"这条流水线到底存不存在"时,必须连私有仓库一起扫:只列 public 仓库会漏掉 oracis/cert-renew 这类私有基础设施仓库,从而误判"自动续期没搭"。 用 gh repo list --private --limit 200 或 API /user/repos?visibility=private。 另:本机 gh 常处于未登录状态(gh auth status → not logged in), 可直接从 ~/.git-credentials 取 token: TOKEN=$(sed -n 's#^https://\([^:]*\):\([^@]*\)@github.com#\2#p' ~/.git-credentials), 然后 curl -H "Authorization: Bearer $TOKEN" 调 API(记得先 unset 本机代理变量,见下)。

八、验收清单

跑一次 workflow,日志应依次出现:

OIDC claims -> iss=... aud=... sub=repo:/:ref:refs/heads/main
STS 临时凭证已获取
新证书已签发,有效期至 ...          # 未到期时是「证书仍有效(剩 N 天),无需重新签发」,正常
已上传 CAS,cert_id = ...            # 指纹未变时是「证书未变化(fp 一致),复用 cert_id=...」
DNS 自动发现 OSS 目标 N 个: ...      # 新子域必须出现在这里
处理 OSS  -> 已绑定自定义域名 # 自动发现的会带「(自动发现)」后缀
FC 自定义域名  证书已更新
全部完成 / RENEW_OK

再去浏览器核对站点的证书颁发者(Let's Encrypt R11/R12)与到期时间即可。 验证 HTTPS 时不要用 HEAD:OSS 静态托管只重写 GET,HEAD 会返回 403, 浏览器和用户实际走 GET,属于误报。

附:脚本

  • scripts/setup_oidc.py —— 一次性建 OIDC 提供商 + 角色 + 权限策略(读环境变量里的永久 AK)。
  • scripts/oidc_sts_env.py —— workflow 用:OIDC JWT → AssumeRoleWithOIDC → 写 $GITHUB_ENV。

源码: 许可:MIT

Related skills

Stores durable facts in a categorized, plain-markdown vault on disk, alongside your agent's built-in memory.

by Iván559 installs18 stars

Join a video meeting as an AI bot with voice, avatar, and screenshare across four operating modes.

by johnpatternai22 installs8 stars

Generate and edit Draw.io, Mermaid, and Excalidraw diagrams from natural language using a structured JSON spec.

by nssa.io1.0k installs47 stars

Run Git operations — commits, branches, merges, rebases, conflict resolution, and recovery — with safety rules enforced.

by Iván535 installs31 stars

Fetch raw ad creative, app, ranking, and revenue data from AdMapix as structured JSON.

by fly0pants4.3k installs296 stars

More from oracis

Browse all skills

把本地 Skill 打包并发布到 SkillHub(腾讯 skillhub.cn)与 ClawHub,覆盖预检、发布、状态回读与竞品对标全链路,并在上传被拒时用二分/ddmin 脚本把「服务端 WAF 拦内容(566)」与「包结构/字段问题」精确区分开。当用户说「发布技能到市场」「上架 skill」「SkillHub 提交失败」「Failed to fetch」「566」「上传 zip 报错」「技能审核状态」「版本号被拒」时使用。也讲清「入库成功 ≠ 已公开」——用 `mine` 的 `reviewStatus`、重发探测 `409 VERSION_EXISTS`、`/versions/<v>/signature` 三个探针判断版本到底走到哪一步。

by oracis1 installs1 stars

平台只有网页后台、没有上传 API 时,用本机已装的 Chrome/Edge + CDP 自动化走完上传/发布/填表流程——复用真实登录态、零安装(不下载 500MB Chromium)、能给文件选择框塞文件;并实测富文本编辑器(公众号 / CMS / 邮件模板)里到底哪些 CSS 能活下来,以及在决定是否接入平台自带的「一键排版 / AI 美化」前先抓它输出做结构化 diff。当用户说「只能网页上传」「后台发布」「网页填表自动提交」「发布到技能市场/各平台控制台」「复用已登录的浏览器」「公众号排版样式不生效/保存后样式没了」「一键排版要不要用」「点了按钮没反应/提交没成功也没报错」时使用。

by oracis2 installs

一键安装并实测验证已验证可用的免费 LLM 到 WorkBuddy/Claude Code,无需注册、无需 API Key。当用户说「装免费模型」「加免费模型」「免费 LLM」「免费模型一键装」「验证免费模型可用吗」「免费模型怎么接入」「帮我配几个免费模型」「监控免费模型」「免费模型挂了吗」「kilo 网关」「kilo-auto」「nemotron 免费」「space-bunny」「智谱 glm 免费」「zhipu 免费」「glm-4-flash」「glm-4.7-flash」「glm-z1-flash」「glm-4v-flash」「探测国外免费模型」「国外平台免费 llm」「优先用免费模型」「读图桥接」「vision bridge」「给 glm-4-flash 加读图」「不读图的模型怎么看图」「阶跃星辰」「stepfun」「step 5」「step-5-preview」「限时免费模型」「ccproxy 读图转发」「给 claude code 加读图」时使用。也用于验证已有免费模型配置是否真的可用(tool calling 是否支持、是否限流、是否支持读图),以及定期 watch 监控免费模型是否下线/限流/失去工具调用能力。

by oracis1 installs

在中国大陆网络环境下用 yt-dlp 下载视频(含 Windows GUI 封装)。当用户要下载 YouTube、Bilibili、SoundCloud 等 yt-dlp 支持的站点内容,或遇到「yt-dlp 装不上 / 下载报 502 / Tunnel connection failed / ffmpeg not found / 缺 JS runtime / TLS fingerprint / watch?v=&list= 把整个列表都下了 / Sign in to confirm you're not a bot / The page needs to be reloaded / 下到一半突然下不动 / 下回来的视频糊、有拖影残影 / 下载速度特别慢」等问题时使用。

by oracis2 installs

聚合搜索百度网盘/夸克网盘/阿里云盘等网盘分享链接。当用户想按作者名、课程名、剧名、书名、软件名找网盘下载路径,或提到「网盘搜索」「PanSou」「盘搜」「找到下载链接」「夸克/百度/阿里网盘资源」时使用。包含已验证的 PanSou 协议端点、绕过系统代理的必要写法、以及可直接运行的本地 Web 应用 panradar.py。

by oracis2 installs

在 WorkBuddy(Windows)环境里交付零依赖本地 Web 应用,并绕开官方测试技能完全不覆盖的本机专属坑。当用户要在本机跑起来一个本地 Web 应用 / 单文件工具 / 案例库 / 看板 / 小工具,或要验证刚写完的 HTML/JS/Python 交付物真的能工作时使用。独家覆盖:Bash 缺 coreutils、bash 是 WSL 启动桩、rm 是坏 shim、PowerShell 不回显 stdout、cwd 漂移、预览面板靠 HEAD 探活——这些在干净 Linux 环境里遇不到。

by oracis1 installs