Skip to content

POWERDNS: Allow editing the SOA record#3404

Merged
tlimoncelli merged 3 commits intoStackExchange:mainfrom
Veratil:powerdns-soa
Feb 5, 2025
Merged

POWERDNS: Allow editing the SOA record#3404
tlimoncelli merged 3 commits intoStackExchange:mainfrom
Veratil:powerdns-soa

Conversation

@Veratil
Copy link
Contributor

@Veratil Veratil commented Feb 1, 2025

PowerDNS treats the SOA record like any other, and allows editing it through the API. This PR adds the ability to use the SOA modifier with the PowerDNS provider.

Whether this is allowed for any situation where you do not own the host itself, I cannot say if this will work or not. I have no way to test that. I can only test on hosts that I own. That being said, this does work!

$ docker compose exec powerdns pdnsutil create-zone test.internal
Creating empty zone 'test.internal'


$ docker compose exec powerdns pdnsutil list-zone test.internal
$ORIGIN .
test.internal   3600    IN      SOA     a.misconfigured.dns.server.invalid hostmaster.test.internal 0 10800 3600 604800 3600
var REG_NONE = NewRegistrar("none");
var PROV_PDNS = NewDnsProvider("pdns");

D("test.internal", REG_NONE, DnsProvider(PROV_PDNS),
        SOA('@', 'ns.test.internal.', 'noone.test.internal', 60, 60, 604800, 60, TTL(60)),
        A("@", "172.18.0.2")
);
$ dnscontrol preview
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 1 zone(s)
Serially Gathering: "test.internal"
******************** Domain: test.internal
2 corrections (pdns)
#1: ± BATCHED CHANGE/CREATEs for test.internal
± MODIFY test.internal SOA (a.misconfigured.dns.server.invalid. hostmaster.test.internal. 10800 3600 604800 3600 ttl=3600) -> (ns.test.internal. noone.test.internal.test.internal. 60 60 604800 60 ttl=60)
+ CREATE test.internal A 172.18.0.2 ttl=300
INFO#1: Skipping registrar "none": No nameservers declared for domain "test.internal". Add {no_ns:'true'} to force
Done. 2 corrections.


$ docker compose exec powerdns pdnsutil list-zone test.internal
$ORIGIN .
test.internal   300     IN      A       172.18.0.2
test.internal   60      IN      SOA     ns.test.internal noone.test.internal.test.internal 0 60 60 604800 60


$ dig @127.0.0.1 -p 8053 test.internal SOA

; <<>> DiG 9.18.28-0ubuntu0.22.04.1-Ubuntu <<>> @127.0.0.1 -p 8053 test.internal SOA
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 16124
;; 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: 1232
;; QUESTION SECTION:
;test.internal.                 IN      SOA

;; ANSWER SECTION:
test.internal.          60      IN      SOA     ns.test.internal. noone.test.internal.test.internal. 0 60 60 604800 60

;; Query time: 20 msec
;; SERVER: 127.0.0.1#8053(127.0.0.1) (UDP)
;; WHEN: Fri Jan 31 20:41:39 CST 2025
;; MSG SIZE  rcvd: 101

Side mention, the docs say:

If you accidentally include an @ in the email field DNSControl will quietly change it to a `.`. This way you can specify a human-readable email address when you are making it easier for spammers how to find you.

But in practice when I did noone@test.internal (and me forgetting the . originally):

$ dnscontrol preview
2025/01/31 20:39:24 2 Validation errors:
2025/01/31 20:39:24 ERROR: in SOA @.test.internal: SOA MBox must have '.' instead of '@'
2025/01/31 20:39:24 ERROR: in SOA @.test.internal: target (ns.test.internal) must end with a (.) [https://docs.dnscontrol.org/language-reference/why-the-dot]
exiting due to validation errors

Also note that the MBox needed a trailing . but did not error and ended up noone.test.internal.test.internal :)

@tlimoncelli
Copy link
Collaborator

Hey @jpbede! Can I get your approval plz?

@jpbede
Copy link
Collaborator

jpbede commented Feb 1, 2025

