certbot-dns-alias 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .coverage
7
+ htmlcov/
8
+ coverage.xml
9
+ dist/
10
+ build/
11
+ *.egg-info/
12
+ .DS_Store
13
+ credentials.ini
14
+ *.secret.ini
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TiyeeJiang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,283 @@
1
+ Metadata-Version: 2.4
2
+ Name: certbot-dns-alias
3
+ Version: 0.1.0
4
+ Summary: Certbot DNS-01 plugin with CNAME delegation for Alibaba Cloud DNS and Tencent Cloud DNSPod
5
+ Project-URL: Homepage, https://github.com/tiyee/certbot-dns-alias
6
+ Project-URL: Repository, https://github.com/tiyee/certbot-dns-alias
7
+ Project-URL: Issues, https://github.com/tiyee/certbot-dns-alias/issues
8
+ Author-email: tiyee <tiyee@live.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: acme,aliyun,certbot,cname,dns-01,dnspod,tencent
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Plugins
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Security
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: alibabacloud-alidns20150109<5,>=3.0
25
+ Requires-Dist: alibabacloud-tea-openapi<1,>=0.3
26
+ Requires-Dist: alibabacloud-tea-util<1,>=0.3
27
+ Requires-Dist: certbot<6,>=3.0
28
+ Requires-Dist: dnspython<3,>=2.6
29
+ Requires-Dist: tencentcloud-sdk-python-dnspod<4,>=3.1.130
30
+ Description-Content-Type: text/markdown
31
+
32
+ # certbot-dns-alias
33
+
34
+ Certbot DNS-01 插件,通过 CNAME 委托在阿里云 DNS 或腾讯云 DNSPod 管理 TXT 验证记录。
35
+
36
+ Certbot DNS-01 authentication with CNAME delegation, supporting Alibaba Cloud DNS and Tencent Cloud DNSPod.
37
+
38
+ ## 工作方式
39
+
40
+ 在业务域名的 DNS 中预先创建 CNAME:
41
+
42
+ ```dns
43
+ _acme-challenge.example.com. 300 IN CNAME example-com.delegate.example.net.
44
+ ```
45
+
46
+ `delegate.example.net` 托管在阿里云或腾讯云。插件自动跟随 CNAME 链,在最终目标
47
+ `example-com.delegate.example.net` 添加本次挑战的 TXT 值,等待 DNS 传播,完成后按记录 ID 清理。
48
+ 业务域名可以由任意 DNS 服务商托管;插件只需要目标托管区域的 API 凭据。
49
+
50
+ - 支持多级 CNAME、泛域名、一个证书包含多个域名。
51
+ - 支持阿里云、腾讯云单独使用,或 `auto` 模式在一次申请中同时使用两者。
52
+ - 根据托管区域列表做最长 DNS 后缀匹配,支持 `example.co.uk` 和独立托管的子域,匹配包含标签边界。
53
+ - 区域列表及 TXT 查询支持 API 分页;可以显式配置区域以跳过自动枚举。
54
+ - 每个 TXT 值单独创建,保留同名记录的其他值;复用已有的相同有效 TXT 时不删除原记录。
55
+ - 清理使用创建时保存的目标区域和记录 ID;CNAME 发生变化也不会改删其他区域。
56
+ - CNAME 环路、超深链、DNS 超时、权限错误和区域归属冲突会产生明确错误。
57
+
58
+ Python **3.10+**,Certbot **3.x–5.x**。两个云服务商 SDK 均随插件安装。
59
+
60
+ ## 安装
61
+
62
+ ### 本地开发(uv)
63
+
64
+ ```bash
65
+ uv sync --locked
66
+ uv run certbot plugins --text
67
+ uv run certbot --help dns-alias
68
+ ```
69
+
70
+ `uv.lock` 已纳入版本管理,`uv sync` 会创建 `.venv` 并以 editable 模式安装插件。
71
+
72
+ ### 从 PyPI 安装(本项目发布后)
73
+
74
+ 使用 uv 安装 Certbot 和插件到同一个工具环境:
75
+
76
+ ```bash
77
+ uv tool install --with certbot-dns-alias certbot
78
+ certbot plugins --text
79
+ ```
80
+
81
+ 或者在安装 Certbot 的 Python 虚拟环境中执行:
82
+
83
+ ```bash
84
+ python -m pip install certbot-dns-alias
85
+ ```
86
+
87
+ 插件与 Certbot 必须处于同一 Python 环境。如果已有 snap/docker 版 Certbot,需在该运行环境中
88
+ 安装插件,或者改用上述 uv 工具环境。
89
+
90
+ ## 凭据配置
91
+
92
+ 使用不带 INI section 的 `key = value` 格式。完整示例见 [examples](examples)。
93
+ 复制示例后填入密钥,并设置权限:
94
+
95
+ ```bash
96
+ cp examples/tencent.ini credentials.ini
97
+ chmod 600 credentials.ini
98
+ ```
99
+
100
+ ### 阿里云
101
+
102
+ ```ini
103
+ dns_alias_provider = aliyun
104
+ dns_alias_aliyun_access_key_id = YOUR_ACCESS_KEY_ID
105
+ dns_alias_aliyun_access_key_secret = YOUR_ACCESS_KEY_SECRET
106
+ dns_alias_aliyun_zones = delegate.example.net
107
+ ```
108
+
109
+ 可选项:`dns_alias_aliyun_region_id`(默认 `cn-hangzhou`)、
110
+ `dns_alias_aliyun_security_token`(临时 STS 凭据)。固定使用公共端点 `alidns.aliyuncs.com`。
111
+
112
+ ### 腾讯云 DNSPod
113
+
114
+ ```ini
115
+ dns_alias_provider = tencent
116
+ dns_alias_tencent_secret_id = YOUR_SECRET_ID
117
+ dns_alias_tencent_secret_key = YOUR_SECRET_KEY
118
+ dns_alias_tencent_zones = delegate.example.net
119
+ ```
120
+
121
+ 可选项:`dns_alias_tencent_token`(临时会话凭据)。使用腾讯云 API v20210323 和
122
+ `dnspod.tencentcloudapi.com`,不使用旧版 DNSPod Token API 或国际版端点。
123
+
124
+ ### 同时使用两家云服务商
125
+
126
+ ```ini
127
+ dns_alias_provider = auto
128
+ dns_alias_aliyun_access_key_id = YOUR_ACCESS_KEY_ID
129
+ dns_alias_aliyun_access_key_secret = YOUR_ACCESS_KEY_SECRET
130
+ dns_alias_aliyun_zones = ali-delegate.example.net
131
+ dns_alias_tencent_secret_id = YOUR_SECRET_ID
132
+ dns_alias_tencent_secret_key = YOUR_SECRET_KEY
133
+ dns_alias_tencent_zones = tencent-delegate.example.org
134
+ ```
135
+
136
+ 例如:
137
+
138
+ ```dns
139
+ _acme-challenge.example.com. 300 IN CNAME example-com.ali-delegate.example.net.
140
+ _acme-challenge.api.example.org. 300 IN CNAME api-example-org.tencent-delegate.example.org.
141
+ ```
142
+
143
+ 随后在同一命令中传入 `-d example.com -d api.example.org` 即可。`auto` 至少需要一组完整密钥,
144
+ 也可以只配置一家。如果同一最长匹配区域同时属于两个服务商,插件会报错;通过调整显式区域列表
145
+ 或选择单一 `provider` 消除歧义。
146
+
147
+ 所有 `*_zones` 均为可选项,多个区域以逗号分隔,例如 `example.net, example.co.uk`。
148
+ 配置后只允许这些区域,不再调用对应的域名枚举 API;填写云平台实际托管区域名称,
149
+ 不要填写完整 TXT 主机名。未配置时自动枚举当前凭据可见的区域。
150
+ 每家服务商目前支持一个凭据账号。
151
+
152
+ ## 申请和续期
153
+
154
+ 先创建 CNAME,并确保公共 DNS 可以解析;第一次申请可以使用测试环境:
155
+
156
+ ```bash
157
+ uv run certbot certonly \
158
+ --authenticator dns-alias \
159
+ --dns-alias-credentials "$(pwd)/credentials.ini" \
160
+ --dns-alias-require-cname \
161
+ --dns-alias-propagation-seconds 120 \
162
+ --staging \
163
+ --non-interactive --agree-tos --email admin@example.com \
164
+ -d example.com -d '*.example.com'
165
+ ```
166
+
167
+ 测试通过后去掉 `--staging` 申请正式证书。域名和邮箱应替换为自己的值。
168
+ 如果使用 uv 工具安装,命令前缀用 `certbot`。
169
+ Certbot 默认写入 `/etc/letsencrypt`、`/var/lib/letsencrypt` 和 `/var/log/letsencrypt`,
170
+ 运行账号需要相应权限;也可使用 `--config-dir`、`--work-dir` 和 `--logs-dir` 指定目录。
171
+
172
+ Certbot 保存认证器和凭据文件的绝对路径,后续可使用同一环境续期:
173
+
174
+ ```bash
175
+ uv run certbot renew --dry-run
176
+ uv run certbot renew
177
+ ```
178
+
179
+ 凭据文件需长期保留,临时凭据过期前需更新。按部署方式配置定时续期及证书部署 hook。
180
+
181
+ ### 可配置参数
182
+
183
+ | 参数 | 默认值 | 作用 |
184
+ | --- | --- | --- |
185
+ | `--dns-alias-credentials` | 必填 | 凭据 INI 路径 |
186
+ | `--dns-alias-propagation-seconds` | `60` | 全部 TXT 添加后,统一等待的传播时间 |
187
+ | `--dns-alias-ttl` | `600` | 创建 TXT 时的 TTL,需满足云套餐限制 |
188
+ | `--dns-alias-cname-max-depth` | `8` | 允许的最大 CNAME 链接数 |
189
+ | `--dns-alias-dns-timeout` | `10` | 每次 CNAME 查询的总超时,秒 |
190
+ | `--dns-alias-dns-retries` | `2` | 超时或无可用 nameserver 时额外重试次数 |
191
+ | `--dns-alias-resolvers` | 系统 DNS | 逗号分隔的 DNS 服务器 IPv4/IPv6 地址 |
192
+ | `--dns-alias-require-cname` | 关闭 | 原始挑战名称没有 CNAME 时拒绝写入 |
193
+
194
+ 没有 CNAME 时,默认允许直接在原始挑战名称所属托管区域创建 TXT。只允许委托验证时启用
195
+ `--dns-alias-require-cname`。解析器使用绝对 DNS 名称,禁止系统 search suffix 扩展。
196
+ NXDOMAIN / 无 CNAME 表示链终点;超时、SERVFAIL 等解析失败不会当作链终点。
197
+
198
+ TTL 与传播等待时间不同。第一次创建目标主机可能受 DNS 负缓存影响,必要时提高传播等待时间。
199
+ 不同业务域名应使用不同委托主机名;普通域名与其泛域名会共用 `_acme-challenge` 名称,
200
+ 插件会保留本次申请需要的多个 TXT 值。插件不会更新整个 TXT RRset。
201
+
202
+ ## API 权限
203
+
204
+ 阿里云需要:
205
+
206
+ - `alidns:DescribeDomainRecords`
207
+ - `alidns:AddDomainRecord`
208
+ - `alidns:DeleteDomainRecord`
209
+ - 未配置 `dns_alias_aliyun_zones` 时,还需要 `alidns:DescribeDomains`
210
+
211
+ 腾讯云需要:
212
+
213
+ - `dnspod:DescribeRecordList`
214
+ - `dnspod:CreateRecord`
215
+ - `dnspod:DeleteRecord`
216
+ - 未配置 `dns_alias_tencent_zones` 时,还需要 `dnspod:DescribeDomainList`
217
+
218
+ 可根据云平台支持的资源范围将权限限制在委托区域。API 字段与权限参考
219
+ [阿里云 AddDomainRecord](https://www.alibabacloud.com/help/en/dns/api-alidns-2015-01-09-adddomainrecord)、
220
+ [阿里云 DescribeDomains](https://www.alibabacloud.com/help/en/dns/api-alidns-2015-01-09-describedomains) 和
221
+ [腾讯云 DescribeRecordList](https://cloud.tencent.com/document/api/1427/56166)。
222
+
223
+ ## 测试和构建
224
+
225
+ ```bash
226
+ uv sync --locked
227
+ uv run ruff check .
228
+ uv run ruff format --check .
229
+ uv run pytest --cov=certbot_dns_alias --cov-report=term-missing
230
+ uv build
231
+ uv run twine check --strict dist/*
232
+ ```
233
+
234
+ 测试通过 mock 官方 SDK 和 DNS 响应运行,不需要真实 API 密钥,不会写入云端 DNS。
235
+ 包含 Certbot 公共挑战生命周期测试、CNAME 链 / 环路 / 超时 / NXDOMAIN、区域匹配、
236
+ 两个 SDK 的真实请求模型和分页、已有 TXT 保留、多挑战共享与清理失败处理。
237
+ CI 配置 Python 3.10–3.14,以及 Certbot 3.0 兼容性验证。
238
+ Certbot 3.0 的旧 ACME/josepy 依赖需要 `pyOpenSSL<25`;兼容性测试使用这一旧版本依赖组合。
239
+ 新安装默认由 uv 选择最新可用的 Certbot 版本。
240
+
241
+ 目录结构:
242
+
243
+ ```text
244
+ certbot_dns_alias/
245
+ dns_alias.py # Certbot Authenticator、TXT 所有权和清理状态
246
+ dns.py # CNAME 解析、DNS 名称和相对主机记录
247
+ config.py # INI 校验和服务商构建
248
+ providers/
249
+ base.py # 服务商接口及区域路由
250
+ aliyun.py # 阿里云 OpenAPI SDK
251
+ tencent.py # 腾讯云 DNSPod SDK
252
+ examples/ # 三种模式的凭据示例
253
+ tests/ # 无网络单元和生命周期测试
254
+ ```
255
+
256
+ 创建和删除 API 不自动重试写请求,避免响应丢失后的重复创建。清理失败会记录警告并继续清理其他挑战。
257
+ 创建状态保存在当前 Certbot 进程内;进程被强制终止、创建成功但响应丢失或删除失败时,可能留有 TXT,
258
+ 需要在委托区域手动清理。插件不持久化密钥或挑战值,也不会通过重新解析 CNAME 来猜测待删除记录。
259
+
260
+ ## 发布到 PyPI
261
+
262
+ 包名为 `certbot-dns-alias`,Certbot 入口点为 `dns-alias`。
263
+
264
+ 手动发布前,在 `pyproject.toml` 更新版本,再运行 `uv lock`、测试和构建。可先上传 TestPyPI:
265
+
266
+ ```bash
267
+ uv build
268
+ uv publish --publish-url https://test.pypi.org/legacy/ dist/*
269
+ # 正式发布
270
+ uv publish dist/*
271
+ ```
272
+
273
+ 上传时配置对应的 `UV_PUBLISH_TOKEN`。发布新版本前清空旧 `dist` 产物,以免上传旧版本。
274
+
275
+ 仓库包含 `.github/workflows/publish.yml`,GitHub Release 发布时会验证标签与项目版本一致
276
+ (例如版本 `0.1.0` 对应 `v0.1.0`),运行测试、构建 wheel/sdist 并通过 PyPI Trusted Publishing 上传。
277
+ 需先在 PyPI 为仓库 `tiyee/certbot-dns-alias` 配置 Trusted Publisher,工作流文件名 `publish.yml`,
278
+ environment 为 `pypi`;尚未创建 PyPI 项目时可以使用 pending publisher。
279
+ GitHub 仓库中创建同名 environment,可按需要设置发布审核。预发布 Release 只构建,不上传正式 PyPI。
280
+
281
+ ## License
282
+
283
+ MIT,见 [LICENSE](LICENSE)。
@@ -0,0 +1,252 @@
1
+ # certbot-dns-alias
2
+
3
+ Certbot DNS-01 插件,通过 CNAME 委托在阿里云 DNS 或腾讯云 DNSPod 管理 TXT 验证记录。
4
+
5
+ Certbot DNS-01 authentication with CNAME delegation, supporting Alibaba Cloud DNS and Tencent Cloud DNSPod.
6
+
7
+ ## 工作方式
8
+
9
+ 在业务域名的 DNS 中预先创建 CNAME:
10
+
11
+ ```dns
12
+ _acme-challenge.example.com. 300 IN CNAME example-com.delegate.example.net.
13
+ ```
14
+
15
+ `delegate.example.net` 托管在阿里云或腾讯云。插件自动跟随 CNAME 链,在最终目标
16
+ `example-com.delegate.example.net` 添加本次挑战的 TXT 值,等待 DNS 传播,完成后按记录 ID 清理。
17
+ 业务域名可以由任意 DNS 服务商托管;插件只需要目标托管区域的 API 凭据。
18
+
19
+ - 支持多级 CNAME、泛域名、一个证书包含多个域名。
20
+ - 支持阿里云、腾讯云单独使用,或 `auto` 模式在一次申请中同时使用两者。
21
+ - 根据托管区域列表做最长 DNS 后缀匹配,支持 `example.co.uk` 和独立托管的子域,匹配包含标签边界。
22
+ - 区域列表及 TXT 查询支持 API 分页;可以显式配置区域以跳过自动枚举。
23
+ - 每个 TXT 值单独创建,保留同名记录的其他值;复用已有的相同有效 TXT 时不删除原记录。
24
+ - 清理使用创建时保存的目标区域和记录 ID;CNAME 发生变化也不会改删其他区域。
25
+ - CNAME 环路、超深链、DNS 超时、权限错误和区域归属冲突会产生明确错误。
26
+
27
+ Python **3.10+**,Certbot **3.x–5.x**。两个云服务商 SDK 均随插件安装。
28
+
29
+ ## 安装
30
+
31
+ ### 本地开发(uv)
32
+
33
+ ```bash
34
+ uv sync --locked
35
+ uv run certbot plugins --text
36
+ uv run certbot --help dns-alias
37
+ ```
38
+
39
+ `uv.lock` 已纳入版本管理,`uv sync` 会创建 `.venv` 并以 editable 模式安装插件。
40
+
41
+ ### 从 PyPI 安装(本项目发布后)
42
+
43
+ 使用 uv 安装 Certbot 和插件到同一个工具环境:
44
+
45
+ ```bash
46
+ uv tool install --with certbot-dns-alias certbot
47
+ certbot plugins --text
48
+ ```
49
+
50
+ 或者在安装 Certbot 的 Python 虚拟环境中执行:
51
+
52
+ ```bash
53
+ python -m pip install certbot-dns-alias
54
+ ```
55
+
56
+ 插件与 Certbot 必须处于同一 Python 环境。如果已有 snap/docker 版 Certbot,需在该运行环境中
57
+ 安装插件,或者改用上述 uv 工具环境。
58
+
59
+ ## 凭据配置
60
+
61
+ 使用不带 INI section 的 `key = value` 格式。完整示例见 [examples](examples)。
62
+ 复制示例后填入密钥,并设置权限:
63
+
64
+ ```bash
65
+ cp examples/tencent.ini credentials.ini
66
+ chmod 600 credentials.ini
67
+ ```
68
+
69
+ ### 阿里云
70
+
71
+ ```ini
72
+ dns_alias_provider = aliyun
73
+ dns_alias_aliyun_access_key_id = YOUR_ACCESS_KEY_ID
74
+ dns_alias_aliyun_access_key_secret = YOUR_ACCESS_KEY_SECRET
75
+ dns_alias_aliyun_zones = delegate.example.net
76
+ ```
77
+
78
+ 可选项:`dns_alias_aliyun_region_id`(默认 `cn-hangzhou`)、
79
+ `dns_alias_aliyun_security_token`(临时 STS 凭据)。固定使用公共端点 `alidns.aliyuncs.com`。
80
+
81
+ ### 腾讯云 DNSPod
82
+
83
+ ```ini
84
+ dns_alias_provider = tencent
85
+ dns_alias_tencent_secret_id = YOUR_SECRET_ID
86
+ dns_alias_tencent_secret_key = YOUR_SECRET_KEY
87
+ dns_alias_tencent_zones = delegate.example.net
88
+ ```
89
+
90
+ 可选项:`dns_alias_tencent_token`(临时会话凭据)。使用腾讯云 API v20210323 和
91
+ `dnspod.tencentcloudapi.com`,不使用旧版 DNSPod Token API 或国际版端点。
92
+
93
+ ### 同时使用两家云服务商
94
+
95
+ ```ini
96
+ dns_alias_provider = auto
97
+ dns_alias_aliyun_access_key_id = YOUR_ACCESS_KEY_ID
98
+ dns_alias_aliyun_access_key_secret = YOUR_ACCESS_KEY_SECRET
99
+ dns_alias_aliyun_zones = ali-delegate.example.net
100
+ dns_alias_tencent_secret_id = YOUR_SECRET_ID
101
+ dns_alias_tencent_secret_key = YOUR_SECRET_KEY
102
+ dns_alias_tencent_zones = tencent-delegate.example.org
103
+ ```
104
+
105
+ 例如:
106
+
107
+ ```dns
108
+ _acme-challenge.example.com. 300 IN CNAME example-com.ali-delegate.example.net.
109
+ _acme-challenge.api.example.org. 300 IN CNAME api-example-org.tencent-delegate.example.org.
110
+ ```
111
+
112
+ 随后在同一命令中传入 `-d example.com -d api.example.org` 即可。`auto` 至少需要一组完整密钥,
113
+ 也可以只配置一家。如果同一最长匹配区域同时属于两个服务商,插件会报错;通过调整显式区域列表
114
+ 或选择单一 `provider` 消除歧义。
115
+
116
+ 所有 `*_zones` 均为可选项,多个区域以逗号分隔,例如 `example.net, example.co.uk`。
117
+ 配置后只允许这些区域,不再调用对应的域名枚举 API;填写云平台实际托管区域名称,
118
+ 不要填写完整 TXT 主机名。未配置时自动枚举当前凭据可见的区域。
119
+ 每家服务商目前支持一个凭据账号。
120
+
121
+ ## 申请和续期
122
+
123
+ 先创建 CNAME,并确保公共 DNS 可以解析;第一次申请可以使用测试环境:
124
+
125
+ ```bash
126
+ uv run certbot certonly \
127
+ --authenticator dns-alias \
128
+ --dns-alias-credentials "$(pwd)/credentials.ini" \
129
+ --dns-alias-require-cname \
130
+ --dns-alias-propagation-seconds 120 \
131
+ --staging \
132
+ --non-interactive --agree-tos --email admin@example.com \
133
+ -d example.com -d '*.example.com'
134
+ ```
135
+
136
+ 测试通过后去掉 `--staging` 申请正式证书。域名和邮箱应替换为自己的值。
137
+ 如果使用 uv 工具安装,命令前缀用 `certbot`。
138
+ Certbot 默认写入 `/etc/letsencrypt`、`/var/lib/letsencrypt` 和 `/var/log/letsencrypt`,
139
+ 运行账号需要相应权限;也可使用 `--config-dir`、`--work-dir` 和 `--logs-dir` 指定目录。
140
+
141
+ Certbot 保存认证器和凭据文件的绝对路径,后续可使用同一环境续期:
142
+
143
+ ```bash
144
+ uv run certbot renew --dry-run
145
+ uv run certbot renew
146
+ ```
147
+
148
+ 凭据文件需长期保留,临时凭据过期前需更新。按部署方式配置定时续期及证书部署 hook。
149
+
150
+ ### 可配置参数
151
+
152
+ | 参数 | 默认值 | 作用 |
153
+ | --- | --- | --- |
154
+ | `--dns-alias-credentials` | 必填 | 凭据 INI 路径 |
155
+ | `--dns-alias-propagation-seconds` | `60` | 全部 TXT 添加后,统一等待的传播时间 |
156
+ | `--dns-alias-ttl` | `600` | 创建 TXT 时的 TTL,需满足云套餐限制 |
157
+ | `--dns-alias-cname-max-depth` | `8` | 允许的最大 CNAME 链接数 |
158
+ | `--dns-alias-dns-timeout` | `10` | 每次 CNAME 查询的总超时,秒 |
159
+ | `--dns-alias-dns-retries` | `2` | 超时或无可用 nameserver 时额外重试次数 |
160
+ | `--dns-alias-resolvers` | 系统 DNS | 逗号分隔的 DNS 服务器 IPv4/IPv6 地址 |
161
+ | `--dns-alias-require-cname` | 关闭 | 原始挑战名称没有 CNAME 时拒绝写入 |
162
+
163
+ 没有 CNAME 时,默认允许直接在原始挑战名称所属托管区域创建 TXT。只允许委托验证时启用
164
+ `--dns-alias-require-cname`。解析器使用绝对 DNS 名称,禁止系统 search suffix 扩展。
165
+ NXDOMAIN / 无 CNAME 表示链终点;超时、SERVFAIL 等解析失败不会当作链终点。
166
+
167
+ TTL 与传播等待时间不同。第一次创建目标主机可能受 DNS 负缓存影响,必要时提高传播等待时间。
168
+ 不同业务域名应使用不同委托主机名;普通域名与其泛域名会共用 `_acme-challenge` 名称,
169
+ 插件会保留本次申请需要的多个 TXT 值。插件不会更新整个 TXT RRset。
170
+
171
+ ## API 权限
172
+
173
+ 阿里云需要:
174
+
175
+ - `alidns:DescribeDomainRecords`
176
+ - `alidns:AddDomainRecord`
177
+ - `alidns:DeleteDomainRecord`
178
+ - 未配置 `dns_alias_aliyun_zones` 时,还需要 `alidns:DescribeDomains`
179
+
180
+ 腾讯云需要:
181
+
182
+ - `dnspod:DescribeRecordList`
183
+ - `dnspod:CreateRecord`
184
+ - `dnspod:DeleteRecord`
185
+ - 未配置 `dns_alias_tencent_zones` 时,还需要 `dnspod:DescribeDomainList`
186
+
187
+ 可根据云平台支持的资源范围将权限限制在委托区域。API 字段与权限参考
188
+ [阿里云 AddDomainRecord](https://www.alibabacloud.com/help/en/dns/api-alidns-2015-01-09-adddomainrecord)、
189
+ [阿里云 DescribeDomains](https://www.alibabacloud.com/help/en/dns/api-alidns-2015-01-09-describedomains) 和
190
+ [腾讯云 DescribeRecordList](https://cloud.tencent.com/document/api/1427/56166)。
191
+
192
+ ## 测试和构建
193
+
194
+ ```bash
195
+ uv sync --locked
196
+ uv run ruff check .
197
+ uv run ruff format --check .
198
+ uv run pytest --cov=certbot_dns_alias --cov-report=term-missing
199
+ uv build
200
+ uv run twine check --strict dist/*
201
+ ```
202
+
203
+ 测试通过 mock 官方 SDK 和 DNS 响应运行,不需要真实 API 密钥,不会写入云端 DNS。
204
+ 包含 Certbot 公共挑战生命周期测试、CNAME 链 / 环路 / 超时 / NXDOMAIN、区域匹配、
205
+ 两个 SDK 的真实请求模型和分页、已有 TXT 保留、多挑战共享与清理失败处理。
206
+ CI 配置 Python 3.10–3.14,以及 Certbot 3.0 兼容性验证。
207
+ Certbot 3.0 的旧 ACME/josepy 依赖需要 `pyOpenSSL<25`;兼容性测试使用这一旧版本依赖组合。
208
+ 新安装默认由 uv 选择最新可用的 Certbot 版本。
209
+
210
+ 目录结构:
211
+
212
+ ```text
213
+ certbot_dns_alias/
214
+ dns_alias.py # Certbot Authenticator、TXT 所有权和清理状态
215
+ dns.py # CNAME 解析、DNS 名称和相对主机记录
216
+ config.py # INI 校验和服务商构建
217
+ providers/
218
+ base.py # 服务商接口及区域路由
219
+ aliyun.py # 阿里云 OpenAPI SDK
220
+ tencent.py # 腾讯云 DNSPod SDK
221
+ examples/ # 三种模式的凭据示例
222
+ tests/ # 无网络单元和生命周期测试
223
+ ```
224
+
225
+ 创建和删除 API 不自动重试写请求,避免响应丢失后的重复创建。清理失败会记录警告并继续清理其他挑战。
226
+ 创建状态保存在当前 Certbot 进程内;进程被强制终止、创建成功但响应丢失或删除失败时,可能留有 TXT,
227
+ 需要在委托区域手动清理。插件不持久化密钥或挑战值,也不会通过重新解析 CNAME 来猜测待删除记录。
228
+
229
+ ## 发布到 PyPI
230
+
231
+ 包名为 `certbot-dns-alias`,Certbot 入口点为 `dns-alias`。
232
+
233
+ 手动发布前,在 `pyproject.toml` 更新版本,再运行 `uv lock`、测试和构建。可先上传 TestPyPI:
234
+
235
+ ```bash
236
+ uv build
237
+ uv publish --publish-url https://test.pypi.org/legacy/ dist/*
238
+ # 正式发布
239
+ uv publish dist/*
240
+ ```
241
+
242
+ 上传时配置对应的 `UV_PUBLISH_TOKEN`。发布新版本前清空旧 `dist` 产物,以免上传旧版本。
243
+
244
+ 仓库包含 `.github/workflows/publish.yml`,GitHub Release 发布时会验证标签与项目版本一致
245
+ (例如版本 `0.1.0` 对应 `v0.1.0`),运行测试、构建 wheel/sdist 并通过 PyPI Trusted Publishing 上传。
246
+ 需先在 PyPI 为仓库 `tiyee/certbot-dns-alias` 配置 Trusted Publisher,工作流文件名 `publish.yml`,
247
+ environment 为 `pypi`;尚未创建 PyPI 项目时可以使用 pending publisher。
248
+ GitHub 仓库中创建同名 environment,可按需要设置发布审核。预发布 Release 只构建,不上传正式 PyPI。
249
+
250
+ ## License
251
+
252
+ MIT,见 [LICENSE](LICENSE)。
@@ -0,0 +1 @@
1
+ """CNAME delegation for Certbot's DNS-01 authenticator."""
@@ -0,0 +1,95 @@
1
+ """Validate Certbot INI credentials and construct provider accounts."""
2
+
3
+ from certbot import errors
4
+ from certbot.plugins.dns_common import CredentialsConfiguration
5
+
6
+ from certbot_dns_alias.dns import normalize_name
7
+ from certbot_dns_alias.providers.aliyun import AliyunDNSProvider
8
+ from certbot_dns_alias.providers.base import DNSProvider, ZoneRouter
9
+ from certbot_dns_alias.providers.tencent import TencentDNSProvider
10
+
11
+
12
+ def setting(credentials: CredentialsConfiguration, key: str, default: str = "") -> str:
13
+ value = credentials.conf(key)
14
+ if value is None:
15
+ return default
16
+ if not isinstance(value, str):
17
+ raise errors.PluginError(f"dns_alias_{key} must be a single value")
18
+ return value.strip()
19
+
20
+
21
+ def provider_names(credentials: CredentialsConfiguration) -> list[str]:
22
+ mode = setting(credentials, "provider").lower()
23
+ if mode not in {"aliyun", "tencent", "auto"}:
24
+ raise errors.PluginError("dns_alias_provider must be aliyun, tencent, or auto")
25
+ required = {
26
+ "aliyun": {
27
+ "aliyun_access_key_id": "Alibaba Cloud AccessKey ID",
28
+ "aliyun_access_key_secret": "Alibaba Cloud AccessKey secret",
29
+ },
30
+ "tencent": {
31
+ "tencent_secret_id": "Tencent Cloud SecretId",
32
+ "tencent_secret_key": "Tencent Cloud SecretKey",
33
+ },
34
+ }
35
+ names = (
36
+ [mode]
37
+ if mode != "auto"
38
+ else [
39
+ name
40
+ for name, keys in required.items()
41
+ if any(credentials.conf(key) is not None for key in keys)
42
+ ]
43
+ )
44
+ if not names:
45
+ raise errors.PluginError("dns_alias_provider=auto requires at least one provider's keys")
46
+ for name in names:
47
+ credentials.require(required[name])
48
+ for key in required[name]:
49
+ if not setting(credentials, key):
50
+ raise errors.PluginError(f"dns_alias_{key} must not be empty")
51
+ return names
52
+
53
+
54
+ def configured_zones(credentials: CredentialsConfiguration, provider: str) -> list[str] | None:
55
+ value = credentials.conf(f"{provider}_zones")
56
+ if value is None:
57
+ return None
58
+ # ConfigObj parses an unquoted comma-separated INI value as a list.
59
+ parts = value if isinstance(value, list) else value.split(",")
60
+ if not parts or any(not isinstance(part, str) or not part.strip() for part in parts):
61
+ raise errors.PluginError(f"dns_alias_{provider}_zones must contain DNS zone names")
62
+ return [normalize_name(part) for part in parts]
63
+
64
+
65
+ def validate_credentials(credentials: CredentialsConfiguration) -> None:
66
+ for name in provider_names(credentials):
67
+ configured_zones(credentials, name)
68
+ optional = (
69
+ ["aliyun_region_id", "aliyun_security_token"] if name == "aliyun" else ["tencent_token"]
70
+ )
71
+ for key in optional:
72
+ setting(credentials, key)
73
+
74
+
75
+ def build_router(credentials: CredentialsConfiguration) -> ZoneRouter:
76
+ providers: dict[str, DNSProvider] = {}
77
+ zones = {}
78
+ for name in provider_names(credentials):
79
+ explicit = configured_zones(credentials, name)
80
+ if explicit is not None:
81
+ zones[name] = explicit
82
+ if name == "aliyun":
83
+ providers[name] = AliyunDNSProvider(
84
+ setting(credentials, "aliyun_access_key_id"),
85
+ setting(credentials, "aliyun_access_key_secret"),
86
+ region_id=setting(credentials, "aliyun_region_id", "cn-hangzhou"),
87
+ security_token=setting(credentials, "aliyun_security_token") or None,
88
+ )
89
+ else:
90
+ providers[name] = TencentDNSProvider(
91
+ setting(credentials, "tencent_secret_id"),
92
+ setting(credentials, "tencent_secret_key"),
93
+ token=setting(credentials, "tencent_token") or None,
94
+ )
95
+ return ZoneRouter(providers, zones)