DNS service drivers

ACME’s dns-01 authorization was sewer’s original target. There are a number of DNS services supported in-tree, and implementations for other services are difficult to write only if the service’s API is difficult.

DNS services supported

Currently, these are all legacy drivers, built on the original DNS-only interface. That’s okay, there’s no plan to drop them (just a hope that interested users will step up to get them updated), but that does mean that support for some features varies.

service driver name wc+ alias prop notes
acme-dns acmedns ? no no  
Aliyun aliyun ? no no  
Aurora aurora ? no no  
Cloudflare cloudflare ? no no patch in #123, needs confirmation?
ClouDNS cloudns ? no no test coverage 75%
DNSPod dnspod ? no no  
DuckDNS duckdns ? no no  
Gandi gandi OK no no wildcard & other fixes in 0.8.3
Hurricane Electric hurricane ? no no test coverage 70%
PowerDNS powerdns NO no no apparently not in 0.8.2; bug #195
Rackspace rackspace ? no no test coverage 69%
Route 53 (AWS) route53 (1) OK no no wc+ in 0.8.2; not in CLI
Unbound unbound_ssh OK yes no Working demonstrator model for local unbound server

Add a driver for your DNS service

Most (?) of the DNS drivers came about because someone wanted to use sewer with their DNS service provider, but there wasn’t a driver to use with the SP yet. This involvement of sewer and DNS-service users is a practical necessity, as there is no substitute for being able to test the driver against the DNS-service, and many such services are for-pay or bundled with other for-pay services.

sewer’s legacy DNS driver interface (BaseDns in dns_providers/ is deprecated, although there is no plan for its removal other than after they have all migrated to the new interface. New DNS drivers should use DNSProviderBase from the start, of course, and will be placed in sewer/providers/ if added to the project.

# sketch of simple dns-01 provider, including alias support

from .. import auth
from .. import lib

class Provider(auth.DNSProviderBase):
    def __init__(self, *, my_api_url, my_api_id, my_api_key, **kwargs):
        super().__init__(self, **kwargs)
        self.api_url = my_api_url
        self.api_id = my_api_id
        self.api_key = my_api_key

    def setup(self, challenges):
        for challenge in challenges:
            fqdn = self.target_domain(challenge)
            txt_value = lib.dns_challenge(challenge["key_auth"])
            self.my_api_add_txt(fqdn, txt_value)

    def unpropagated(self, challenges):
        return []  # if service has a propagation check, use it here

    def clear(self, challenges):
        # like setup, but calling my_api_del_txt; may not need txt_value

    def my_api_add_txt(self, fqdn, txt_value):
        # this is where you talk to the DNS service to add a TXT

    def my_api_del_txt(self, fqdn):
        # talk to DNS service to remove TXT

Most of your work is in implementing the two methods (or one method, or inline code, but inline makes testing without access to the service more difficult) which actually communicate with the DNS service. This can be easy or very difficult, depending on the service provider’s API (or lack of designed API if you have to use a mix of web scraping and HTTP request generation to operate a mechanism that was designed for interactive use).

The above is bare-bones, not taking advantage of the batching of challenges which the new-model interface provides - that can be a big win for large-SAN certificates if you have to grovel the service’s API (or web pages) to guide the construction of your commands to them. It does show the use of target_domain to support DNS aliasing.