Hmm this could cause conflicts when PowerDNS manages (incrementing on its own when a API update happens) the SOA serial and have the SOA entry here. We should at least mention it in the docs.

@Veratil
Copy link
Contributor Author

Veratil commented Feb 1, 2025

I've committed @jpbede's suggestion, but I'll go through and test with the various options to get a verification on behavior.

@jpbede
Copy link
Collaborator

jpbede commented Feb 1, 2025

I've committed @jpbede's suggestion, but I'll go through and test with the various options to get a verification on behavior.

Awesome, thanks a lot! I currently have no way of doing this.

@Veratil
Copy link
Contributor Author

Veratil commented Feb 1, 2025

Testing with the main 3 options: DEFAULT, INCREASE, EPOCH

TLDR:
DEFAULT: SERIAL will not change until the next push which updates a non-SOA record
INCREASE: SERIAL was changed on first push to reflect META change
EPOCH: SERIAL was changed on first push to reflect META change

I suspect that if the serial for INCREASE was set to 1 before the initial push, the serial would not have changed similar to DEFAULT. EPOCH I suspect by nature will always change.

So with this, usage on a domain that is present already in the wild would possibly need two pushes to update the serial. One for the SOA and one for the serial. Definitely worth mentioning, but looks safe to use!

DEFAULT:

$ docker compose exec powerdns pdnsutil list-all-zones

$ dnscontrol preview
******************** Domain: test.internal
1 correction (pdns)
#1: Ensuring zone "test.internal" exists in "pdns"
SUCCESS!
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 1 zone(s)                                                                                                                                                                                                                 Serially Gathering: "test.internal"
******************** Domain: test.internal
2 corrections (pdns)
#1: ± BATCHED CHANGE/CREATEs for test.internal
± MODIFY test.internal SOA (a.misconfigured.dns.server.invalid. hostmaster.test.internal. 10800 3600 604800 3600 ttl=3600) -> (ns.test.internal. noone.test.internal.test.internal. 60 60 604800 60 ttl=60)
+ CREATE test.internal A 172.18.0.2 ttl=300
INFO#1: Skipping registrar "none": No nameservers declared for domain "test.internal". Add {no_ns:'true'} to force
Done. 3 corrections.

$ docker compose exec powerdns pdnsutil list-all-zones
test.internal

$ docker compose exec powerdns pdnsutil get-meta test.internal
Metadata for 'test.internal'
SOA-EDIT-API = DEFAULT

$ dig @127.0.0.1 -p 8053 test.internal SOA
test.internal.          3600    IN      SOA     a.misconfigured.dns.server.invalid. hostmaster.test.internal. 2025020101 10800 3600 604800 3600

$ dnscontrol push
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 1 zone(s)
Serially Gathering: "test.internal"
******************** Domain: test.internal
2 corrections (pdns)
#1: ± BATCHED CHANGE/CREATEs for test.internal
± MODIFY test.internal SOA (a.misconfigured.dns.server.invalid. hostmaster.test.internal. 10800 3600 604800 3600 ttl=3600) -> (ns.test.internal. noone.test.internal.test.internal. 60 60 604800 60 ttl=60)
+ CREATE test.internal A 172.18.0.2 ttl=300
SUCCESS!
INFO#1: Skipping registrar "none": No nameservers declared for domain "test.internal". Add {no_ns:'true'} to force
Done. 2 corrections.

$ dig @127.0.0.1 -p 8053 test.internal SOA
test.internal.          60      IN      SOA     ns.test.internal. noone.test.internal.test.internal. 2025020101 60 60 604800 60

## SOA SERIAL DID NOT INCREASE OR CHANGE
## Add new record

$ dnscontrol push
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 1 zone(s)
Serially Gathering: "test.internal"
******************** Domain: test.internal
1 correction (pdns)
#1: ± BATCHED CHANGE/CREATEs for test.internal
+ CREATE bump.test.internal A 172.18.0.3 ttl=300
SUCCESS!
INFO#1: Skipping registrar "none": No nameservers declared for domain "test.internal". Add {no_ns:'true'} to force
Done. 1 corrections.

