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.
- certbot_dns_alias-0.1.0/.gitignore +14 -0
- certbot_dns_alias-0.1.0/LICENSE +21 -0
- certbot_dns_alias-0.1.0/PKG-INFO +283 -0
- certbot_dns_alias-0.1.0/README.md +252 -0
- certbot_dns_alias-0.1.0/certbot_dns_alias/__init__.py +1 -0
- certbot_dns_alias-0.1.0/certbot_dns_alias/config.py +95 -0
- certbot_dns_alias-0.1.0/certbot_dns_alias/dns.py +115 -0
- certbot_dns_alias-0.1.0/certbot_dns_alias/dns_alias.py +169 -0
- certbot_dns_alias-0.1.0/certbot_dns_alias/providers/__init__.py +1 -0
- certbot_dns_alias-0.1.0/certbot_dns_alias/providers/aliyun.py +127 -0
- certbot_dns_alias-0.1.0/certbot_dns_alias/providers/base.py +81 -0
- certbot_dns_alias-0.1.0/certbot_dns_alias/providers/tencent.py +106 -0
- certbot_dns_alias-0.1.0/examples/aliyun.ini +9 -0
- certbot_dns_alias-0.1.0/examples/auto.ini +8 -0
- certbot_dns_alias-0.1.0/examples/tencent.ini +8 -0
- certbot_dns_alias-0.1.0/pyproject.toml +65 -0
- certbot_dns_alias-0.1.0/tests/conftest.py +37 -0
- certbot_dns_alias-0.1.0/tests/test_authenticator.py +100 -0
- certbot_dns_alias-0.1.0/tests/test_config_and_routing.py +161 -0
- certbot_dns_alias-0.1.0/tests/test_dns.py +160 -0
- certbot_dns_alias-0.1.0/tests/test_providers.py +294 -0
- certbot_dns_alias-0.1.0/uv.lock +2147 -0
|
@@ -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)
|