问题
我正在使用 letsencrypt certbot 的 DNS-01 质询,但它不会颁发深度超过一个子域级别的证书。
命名配置文件
# grep -A 3 ^key /etc/bind/named.conf.local
key "certbot." {
algorithm hmac-sha512;
secret "[REDACTED]";
};
# grep -A 2 example.tld /etc/bind/named.conf.local
zone "example.tld" {
type master;
file "/var/cache/bind/fdb.example.tld.signed";
allow-transfer { pub-ns-acl; };
update-policy {
grant certbot. name _acme-challenge.example.tld. txt;
};
};
单个子域名
我知道我的密钥配置正确,因为我可以为单个子域颁发证书,即使它是一个通配符:
# certbot certonly --dns-rfc2136 --dns-rfc2136-credentials /etc/letsencrypt/rfc2136/cred.ini --preferred-challenges=dns [email protected] --agree-tos -d *.example.tld -d example.tld --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator dns-rfc2136, Installer None
Cert not due for renewal, but simulating renewal for dry run
Renewing an existing certificate
IMPORTANT NOTES:
- The dry run was successful.
双子域名
这就是它不起作用的地方。
# certbot certonly --dns-rfc2136 --dns-rfc2136-credentials /etc/letsencrypt/rfc2136/cred.ini --preferred-challenges=dns [email protected] --agree-tos -d example.tld -d *.example.tld -d www.www.example.tld --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator dns-rfc2136, Installer None
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
You have an existing certificate that contains a portion of the domains you requested (ref: /etc/letsencrypt/renewal/example.tld.conf)
It contains these names: *.example.tld, example.tld
You requested these names for the new certificate: *.example.tld, example.tld, www.www.example.tld.
Do you want to expand and replace this existing certificate with the new certificate?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(E)xpand/(C)ancel: E
Renewing an existing certificate
Performing the following challenges:
dns-01 challenge for www.www.example.tld
Cleaning up challenges
Encountered exception during recovery:
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/certbot/auth_handler.py", line 75, in handle_authorizations
resp = self._solve_challenges(aauthzrs)
File "/usr/lib/python3/dist-packages/certbot/auth_handler.py", line 139, in _solve_challenges
resp = self.auth.perform(all_achalls)
File "/usr/lib/python3/dist-packages/certbot/plugins/dns_common.py", line 57, in perform
self._perform(domain, validation_domain_name, validation)
File "/usr/lib/python3/dist-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 76, in _perform
self._get_rfc2136_client().add_txt_record(validation_name, validation, self.ttl)
File "/usr/lib/python3/dist-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 135, in add_txt_record
.format(dns.rcode.to_text(rcode)))
certbot.errors.PluginError: Received response from server: REFUSED
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/certbot/error_handler.py", line 108, in _call_registered
self.funcs[-1]()
File "/usr/lib/python3/dist-packages/certbot/auth_handler.py", line 323, in _cleanup_challenges
self.auth.cleanup(achalls)
File "/usr/lib/python3/dist-packages/certbot/plugins/dns_common.py", line 76, in cleanup
self._cleanup(domain, validation_domain_name, validation)
File "/usr/lib/python3/dist-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 79, in _cleanup
self._get_rfc2136_client().del_txt_record(validation_name, validation)
File "/usr/lib/python3/dist-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 170, in del_txt_record
.format(dns.rcode.to_text(rcode)))
certbot.errors.PluginError: Received response from server: REFUSED
Received response from server: REFUSED
详细版本:
# certbot certonly --dns-rfc2136 --dns-rfc2136-credentials /etc/letsencrypt/rfc2136/cred.ini --preferred-challenges=dns [email protected] --agree-tos -d example.tld -d *.example.tld -d www.www.example.tld --dry-run -vvv
Root logging level set at -10
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requested authenticator dns-rfc2136 and installer None
Single candidate plugin: * dns-rfc2136
Description: Obtain certificates using a DNS TXT record (if you are using BIND for DNS).
Interfaces: IAuthenticator, IPlugin
Entry point: dns-rfc2136 = certbot_dns_rfc2136.dns_rfc2136:Authenticator
Initialized: <certbot_dns_rfc2136.dns_rfc2136.Authenticator object at 0x7fda4a6974e0>
Prep: True
Selected authenticator <certbot_dns_rfc2136.dns_rfc2136.Authenticator object at 0x7fda4a6974e0> and installer None
Plugins selected: Authenticator dns-rfc2136, Installer None
Picked account: <Account(RegistrationResource(body=Registration(key=None, contact=(), agreement=None, status=None, terms_of_service_agreed=None, only_return_existing=None, external_account_binding=None), uri='https://acme-staging-v02.api.letsencrypt.org/acme/acct/12742232', new_authzr_uri=None, terms_of_service=None), [REDACTED], Meta(creation_dt=datetime.datetime(2020, 3, 11, 1, 14, 11, tzinfo=<UTC>), creation_host='localhost'))>
Sending GET request to https://acme-staging-v02.api.letsencrypt.org/directory.
Starting new HTTPS connection (1): acme-staging-v02.api.letsencrypt.org:443
https://acme-staging-v02.api.letsencrypt.org:443 "GET /directory HTTP/1.1" 200 724
Received response:
HTTP 200
Server: nginx
Date: Fri, 21 Aug 2020 15:47:02 GMT
Content-Type: application/json
Content-Length: 724
Connection: keep-alive
Cache-Control: public, max-age=0, no-cache
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800
{
"cwHOlqiOgc0": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417",
"keyChange": "https://acme-staging-v02.api.letsencrypt.org/acme/key-change",
"meta": {
"caaIdentities": [
"letsencrypt.org"
],
"termsOfService": "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf",
"website": "https://letsencrypt.org/docs/staging-environment/"
},
"newAccount": "https://acme-staging-v02.api.letsencrypt.org/acme/new-acct",
"newNonce": "https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce",
"newOrder": "https://acme-staging-v02.api.letsencrypt.org/acme/new-order",
"revokeCert": "https://acme-staging-v02.api.letsencrypt.org/acme/revoke-cert"
}
Renewal conf file /etc/letsencrypt/renewal/www.otherdomain.tld.conf is broken. Skipping.
Traceback was:
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/certbot/cert_manager.py", line 383, in _search_lineages
candidate_lineage = storage.RenewableCert(renewal_file, cli_config)
File "/usr/lib/python3/dist-packages/certbot/storage.py", line 463, in __init__
self._check_symlinks()
File "/usr/lib/python3/dist-packages/certbot/storage.py", line 522, in _check_symlinks
"expected {0} to be a symlink".format(link))
certbot.errors.CertStorageError: expected /etc/letsencrypt/live/www.otherdomain.tld/cert.pem to be a symlink
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
You have an existing certificate that contains a portion of the domains you
requested (ref: /etc/letsencrypt/renewal/example.tld.conf)
It contains these names: *.example.tld, example.tld
You requested these names for the new certificate: *.example.tld, example.tld,
www.www.example.tld.
Do you want to expand and replace this existing certificate with the new
certificate?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(E)xpand/(C)ancel: E
Renewing an existing certificate
Requesting fresh nonce
Sending HEAD request to https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce.
https://acme-staging-v02.api.letsencrypt.org:443 "HEAD /acme/new-nonce HTTP/1.1" 200 0
Received response:
HTTP 200
Server: nginx
Date: Fri, 21 Aug 2020 15:47:03 GMT
Connection: keep-alive
Cache-Control: public, max-age=0, no-cache
Link: <https://acme-staging-v02.api.letsencrypt.org/directory>;rel="index"
Replay-Nonce: [REDACTED]
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800
Storing nonce: [REDACTED]
JWS payload:
b'{\n "identifiers": [\n {\n "type": "dns",\n "value": "*.example.tld"\n },\n {\n "type": "dns",\n "value": "example.tld"\n },\n {\n "type": "dns",\n "value": "www.www.example.tld"\n }\n ]\n}'
Sending POST request to https://acme-staging-v02.api.letsencrypt.org/acme/new-order:
{
"protected": "[REDACTED]",
"signature": "[REDACTED]",
"payload": "[REDACTED]"
}
https://acme-staging-v02.api.letsencrypt.org:443 "POST /acme/new-order HTTP/1.1" 201 621
Received response:
HTTP 201
Server: nginx
Date: Fri, 21 Aug 2020 15:47:03 GMT
Content-Type: application/json
Content-Length: 621
Connection: keep-alive
Boulder-Requester: 12742232
Cache-Control: public, max-age=0, no-cache
Link: <https://acme-staging-v02.api.letsencrypt.org/directory>;rel="index"
Location: https://acme-staging-v02.api.letsencrypt.org/acme/order/[REDACTED]/[REDACTED]
Replay-Nonce: [REDACTED]
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800
{
"status": "pending",
"expires": "2020-08-28T15:44:16Z",
"identifiers": [
{
"type": "dns",
"value": "*.example.tld"
},
{
"type": "dns",
"value": "example.tld"
},
{
"type": "dns",
"value": "www.www.example.tld"
}
],
"authorizations": [
"https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/[REDACTED]",
"https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/[REDACTED]",
"https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/[REDACTED]"
],
"finalize": "https://acme-staging-v02.api.letsencrypt.org/acme/finalize/[REDACTED]/[REDACTED]"
}
Storing nonce: [REDACTED]
JWS payload:
b''
Sending POST request to https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/95814309:
{
"protected": "[REDACTED]",
"signature": "[REDACTED]",
"payload": ""
}
https://acme-staging-v02.api.letsencrypt.org:443 "POST /acme/authz-v3/[REDACTED] HTTP/1.1" 200 472
Received response:
HTTP 200
Server: nginx
Date: Fri, 21 Aug 2020 15:47:03 GMT
Content-Type: application/json
Content-Length: 472
Connection: keep-alive
Boulder-Requester: 12742232
Cache-Control: public, max-age=0, no-cache
Link: <https://acme-staging-v02.api.letsencrypt.org/directory>;rel="index"
Replay-Nonce: [REDACTED]
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800
{
"identifier": {
"type": "dns",
"value": "example.tld"
},
"status": "valid",
"expires": "2020-09-17T22:29:52Z",
"challenges": [
{
"type": "dns-01",
"status": "valid",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/[REDACTED]/MUKGGw",
"token": "[REDACTED]",
"validationRecord": [
{
"hostname": "example.tld"
}
]
}
],
"wildcard": true
}
Storing nonce: [REDACTED]
JWS payload:
b''
Sending POST request to https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/[REDACTED]:
{
"protected": "[REDACTED]",
"signature": "[REDACTED]",
"payload": ""
}
https://acme-staging-v02.api.letsencrypt.org:443 "POST /acme/authz-v3/[REDACTED] HTTP/1.1" 200 452
Received response:
HTTP 200
Server: nginx
Date: Fri, 21 Aug 2020 15:47:04 GMT
Content-Type: application/json
Content-Length: 452
Connection: keep-alive
Boulder-Requester: 12742232
Cache-Control: public, max-age=0, no-cache
Link: <https://acme-staging-v02.api.letsencrypt.org/directory>;rel="index"
Replay-Nonce: [REDACTED]-vau0I
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800
{
"identifier": {
"type": "dns",
"value": "example.tld"
},
"status": "valid",
"expires": "2020-09-17T22:29:52Z",
"challenges": [
{
"type": "dns-01",
"status": "valid",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/[REDACTED]/[REDACTED]",
"token": "[REDACTED]",
"validationRecord": [
{
"hostname": "example.tld"
}
]
}
]
}
Storing nonce: [REDACTED]
JWS payload:
b''
Sending POST request to https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/97803217:
{
"protected": "[REDACTED]",
"signature": "[REDACTED]",
"payload": ""
}
https://acme-staging-v02.api.letsencrypt.org:443 "POST /acme/authz-v3/97803217 HTTP/1.1" 200 812
Received response:
HTTP 200
Server: nginx
Date: Fri, 21 Aug 2020 15:47:04 GMT
Content-Type: application/json
Content-Length: 812
Connection: keep-alive
Boulder-Requester: 12742232
Cache-Control: public, max-age=0, no-cache
Link: <https://acme-staging-v02.api.letsencrypt.org/directory>;rel="index"
Replay-Nonce: [REDACTED]
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800
{
"identifier": {
"type": "dns",
"value": "www.www.example.tld"
},
"status": "pending",
"expires": "2020-08-28T15:44:16Z",
"challenges": [
{
"type": "http-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/[REDACTED]/[REDACTED]",
"token": "[REDACTED]"
},
{
"type": "dns-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/[REDACTED]/[REDACTED]",
"token": "[REDACTED]"
},
{
"type": "tls-alpn-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/[REDACTED]/[REDACTED]",
"token": "[REDACTED]"
}
]
}
Storing nonce: [REDACTED]
Performing the following challenges:
dns-01 challenge for www.www.example.tld
No authoritative SOA record found for _acme-challenge.www.www.example.tld
No authoritative SOA record found for www.www.example.tld
No authoritative SOA record found for www.example.tld
Received authoritative SOA response for example.tld
Encountered exception:
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/certbot/auth_handler.py", line 75, in handle_authorizations
resp = self._solve_challenges(aauthzrs)
File "/usr/lib/python3/dist-packages/certbot/auth_handler.py", line 139, in _solve_challenges
resp = self.auth.perform(all_achalls)
File "/usr/lib/python3/dist-packages/certbot/plugins/dns_common.py", line 57, in perform
self._perform(domain, validation_domain_name, validation)
File "/usr/lib/python3/dist-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 76, in _perform
self._get_rfc2136_client().add_txt_record(validation_name, validation, self.ttl)
File "/usr/lib/python3/dist-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 135, in add_txt_record
.format(dns.rcode.to_text(rcode)))
certbot.errors.PluginError: Received response from server: REFUSED
Calling registered functions
Cleaning up challenges
No authoritative SOA record found for _acme-challenge.www.www.example.tld
No authoritative SOA record found for www.www.example.tld
No authoritative SOA record found for www.example.tld
Received authoritative SOA response for example.tld
Encountered exception during recovery:
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/certbot/auth_handler.py", line 75, in handle_authorizations
resp = self._solve_challenges(aauthzrs)
File "/usr/lib/python3/dist-packages/certbot/auth_handler.py", line 139, in _solve_challenges
resp = self.auth.perform(all_achalls)
File "/usr/lib/python3/dist-packages/certbot/plugins/dns_common.py", line 57, in perform
self._perform(domain, validation_domain_name, validation)
File "/usr/lib/python3/dist-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 76, in _perform
self._get_rfc2136_client().add_txt_record(validation_name, validation, self.ttl)
File "/usr/lib/python3/dist-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 135, in add_txt_record
.format(dns.rcode.to_text(rcode)))
certbot.errors.PluginError: Received response from server: REFUSED
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/certbot/error_handler.py", line 108, in _call_registered
self.funcs[-1]()
File "/usr/lib/python3/dist-packages/certbot/auth_handler.py", line 323, in _cleanup_challenges
self.auth.cleanup(achalls)
File "/usr/lib/python3/dist-packages/certbot/plugins/dns_common.py", line 76, in cleanup
self._cleanup(domain, validation_domain_name, validation)
File "/usr/lib/python3/dist-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 79, in _cleanup
self._get_rfc2136_client().del_txt_record(validation_name, validation)
File "/usr/lib/python3/dist-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 170, in del_txt_record
.format(dns.rcode.to_text(rcode)))
certbot.errors.PluginError: Received response from server: REFUSED
Exiting abnormally:
Traceback (most recent call last):
File "/usr/bin/certbot", line 11, in <module>
load_entry_point('certbot==0.31.0', 'console_scripts', 'certbot')()
File "/usr/lib/python3/dist-packages/certbot/main.py", line 1365, in main
return config.func(config, plugins)
File "/usr/lib/python3/dist-packages/certbot/main.py", line 1250, in certonly
lineage = _get_and_save_cert(le_client, config, domains, certname, lineage)
File "/usr/lib/python3/dist-packages/certbot/main.py", line 116, in _get_and_save_cert
renewal.renew_cert(config, domains, le_client, lineage)
File "/usr/lib/python3/dist-packages/certbot/renewal.py", line 310, in renew_cert
new_cert, new_chain, new_key, _ = le_client.obtain_certificate(domains, new_key)
File "/usr/lib/python3/dist-packages/certbot/client.py", line 353, in obtain_certificate
orderr = self._get_order_and_authorizations(csr.data, self.config.allow_subset_of_names)
File "/usr/lib/python3/dist-packages/certbot/client.py", line 389, in _get_order_and_authorizations
authzr = self.auth_handler.handle_authorizations(orderr, best_effort)
File "/usr/lib/python3/dist-packages/certbot/auth_handler.py", line 75, in handle_authorizations
resp = self._solve_challenges(aauthzrs)
File "/usr/lib/python3/dist-packages/certbot/auth_handler.py", line 139, in _solve_challenges
resp = self.auth.perform(all_achalls)
File "/usr/lib/python3/dist-packages/certbot/plugins/dns_common.py", line 57, in perform
self._perform(domain, validation_domain_name, validation)
File "/usr/lib/python3/dist-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 76, in _perform
self._get_rfc2136_client().add_txt_record(validation_name, validation, self.ttl)
File "/usr/lib/python3/dist-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 135, in add_txt_record
.format(dns.rcode.to_text(rcode)))
certbot.errors.PluginError: Received response from server: REFUSED
Received response from server: REFUSED
观察结果、潜在解决方案
其中有一行引起了我的注意:
No authoritative SOA record found for _acme-challenge.www.www.example.tld
Certbot 可能正在尝试对子域进行 nsupdate _acme-challenge.www.www.example.tld
,但update-policy
不允许这样做:
grant certbot. name _acme-challenge.example.tld. txt;
所以问题是:我可以使用哪些更新策略规则来允许 nsupdate _acme-challenge.*.*.example.tld
?是否有规则可以涵盖进一步的子域名_acme-challenge.*.*.*.example.tld
?
答案1
花了一天多的时间之后,我在写问题的时候发现了它。
正如最左侧子域之外没有可用的通配符扩展一样,您也不能以update-policy
这种方式使用通配符规则类型。也就是说,它对 不起作用_acme-domain.*.example.tld
,但对 起作用*.www.example.tld
。
考虑到我已经知道最左边的子域名是,通配符是不必要的。我能做的最好的就是为所有子域名明确_acme-challenge
设置:update-policy
update-policy {
grant certbot. name _acme-challenge.example.tld. txt;
grant certbot. name _acme-challenge.www.example.tld. txt;
};
答案2
你不能直接做你想做的事情,但是,如果你不想在更新策略中明确列出它,你能摆脱一些 CNAME 欺骗并提供角色/访问的分离。
首先,要有:
update-policy {
grant key_update_acme wildcard *.acme.dyn.example.tld. TXT;
}
然后在所有区域中声明:
_acme-challenge.www.example.tld. IN CNAME www.example.tld.acme.dyn.example.tld.
_acme-challenge.example.tld. IN CNAME example.tld.acme.dyn.example.tld.
(etc, for each subdomain you want)
该密钥key_update_acme
仅允许更新 子域内的 TXT 记录*.acme.dyn.example.tld.
。
但是,Certbot 本身并不支持此功能,因此您需要使用其他 ACME 客户端。