$ dig @127.0.0.1 -p 8053 test.internal SOA
test.internal.          60      IN      SOA     ns.test.internal. noone.test.internal.test.internal. 2025020102 60 60 604800 60

INCREASE

$ docker compose exec powerdns pdnsutil list-all-zones

$ dnscontrol preview
******************** Domain: test.internal
1 correction (pdns)
#1: Ensuring zone "test.internal" exists in "pdns"
SUCCESS!
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 1 zone(s)                                                                                                                                                                                                                 Serially Gathering: "test.internal"
******************** Domain: test.internal
2 corrections (pdns)
#1: ± BATCHED CHANGE/CREATEs for test.internal
± MODIFY test.internal SOA (a.misconfigured.dns.server.invalid. hostmaster.test.internal. 10800 3600 604800 3600 ttl=3600) -> (ns.test.internal. noone.test.internal.test.internal. 60 60 604800 60 ttl=60)
+ CREATE test.internal A 172.18.0.2 ttl=300
INFO#1: Skipping registrar "none": No nameservers declared for domain "test.internal". Add {no_ns:'true'} to force
Done. 3 corrections.

$ docker compose exec powerdns pdnsutil set-meta test.internal SOA-EDIT-API INCREASE
Set 'test.internal' meta SOA-EDIT-API = INCREASE

$ docker compose exec powerdns pdnsutil get-meta test.internal
Metadata for 'test.internal'
SOA-EDIT-API = INCREASE

$ dig @127.0.0.1 -p 8053 test.internal SOA
test.internal.          3600    IN      SOA     a.misconfigured.dns.server.invalid. hostmaster.test.internal. 2025020101 10800 3600 604800 3600

$ dnscontrol push
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 1 zone(s)
Serially Gathering: "test.internal"
******************** Domain: test.internal
2 corrections (pdns)
#1: ± BATCHED CHANGE/CREATEs for test.internal
± MODIFY test.internal SOA (a.misconfigured.dns.server.invalid. hostmaster.test.internal. 10800 3600 604800 3600 ttl=3600) -> (ns.test.internal. noone.test.internal.test.internal. 60 60 604800 60 ttl=60)
+ CREATE test.internal A 172.18.0.2 ttl=300
SUCCESS!
INFO#1: Skipping registrar "none": No nameservers declared for domain "test.internal". Add {no_ns:'true'} to force
Done. 2 corrections.

$ dig @127.0.0.1 -p 8053 test.internal SOA
test.internal.          60      IN      SOA     ns.test.internal. noone.test.internal.test.internal. 1 60 60 604800 60

## SOA SERIAL DID CHANGE TO REPRESENT META CHANGE
## Add new record

$ dnscontrol push
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 1 zone(s)
Serially Gathering: "test.internal"
******************** Domain: test.internal
1 correction (pdns)
#1: ± BATCHED CHANGE/CREATEs for test.internal
+ CREATE bump.test.internal A 172.18.0.3 ttl=300
SUCCESS!
INFO#1: Skipping registrar "none": No nameservers declared for domain "test.internal". Add {no_ns:'true'} to force
Done. 1 corrections.

$ dig @127.0.0.1 -p 8053 test.internal SOA
test.internal.          60      IN      SOA     ns.test.internal. noone.test.internal.test.internal. 2 60 60 604800 60

EPOCH

$ docker compose exec powerdns pdnsutil list-all-zones

$ dnscontrol preview
******************** Domain: test.internal
1 correction (pdns)
#1: Ensuring zone "test.internal" exists in "pdns"
SUCCESS!
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 1 zone(s)                                                                                                                                                                                                                 Serially Gathering: "test.internal"
******************** Domain: test.internal
2 corrections (pdns)
#1: ± BATCHED CHANGE/CREATEs for test.internal
± MODIFY test.internal SOA (a.misconfigured.dns.server.invalid. hostmaster.test.internal. 10800 3600 604800 3600 ttl=3600) -> (ns.test.internal. noone.test.internal.test.internal. 60 60 604800 60 ttl=60)
+ CREATE test.internal A 172.18.0.2 ttl=300
INFO#1: Skipping registrar "none": No nameservers declared for domain "test.internal". Add {no_ns:'true'} to force
Done. 3 corrections.

