[GH-ISSUE #357] Unable to obtain the corresponding TXT record through _acme-challenge.example.tld #200

Open
opened 2026-03-13 16:08:12 +03:00 by kerem · 3 comments
Owner

Originally created by @jinrenjie on GitHub (Jul 15, 2024).
Original GitHub issue: https://github.com/acme-dns/acme-dns/issues/357

Architecture

  • Traefik: 10.8.10.254
  • Smallstep: 10.8.10.253
  • ACME DNS: 10.8.10.252

These services all run in containers and can communicate with each other.

acme-dns.cfg is configured as follows:

[general]
# DNS interface. Note that systemd-resolved may reserve port 53 on 127.0.0.53
# In this case acme-dns will error out and you will need to define the listening interface
# for example: listen = "127.0.0.1:53"
listen = "0.0.0.0:53"
# protocol, "both", "both4", "both6", "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6"
protocol = "both"
# domain name to serve the requests off of
domain = "auth.acme.org"
# zone name server
nsname = "ns1.auth.acme.org"
# admin email address, where @ is substituted with .
nsadmin = "admin.acme.org"
# predefined records served in addition to the TXT
records = [
    "ingress.test. A 10.8.10.254",
    "_acme-challenge.ingress.test. CNAME 1821f647-e876-455c-9cd1-d7c54b5aacd0.auth.acme.org.",
    "ingress.test. NS ns1.auth.acme.org.",
    # domain pointing to the public IP of your acme-dns server 
    "ns1.auth.acme.org. A 10.8.10.252",
    # specify that auth.example.org will resolve any *.auth.example.org records
    "auth.acme.org. NS ns1.auth.acme.org."
]
# debug messages from CORS etc
debug = true

[database]
# Database engine to use, sqlite3 or postgres
engine = "postgres"
# Connection string, filename for sqlite3 and postgres://$username:$password@$host/$db_name for postgres
# Please note that the default Docker image uses path /var/lib/acme-dns/acme-dns.db for sqlite3
# connection = "/var/lib/acme-dns/acme-dns.db"
connection = "postgres://xxxxxx:xxxxxxx@xxxxxxx:5432/acme-dns?sslmode=disable"

[api]
# listen ip eg. 127.0.0.1
ip = "0.0.0.0"
# possible values: "letsencrypt", "letsencryptstaging", "cert", "none"
tls = "none"
# listen port, eg. 443 for default HTTPS
port = "8080"
# disable registration endpoint
disable_registration = false
# only used if tls = "cert"
tls_cert_privkey = "/etc/tls/example.org/privkey.pem"
tls_cert_fullchain = "/etc/tls/example.org/fullchain.pem"
# only used if tls = "letsencrypt"
acme_cache_dir = "api-certs"
# optional e-mail address to which Let's Encrypt will send expiration notices for the API's cert
notification_email = ""
# CORS AllowOrigins, wildcards can be used
corsorigins = [
    "*"
]
# use HTTP header to get the client ip
use_header = false
# header name to pull the ip address / list of ip addresses from
header_name = "X-Forwarded-For"

[logconfig]
# logging level: "error", "warning", "info" or "debug"
loglevel = "debug"
# possible values: stdout, TODO file & integrations
logtype = "stdout"
# file path for logfile TODO
# logfile = "./acme-dns.log"
# format, either "json" or "text"
logformat = "text"

Get acme-dns account fulldomain TXT records:

dig @10.8.10.252 1821f647-e876-455c-9cd1-d7c54b5aacd0.auth.acme.org. txt

; <<>> DiG 9.10.6 <<>> @10.8.10.252 1821f647-e876-455c-9cd1-d7c54b5aacd0.auth.acme.org. txt
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 59470
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;1821f647-e876-455c-9cd1-d7c54b5aacd0.auth.acme.org. IN TXT

;; ANSWER SECTION:
1821f647-e876-455c-9cd1-d7c54b5aacd0.auth.acme.org. 1 IN TXT "___validation_token_received_from_the_ca___"
1821f647-e876-455c-9cd1-d7c54b5aacd0.auth.acme.org. 1 IN TXT "___validation_token_received_from_the_ca___"

;; Query time: 4 msec
;; SERVER: 10.8.10.252#53(10.8.10.252)
;; WHEN: Mon Jul 15 23:18:04 CST 2024
;; MSG SIZE  rcvd: 291

This seems to be all working fine!

Get the NS record of ingress.test:

dig @10.8.10.252 ingress.test ns

; <<>> DiG 9.10.6 <<>> @10.8.10.252 ingress.test ns
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 25235
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;ingress.test.			IN	NS

;; ANSWER SECTION:
ingress.test.		3600	IN	NS	ns1.auth.acme.org.

;; Query time: 1 msec
;; SERVER: 10.8.10.252#53(10.8.10.252)
;; WHEN: Mon Jul 15 23:19:42 CST 2024
;; MSG SIZE  rcvd: 84

Get the TXT record of _acme-challenge.ingress.test:

dig @10.8.10.252 _acme-challenge.ingress.test txt

; <<>> DiG 9.10.6 <<>> @10.8.10.252 _acme-challenge.ingress.test txt
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58130
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;_acme-challenge.ingress.test.	IN	TXT

;; ANSWER SECTION:
_acme-challenge.ingress.test. 3600 IN	CNAME	1821f647-e876-455c-9cd1-d7c54b5aacd0.auth.acme.org.

;; Query time: 3 msec
;; SERVER: 10.8.10.252#53(10.8.10.252)
;; WHEN: Mon Jul 15 23:22:26 CST 2024
;; MSG SIZE  rcvd: 149

There seems to be a problem here. In theory, both the CNAME and TXT records should be queried at the same time, but no TXT record appears. As a result, the Smallstep CA I use cannot verify the DNS challenge and cannot issue a certificate!

This problem has troubled me for a long time and I have not found a solution. I look forward to your answer, which will be of great help to me. Thank you!

Originally created by @jinrenjie on GitHub (Jul 15, 2024). Original GitHub issue: https://github.com/acme-dns/acme-dns/issues/357 # Architecture * Traefik: 10.8.10.254 * Smallstep: 10.8.10.253 * ACME DNS: 10.8.10.252 These services all run in containers and can communicate with each other. `acme-dns.cfg` is configured as follows: ```cfg [general] # DNS interface. Note that systemd-resolved may reserve port 53 on 127.0.0.53 # In this case acme-dns will error out and you will need to define the listening interface # for example: listen = "127.0.0.1:53" listen = "0.0.0.0:53" # protocol, "both", "both4", "both6", "udp", "udp4", "udp6" or "tcp", "tcp4", "tcp6" protocol = "both" # domain name to serve the requests off of domain = "auth.acme.org" # zone name server nsname = "ns1.auth.acme.org" # admin email address, where @ is substituted with . nsadmin = "admin.acme.org" # predefined records served in addition to the TXT records = [ "ingress.test. A 10.8.10.254", "_acme-challenge.ingress.test. CNAME 1821f647-e876-455c-9cd1-d7c54b5aacd0.auth.acme.org.", "ingress.test. NS ns1.auth.acme.org.", # domain pointing to the public IP of your acme-dns server "ns1.auth.acme.org. A 10.8.10.252", # specify that auth.example.org will resolve any *.auth.example.org records "auth.acme.org. NS ns1.auth.acme.org." ] # debug messages from CORS etc debug = true [database] # Database engine to use, sqlite3 or postgres engine = "postgres" # Connection string, filename for sqlite3 and postgres://$username:$password@$host/$db_name for postgres # Please note that the default Docker image uses path /var/lib/acme-dns/acme-dns.db for sqlite3 # connection = "/var/lib/acme-dns/acme-dns.db" connection = "postgres://xxxxxx:xxxxxxx@xxxxxxx:5432/acme-dns?sslmode=disable" [api] # listen ip eg. 127.0.0.1 ip = "0.0.0.0" # possible values: "letsencrypt", "letsencryptstaging", "cert", "none" tls = "none" # listen port, eg. 443 for default HTTPS port = "8080" # disable registration endpoint disable_registration = false # only used if tls = "cert" tls_cert_privkey = "/etc/tls/example.org/privkey.pem" tls_cert_fullchain = "/etc/tls/example.org/fullchain.pem" # only used if tls = "letsencrypt" acme_cache_dir = "api-certs" # optional e-mail address to which Let's Encrypt will send expiration notices for the API's cert notification_email = "" # CORS AllowOrigins, wildcards can be used corsorigins = [ "*" ] # use HTTP header to get the client ip use_header = false # header name to pull the ip address / list of ip addresses from header_name = "X-Forwarded-For" [logconfig] # logging level: "error", "warning", "info" or "debug" loglevel = "debug" # possible values: stdout, TODO file & integrations logtype = "stdout" # file path for logfile TODO # logfile = "./acme-dns.log" # format, either "json" or "text" logformat = "text" ``` Get acme-dns account fulldomain TXT records: ```bash dig @10.8.10.252 1821f647-e876-455c-9cd1-d7c54b5aacd0.auth.acme.org. txt ; <<>> DiG 9.10.6 <<>> @10.8.10.252 1821f647-e876-455c-9cd1-d7c54b5aacd0.auth.acme.org. txt ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 59470 ;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 512 ;; QUESTION SECTION: ;1821f647-e876-455c-9cd1-d7c54b5aacd0.auth.acme.org. IN TXT ;; ANSWER SECTION: 1821f647-e876-455c-9cd1-d7c54b5aacd0.auth.acme.org. 1 IN TXT "___validation_token_received_from_the_ca___" 1821f647-e876-455c-9cd1-d7c54b5aacd0.auth.acme.org. 1 IN TXT "___validation_token_received_from_the_ca___" ;; Query time: 4 msec ;; SERVER: 10.8.10.252#53(10.8.10.252) ;; WHEN: Mon Jul 15 23:18:04 CST 2024 ;; MSG SIZE rcvd: 291 ``` This seems to be all working fine! Get the NS record of `ingress.test`: ```bash dig @10.8.10.252 ingress.test ns ; <<>> DiG 9.10.6 <<>> @10.8.10.252 ingress.test ns ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 25235 ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 512 ;; QUESTION SECTION: ;ingress.test. IN NS ;; ANSWER SECTION: ingress.test. 3600 IN NS ns1.auth.acme.org. ;; Query time: 1 msec ;; SERVER: 10.8.10.252#53(10.8.10.252) ;; WHEN: Mon Jul 15 23:19:42 CST 2024 ;; MSG SIZE rcvd: 84 ``` Get the TXT record of `_acme-challenge.ingress.test`: ```bash dig @10.8.10.252 _acme-challenge.ingress.test txt ; <<>> DiG 9.10.6 <<>> @10.8.10.252 _acme-challenge.ingress.test txt ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58130 ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 512 ;; QUESTION SECTION: ;_acme-challenge.ingress.test. IN TXT ;; ANSWER SECTION: _acme-challenge.ingress.test. 3600 IN CNAME 1821f647-e876-455c-9cd1-d7c54b5aacd0.auth.acme.org. ;; Query time: 3 msec ;; SERVER: 10.8.10.252#53(10.8.10.252) ;; WHEN: Mon Jul 15 23:22:26 CST 2024 ;; MSG SIZE rcvd: 149 ``` There seems to be a problem here. In theory, both the CNAME and TXT records should be queried at the same time, but no TXT record appears. As a result, the Smallstep CA I use cannot verify the DNS challenge and cannot issue a certificate! This problem has troubled me for a long time and I have not found a solution. I look forward to your answer, which will be of great help to me. Thank you!
Author
Owner

@jinrenjie commented on GitHub (Jul 15, 2024):

I think the problem might be here:

func (d *DNSServer) answer(q dns.Question) ([]dns.RR, int, bool, error) {
	var rcode int
	var err error
	var txtRRs []dns.RR
	var authoritative = d.isAuthoritative(q)
	if !d.isOwnChallenge(q.Name) && !d.answeringForDomain(q.Name) {
		rcode = dns.RcodeNameError
	}
	r, _ := d.getRecord(q)

+	for _, rr := range r {
+		if rr.Header().Rrtype == dns.TypeCNAME && len(r) == 1 {
+			q = dns.Question{
+				Name:   rr.(*dns.CNAME).Target,
+				Qtype:  q.Qtype,
+				Qclass: q.Qclass,
+			}
+		}
+	}

	if q.Qtype == dns.TypeTXT {
		if d.isOwnChallenge(q.Name) {
			txtRRs, err = d.answerOwnChallenge(q)
		} else {
			txtRRs, err = d.answerTXT(q)
		}
		if err == nil {
			r = append(r, txtRRs...)
		}
	}
	if len(r) > 0 {
		// Make sure that we return NOERROR if there were dynamic records for the domain
		rcode = dns.RcodeSuccess
	}
	log.WithFields(log.Fields{"qtype": dns.TypeToString[q.Qtype], "domain": q.Name, "rcode": dns.RcodeToString[rcode]}).Debug("Answering question for domain")
	return r, rcode, authoritative, nil
}

When there is only one CNAME record obtained from DNS, use the value of the CNAME record as the parameter of d.answerTXT() to obtain the TXT record in the database.

Once I did this, Smallstep CA Server was able to verify and issue certificates just fine!

I don't know if this is a common practice, But I can traverse and query the TXT records on the CNAME in the cloud service provider's DNS like this:

dig TXT _acme-challenge.betterde.com @223.5.5.5

; <<>> DiG 9.10.6 <<>> TXT _acme-challenge.betterde.com @223.5.5.5
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 59316
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1408
;; QUESTION SECTION:
;_acme-challenge.betterde.com.	IN	TXT

;; ANSWER SECTION:
_acme-challenge.betterde.com. 600 IN	CNAME	1ef56a0d-5f76-4aae-93ca-d3209823a217.betterde.com.
1ef56a0d-5f76-4aae-93ca-d3209823a217.betterde.com. 600 IN TXT "L6gnTrq24MA66xjFQ0jvAFhtia83cxu2zJBtPdMB6UH"

;; Query time: 200 msec
;; SERVER: 223.5.5.5#53(223.5.5.5)
;; WHEN: Tue Jul 16 03:13:24 CST 2024
;; MSG SIZE  rcvd: 164
<!-- gh-comment-id:2229206347 --> @jinrenjie commented on GitHub (Jul 15, 2024): I think the problem might be here: ```diff func (d *DNSServer) answer(q dns.Question) ([]dns.RR, int, bool, error) { var rcode int var err error var txtRRs []dns.RR var authoritative = d.isAuthoritative(q) if !d.isOwnChallenge(q.Name) && !d.answeringForDomain(q.Name) { rcode = dns.RcodeNameError } r, _ := d.getRecord(q) + for _, rr := range r { + if rr.Header().Rrtype == dns.TypeCNAME && len(r) == 1 { + q = dns.Question{ + Name: rr.(*dns.CNAME).Target, + Qtype: q.Qtype, + Qclass: q.Qclass, + } + } + } if q.Qtype == dns.TypeTXT { if d.isOwnChallenge(q.Name) { txtRRs, err = d.answerOwnChallenge(q) } else { txtRRs, err = d.answerTXT(q) } if err == nil { r = append(r, txtRRs...) } } if len(r) > 0 { // Make sure that we return NOERROR if there were dynamic records for the domain rcode = dns.RcodeSuccess } log.WithFields(log.Fields{"qtype": dns.TypeToString[q.Qtype], "domain": q.Name, "rcode": dns.RcodeToString[rcode]}).Debug("Answering question for domain") return r, rcode, authoritative, nil } ``` When there is only one CNAME record obtained from DNS, use the value of the CNAME record as the parameter of `d.answerTXT()` to obtain the TXT record in the database. Once I did this, Smallstep CA Server was able to verify and issue certificates just fine! I don't know if this is a common practice, But I can traverse and query the TXT records on the CNAME in the cloud service provider's DNS like this: ```bash dig TXT _acme-challenge.betterde.com @223.5.5.5 ; <<>> DiG 9.10.6 <<>> TXT _acme-challenge.betterde.com @223.5.5.5 ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 59316 ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 1408 ;; QUESTION SECTION: ;_acme-challenge.betterde.com. IN TXT ;; ANSWER SECTION: _acme-challenge.betterde.com. 600 IN CNAME 1ef56a0d-5f76-4aae-93ca-d3209823a217.betterde.com. 1ef56a0d-5f76-4aae-93ca-d3209823a217.betterde.com. 600 IN TXT "L6gnTrq24MA66xjFQ0jvAFhtia83cxu2zJBtPdMB6UH" ;; Query time: 200 msec ;; SERVER: 223.5.5.5#53(223.5.5.5) ;; WHEN: Tue Jul 16 03:13:24 CST 2024 ;; MSG SIZE rcvd: 164 ```
Author
Owner

@TRPB commented on GitHub (Aug 2, 2024):

@jinrenjie are you able to provide some more specific instructions on that fix?

I have the exact same issue and assumed it was something I'd configured incorrectly. Is there a workaround in the DNS config?

Are you saying we can't have any other CNAMEs at all on the DNS for it to work?

<!-- gh-comment-id:2265732460 --> @TRPB commented on GitHub (Aug 2, 2024): @jinrenjie are you able to provide some more specific instructions on that fix? I have the exact same issue and assumed it was something I'd configured incorrectly. Is there a workaround in the DNS config? Are you saying we can't have any other CNAMEs at all on the DNS for it to work?
Author
Owner

@jinrenjie commented on GitHub (Aug 4, 2024):

@TRPB I think the problem is that when we query the DNS for the TXT record, it doesn't process the CNAME record that exists on the DNS and then query the corresponding TXT record according to the CNAME record!

Later, I did not use this project as the DNS Challenge service provider, but wrote my own project github.com/betterde/cdns, but my project is limited to intranet development or test environment, not for production environment!

<!-- gh-comment-id:2267478055 --> @jinrenjie commented on GitHub (Aug 4, 2024): @TRPB I think the problem is that when we query the DNS for the TXT record, it doesn't process the CNAME record that exists on the DNS and then query the corresponding TXT record according to the CNAME record! Later, I did not use this project as the DNS Challenge service provider, but wrote my own project [github.com/betterde/cdns](https://github.com/betterde/cdns), but my project is limited to intranet development or test environment, not for production environment!
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/acme-dns#200
No description provided.