$ docker compose exec powerdns pdnsutil set-meta test.internal SOA-EDIT-API EPOCH
Set 'test.internal' meta SOA-EDIT-API = EPOCH

$ docker compose exec powerdns pdnsutil get-meta test.internal
Metadata for 'test.internal'
SOA-EDIT-API = EPOCH

$ dig @127.0.0.1 -p 8053 test.internal SOA
test.internal.          3600    IN      SOA     a.misconfigured.dns.server.invalid. hostmaster.test.internal. 2025020101 10800 3600 604800 3600

$ dnscontrol push
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 1 zone(s)
Serially Gathering: "test.internal"
******************** Domain: test.internal
2 corrections (pdns)
#1: ± BATCHED CHANGE/CREATEs for test.internal
± MODIFY test.internal SOA (a.misconfigured.dns.server.invalid. hostmaster.test.internal. 10800 3600 604800 3600 ttl=3600) -> (ns.test.internal. noone.test.internal.test.internal. 60 60 604800 60 ttl=60)
+ CREATE test.internal A 172.18.0.2 ttl=300
SUCCESS!
INFO#1: Skipping registrar "none": No nameservers declared for domain "test.internal". Add {no_ns:'true'} to force
Done. 2 corrections.

$ dig @127.0.0.1 -p 8053 test.internal SOA
test.internal.          60      IN      SOA     ns.test.internal. noone.test.internal.test.internal. 1738420655 60 60 604800 60

## SOA SERIAL DID CHANGE TO REPRESENT META CHANGE
## Add new record

$ dnscontrol push
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 1 zone(s)
Serially Gathering: "test.internal"
******************** Domain: test.internal
1 correction (pdns)
#1: ± BATCHED CHANGE/CREATEs for test.internal
+ CREATE bump.test.internal A 172.18.0.3 ttl=300
SUCCESS!
INFO#1: Skipping registrar "none": No nameservers declared for domain "test.internal". Add {no_ns:'true'} to force
Done. 1 corrections.

$ dig @127.0.0.1 -p 8053 test.internal SOA
test.internal.          60      IN      SOA     ns.test.internal. noone.test.internal.test.internal. 1738421231 60 60 604800 60

@Veratil
Copy link
Contributor Author

Veratil commented Feb 1, 2025

Also, I didn't want to continue testing the SOA-EDIT and SOA-EDIT-INCREASE as I suspected after the three above behavior would be the same. If there's any state that is wanted to test though I'll be happy to set it up locally.

@jpbede
Copy link
Collaborator

jpbede commented Feb 1, 2025

Would be great if you could update the documentation and add a phrase that using SOA may need a second run if SOA-EDIT-API != NONE and it is not recommended to use both at the same time?

Then I'm more than happy with this @tlimoncelli

@Veratil
Copy link
Contributor Author

Veratil commented Feb 1, 2025

I think it's incorrect in saying you cannot use SOA-EDIT-API with SOA. Clearly the usage works as displayed above, but it must be noted that on an SOA record change the serial will not be updated (unless the SOA-EDIT-API has changed). Therefore it is recommended to run an update of the SOA record alone and then push any other changes.

@jpbede
Copy link
Collaborator

jpbede commented Feb 2, 2025

Yes, could you add this to the docs?

Copy link
Collaborator

@jpbede jpbede left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot 👍

@Veratil
Copy link
Contributor Author

Veratil commented Feb 2, 2025

Do you think we can remove the extra commit on the SOA provider.Can? This isn't accurate and can probably lead to misunderstandings.

@jpbede
Copy link
Collaborator

jpbede commented Feb 2, 2025

Ah yes, sure

@tlimoncelli
Copy link
Collaborator

Ready for merge?

@jpbede
Copy link
Collaborator

jpbede commented Feb 5, 2025

@tlimoncelli Ready to merge from my side

@tlimoncelli tlimoncelli merged commit f80c1c0 into StackExchange:main Feb 5, 2025
The SOA record is supported for use, but behavior is slightly different than expected.
If the SOA record is used, [PowerDNS will not increase the serial](https://doc.powerdns.com/authoritative/dnsupdate.html#soa-serial-updates) if the SOA record content changes.
This itself comes with exceptions as well, if the `SOA-EDIT-API` is changed to a different value the logic will update the serial to a new value.
See [this issue for detailed testing](https://github.com/StackExchange/dnscontrol/pull/3404#issuecomment-2628989200) of behavior.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late reply, I’ve been really busy. What’s the reasoning behind not including these results in the documentation? I believe it could be helpful for someone less familiar with GitHub. Also, with a code refactor, these examples could be updated.

cc: @Veratil, @tlimoncelli.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, the description of behavior is well enough defined without an overload of information for those that do not need it.

AsifNawaz-cnic pushed a commit to centralnicgroup-opensource/rtldev-middleware-dnscontrol that referenced this pull request Jan 19, 2026
Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
@flindeberg
Copy link
Contributor

I think this change might have introduced a regression, if you do not manually specify SOA-records powerdns will remove your SOA-record and render your zone(s) invalid. This regardless of soa_edit_api-setting.

I.e., dual provider setups are /sometimes/ impossible, as not all providers allow you to specify SOA-records.

I will try to update this thread with more information and debug logs later on.

@flindeberg
Copy link
Contributor

Illustration of my last post, e.g. "delete SOA if not explicit"-regression. The issue here is that the change is breaking. E.g. the text from patch-notes is quite miss-representing the scale of the change:

f80c1c0: POWERDNS: Allow editing the SOA record (#3404) (@Veratil)

I think that the pdns-provider never should delete SOA-records to maintain backwards compatibility with earlier iterations. This also allows for using pdns as one of many providers, as you cannot run dnscontrol with several providers if they are not somewhat aligned on the records you should specify. For example the AXFR+DDNS-provider and the pdns-provider are now mutually exclusive as SOA is explicitly forbidden for one and explicitly required for the other.

This would not work:

D("test.org", REG_CHANGEME
    , DnsProvider(DSP_PDNS) // <- powerdns, NEEDS SOA explicitly
    , DnsProvider(DSP_DDNS) // <- DDNS, forbids SOA explictly
    , DefaultTTL(3600)
    , SOA("@", "ns.example.org.", "webmaster.example.org.", 3600, 600, 604800, 1440) // How to do here??
    , NAMESERVER("ns.example.org.")
    , TXT("@", "A STRING")
    , A("@", "127.0.0.3")
)

Tried with all of the possible soa_edit_api-settings as well, but exclude them below as they did not matter for the general "delete SOA if not explicit"-regression.

Prior to proper zone-setup (e.g. zones exist in pdns but SOA is default):

## 192.168.1.90 port 5355 is running an otherwise empty latest version `pdns` nameserver. 
> dig @192.168.1.90 -p 5355 soa test.com test.org +short
a.misconfigured.dns.server.invalid. hostmaster.test.com. 1769590229 10800 3600 604800 3600
a.misconfigured.dns.server.invalid. hostmaster.test.org. 1769590230 10800 3600 604800 3600
> dig @192.168.1.90 -p 5355 axfr test.com test.org +short
a.misconfigured.dns.server.invalid. hostmaster.test.com. 1769590933 10800 3600 604800 3600
a.misconfigured.dns.server.invalid. hostmaster.test.com. 1769590933 10800 3600 604800 3600
a.misconfigured.dns.server.invalid. hostmaster.test.org. 1769590933 10800 3600 604800 3600
a.misconfigured.dns.server.invalid. hostmaster.test.org. 1769590933 10800 3600 604800 3600
## v4.15 - prior to dnscontrol SOA-support for pdns, leaves SOAs untouched
> docker run --rm -it -v "$(pwd):/dns" ghcr.io/stackexchange/dnscontrol:4.15.0 preview --config testzones/testzones.js
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 2 zone(s)
Serially Gathering: "test.com"
Serially Gathering: "test.org"
******************** Domain: test.com
INFO#1: No nameservers declared for domain "test.com"; skipping registrar. Add {no_ns:'true'} to force
******************** Domain: test.org
INFO#1: No nameservers declared for domain "test.org"; skipping registrar. Add {no_ns:'true'} to force
Done. 0 corrections.
## v4.16 - after dnscontrol SOA-support for pdns, result is to delete SOAs
> docker run --rm -it -v "$(pwd):/dns" ghcr.io/stackexchange/dnscontrol:4.16.0 preview --config testzones/testzones.js
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 2 zone(s)
Serially Gathering: "test.com"
Serially Gathering: "test.org"
******************** Domain: test.com
1 correction (localpdns)
#1: - BATCHED DELETEs for test.com
- DELETE test.com SOA a.misconfigured.dns.server.invalid. hostmaster.test.com. 10800 3600 604800 3600 ttl=3600
INFO#1: No nameservers declared for domain "test.com"; skipping registrar. Add {no_ns:'true'} to force
******************** Domain: test.org
1 correction (localpdns)
#1: - BATCHED DELETEs for test.org
- DELETE test.org SOA a.misconfigured.dns.server.invalid. hostmaster.test.org. 10800 3600 604800 3600 ttl=3600
INFO#1: No nameservers declared for domain "test.oth"; skipping registrar. Add {no_ns:'true'} to force
Done. 2 corrections.

Now, lets try pushing as well to see what happens. First off v4.15.

## 192.168.1.90 port 5355 is running an otherwise empty latest version `pdns` nameserver. 
> dig @192.168.1.90 -p 5355 soa test.com test.org +short
a.misconfigured.dns.server.invalid. hostmaster.test.com. 1769590229 10800 3600 604800 3600
a.misconfigured.dns.server.invalid. hostmaster.test.org. 1769590230 10800 3600 604800 3600
> dig @192.168.1.90 -p 5355 axfr test.com test.org +short
a.misconfigured.dns.server.invalid. hostmaster.test.com. 1769590933 10800 3600 604800 3600
a.misconfigured.dns.server.invalid. hostmaster.test.com. 1769590933 10800 3600 604800 3600
a.misconfigured.dns.server.invalid. hostmaster.test.org. 1769590933 10800 3600 604800 3600
a.misconfigured.dns.server.invalid. hostmaster.test.org. 1769590933 10800 3600 604800 3600
## push with v4.15
> docker run --rm -it -v "$(pwd):/dns" ghcr.io/stackexchange/dnscontrol:4.15.0 push --config testzones/testzones.js
******************** Domain: test.com
WARNING: No nameservers declared; skipping registrar. Add {no_ns:'true'} to force.
******************** Domain: test.org
WARNING: No nameservers declared; skipping registrar. Add {no_ns:'true'} to force.
Done. 0 corrections.
> dig @192.168.1.90 -p 5355 soa test.com test.org +short
a.misconfigured.dns.server.invalid. hostmaster.test.com. 1769590933 10800 3600 604800 3600
a.misconfigured.dns.server.invalid. hostmaster.test.org. 1769590933 10800 3600 604800 3600
> dig @192.168.1.90 -p 5355 axfr test.com test.org +short
a.misconfigured.dns.server.invalid. hostmaster.test.com. 1769590933 10800 3600 604800 3600
a.misconfigured.dns.server.invalid. hostmaster.test.com. 1769590933 10800 3600 604800 3600
a.misconfigured.dns.server.invalid. hostmaster.test.org. 1769590933 10800 3600 604800 3600
a.misconfigured.dns.server.invalid. hostmaster.test.org. 1769590933 10800 3600 604800 3600

No changes! Let's try v4.16.

> dig @192.168.1.90 -p 5355 soa test.com test.org +short
a.misconfigured.dns.server.invalid. hostmaster.test.com. 1769590933 10800 3600 604800 3600
a.misconfigured.dns.server.invalid. hostmaster.test.org. 1769590933 10800 3600 604800 3600
> dig @192.168.1.90 -p 5355 axfr test.com test.org +short
a.misconfigured.dns.server.invalid. hostmaster.test.com. 1769590933 10800 3600 604800 3600
a.misconfigured.dns.server.invalid. hostmaster.test.com. 1769590933 10800 3600 604800 3600
a.misconfigured.dns.server.invalid. hostmaster.test.org. 1769590933 10800 3600 604800 3600
a.misconfigured.dns.server.invalid. hostmaster.test.org. 1769590933 10800 3600 604800 3600
## Push with v4.16
> docker run --rm -it -v "$(pwd):/dns" ghcr.io/stackexchange/dnscontrol:4.16.0 push --config testzones/testzones.js
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 2 zone(s)
Serially Gathering: "test.com"
Serially Gathering: "test.org"
******************** Domain: test.com
1 correction (localpdns)
#1: - BATCHED DELETEs for test.com
- DELETE test.com SOA a.misconfigured.dns.server.invalid. hostmaster.test.com. 10800 3600 604800 3600 ttl=3600
SUCCESS!
INFO#1: Skipping registrar "none": No nameservers declared for domain "test.com". Add {no_ns:'true'} to force
******************** Domain: test.org
1 correction (localpdns)
#1: - BATCHED DELETEs for test.org
- DELETE test.org SOA a.misconfigured.dns.server.invalid. hostmaster.test.org. 10800 3600 604800 3600 ttl=3600
SUCCESS!
INFO#1: Skipping registrar "none": No nameservers declared for domain "test.org". Add {no_ns:'true'} to force
Done. 2 corrections.
# No anser for soa
> dig @192.168.1.90 -p 5355 soa test.com test.org +short
# transfer failed, because the zone is obviously broken without SOA
> dig @192.168.1.90 -p 5355 axfr test.com test.org +short
; Transfer failed.
; Transfer failed.

From a pdns-perspective the server no longer thinks its authoritative for the zone in question:

<pdns-server>: AXFR-out zone 'test.com', client '192.168.1.42', failed: not authoritative
<pdns-server>: AXFR-out zone 'test.org', client '192.168.1.42', failed: not authoritative

Running v4.15 again does not "reinstate" the SOAs.

# v4.15
> docker run --rm -it -v "$(pwd):/dns" ghcr.io/stackexchange/dnscontrol:4.15.0 preview --config testzones/testzones.js
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 2 zone(s)
Serially Gathering: "test.com"
Serially Gathering: "test.org"
******************** Domain: test.com
INFO#1: No nameservers declared for domain "test.com"; skipping registrar. Add {no_ns:'true'} to force
******************** Domain: test.org
INFO#1: No nameservers declared for domain "test.org"; skipping registrar. Add {no_ns:'true'} to force
Done. 0 corrections.

Lets try to add some records without SOA (identical previews):

# v4.15
> docker run --rm -it -v "$(pwd):/dns" ghcr.io/stackexchange/dnscontrol:4.15.0 preview --config testzones/testzones.js
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 2 zone(s)
Serially Gathering: "test.com"
Serially Gathering: "test.org"
******************** Domain: test.com
3 corrections (localpdns)
#1: ± BATCHED CHANGE/CREATEs for test.com
+ CREATE test.com NS ns.example.org. ttl=300
+ CREATE test.com A 127.0.0.3 ttl=3600
+ CREATE test.com TXT "A STRING" ttl=3600
******************** Domain: test.org
3 corrections (localpdns)
#1: ± BATCHED CHANGE/CREATEs for test.org
+ CREATE test.org NS ns.example.org. ttl=300
+ CREATE test.org A 127.0.0.3 ttl=3600
+ CREATE test.org TXT "A STRING" ttl=3600
Done. 6 corrections.
# v4.16
> docker run --rm -it -v "$(pwd):/dns" ghcr.io/stackexchange/dnscontrol:4.16.0 preview --config testzones/testzones.js
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 2 zone(s)
Serially Gathering: "test.com"
Serially Gathering: "test.org"
******************** Domain: test.com
3 corrections (localpdns)
#1: ± BATCHED CHANGE/CREATEs for test.com
+ CREATE test.com NS ns.example.org. ttl=300
+ CREATE test.com A 127.0.0.3 ttl=3600
+ CREATE test.com TXT "A STRING" ttl=3600
******************** Domain: test.org
3 corrections (localpdns)
#1: ± BATCHED CHANGE/CREATEs for test.org
+ CREATE test.org NS ns.example.org. ttl=300
+ CREATE test.org A 127.0.0.3 ttl=3600
+ CREATE test.org TXT "A STRING" ttl=3600
Done. 6 corrections.

Pushing with v4.15 or v4.16 (without explicit SOA) does not reinstate SOAs. E.g., running once with v4.16+ without explicit SOA will bork zones which further v4.15 runs will not "fix".

Adding in an explicit SOA for test.org (only one of the domains!) and re-running with both v4.15 and v4.16:

# v4.15 which "fails" due to not supporting SOA-records
> docker run --rm -it -v "$(pwd):/dns" ghcr.io/stackexchange/dnscontrol:4.15.0 preview --config testzones/testzones.js
2026/01/28 09:21:07 1 Validation errors:
2026/01/28 09:21:07 ERROR: domain test.org uses SOA records, but DNS provider type POWERDNS does not support them
exiting due to validation errors
# v4.16 is all happy and wants to create a SOA-record
> docker run --rm -it -v "$(pwd):/dns" ghcr.io/stackexchange/dnscontrol:4.16.0 preview --config testzones/testzones.js
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 2 zone(s)
Serially Gathering: "test.com"
Serially Gathering: "test.org"
******************** Domain: test.com
******************** Domain: test.org
1 correction (localpdns)
#1: ± BATCHED CHANGE/CREATEs for test.org
+ CREATE test.org SOA ns.example.org. webmaster.example.org. 3600 600 604800 1440 ttl=3600
Done. 1 corrections.

Pushing with v4.16 puts test.org back on track:

docker run --rm -it -v "$(pwd):/dns" ghcr.io/stackexchange/dnscontrol:4.16.0 push --config testzones/testzones.js
CONCURRENTLY gathering 0 zone(s)
SERIALLY gathering 2 zone(s)
Serially Gathering: "test.com"
Serially Gathering: "test.org"
******************** Domain: test.com
******************** Domain: test.org
1 correction (localpdns)
#1: ± BATCHED CHANGE/CREATEs for test.org
+ CREATE test.org SOA ns.example.org. webmaster.example.org. 3600 600 604800 1440 ttl=3600
SUCCESS!
Done. 1 corrections.
## test.com still borked
> dig @192.168.1.90 -p 5355 soa test.com test.org +short
ns.example.org. webmaster.example.org. 1769592085 3600 600 604800 1440
## test.com still borked
> dig @192.168.1.90 -p 5355 axfr test.com test.org +short
; Transfer failed.
ns.example.org. webmaster.example.org. 1769592085 3600 600 604800 1440
127.0.0.3
ns.example.org.
"A STRING"
ns.example.org. webmaster.example.org. 1769592085 3600 600 604800 1440

From pdns-server perspective:

<pdns-server>: AXFR-out zone 'test.com', client '192.168.1.42', failed: not authoritative
<pdns-server>: AXFR-out zone 'test.org', client '192.168.1.42', transfer initiated

I think above should be enough to illustrate that the v4.16 change is breaking with regards to SOA and pdns-provision.

@flindeberg
Copy link
Contributor

Ping @tlimoncelli @jpbede wrt above. Is the best way forward a PR updating the pdns-provider docs wrt breaking SOA behavior between versions?

@Veratil
Copy link
Contributor Author

Veratil commented Jan 28, 2026

😩 I can't believe I overlooked this. Sorry if this affected anyone.

@tlimoncelli
Copy link
Collaborator

Ping @tlimoncelli @jpbede wrt above. Is the best way forward a PR updating the pdns-provider docs wrt breaking SOA behavior between versions?

Yes. Please update the doc (documentation/provider/powerdns.md)

@flindeberg
Copy link
Contributor

Created #4028 .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants