Easy DNS zone management across providers

2020-06-07

For personal projects and at work I have to manage some DNS zones. They are not that many, but I would say that if they are more than 2 or 3, or if the management interface is not extremely easy and comfortable, a way to automate this process as much as possible is really beneficial.

There are many alternatives out there, like Github's Octodns or StackExchange's dnscontrol. I personally use the latter.

Using tools like these bring a couple of benefits. For one, you can have your DNS zones under version control. Ok, you can do that too with BIND zone files, but it's more difficult to do that if you use one of the "cloud providers" as I do at work. Another benefit is that you can wire this with your CI platform of choice. Again, BIND zones are pretty good at this too. Rsync + reload and you're set. This is the magic of the Unix way. Problem is that one cannot always choose what one works with, but I digress.

Dnscontrol is both a DSL and a cli tool that interprets it and takes the appropriate actions to ensure that your zones are a reflection of what you have on disk. Be that API calls to your fancy provider or interactions with ISC-BIND to update the zones on the fly.

It's written in Go, so good for your fancy container life and supports a fairly comprehensive list of providers.

It also has the ability to export your current zones to dnscontrol's DSL, so migration is easy.

And finally, it can be used to issue LetsEncrypt certificates.

The main problem for me is that the configuration file is JavaScript (or something pretty similar). Yes, I facepalmed too.

But, a part from that, is really simple to use and pretty fast.

I personally only tested it against GANDI and Amazon's R53, but I assume it works the same way for other providers. There's a fairly good documentation on their website.

What I put here are some notes on a basic setup. The installation process can take many paths depending on what you want. So take a look at their documentation. There's a port for OpenBSD, so that's what I used.

Create a folder that will contain all your zone info and credentials to access your providers. Remember to ignore the credentials file in your version control system of choice.

The credentials file is called creds.js by default, but can be anything. You'll have to pass some info to the cli if you do not use the default. It looks like this:

{
  "gandi": {
    "apikey": "super-secret-api-key"
  },
  "r53": {
    "KeyId": "super-secret-key-id",
    "SecretKey": "super-secret-secret-key"
  }
}

The main file, which describes your zones, it's called dnsconfig.js by default and looks like this:

// Providers:

var REG_NONE = NewRegistrar('none', 'NONE');                  // No registrar.
var GANDI = NewDnsProvider("gandi", "GANDI_V5");              // Gandi.

// Domains:

D("example.com", REG_NONE, DnsProvider(GANDI),
        A('@', '100.100.100.100', TTL(86400)),
        AAAA('@', '2000:abc:dead:beef::1', TTL(86400)),
        CAA('@', 'issue', 'letsencrypt.org'),
        MX('@', 10, 'mail01.example.com.'),
        TXT('@', 'v=spf1 a mx ~all'),
        SRV('_xmpp-client._tcp', 5, 0, 5222, 'example.com.', TTL(86400)),
        SRV('_xmpp-server._tcp', 5, 0, 5269, 'example.com.', TTL(86400)),
        A('main', '100.100.100.200', TTL(86400)),
        CNAME('foo', 'example.com.', TTL(600)),
        CNAME('bar', 'main.example.com.', TTL(600))
);

As you can see, first there's the provider definition. The REG_NONE line is there to define an "empty registrar". I do this because my zones are already registered with my providers, but you could create the zone and register it automatically if all your config is correct.

In fact, the only required parameters to the D function are the fqdn and the registrar.

Each domain has its D function that defines it. I usually organize it like you see on the example. With the 1st line containing the fqdn, registrar and DNS provider, and then one line per DNS record. That makes a lot of sense for visually clean diffs later on. But you can use whatever you want as long as you respect the syntax.

There's a complete description list of the DSL functions and modifiers at the language reference section of their website.

Once you have your zones defined, execute dnscontrol preview. I would recommend to set the records as you have them if the zone already exists (more on this later). In that case the output should be similar to this:

******************** Domain: example.com
----- Getting nameservers from: gandi
----- DNS Provider: gandi...0 corrections
----- Registrar: none...0 corrections

When you have changes it may look similar to this:

******************** Domain: example.com
----- Getting nameservers from: gandi
----- DNS Provider: gandi...2 corrections
#1: CREATE CNAME blah.example.com foo.duckdns.org. ttl=600
#2: MODIFY CNAME bar.example.com: (main.example.com. ttl=600) -> (abc.duckdns.org. ttl=600)
----- Registrar: none...0 corrections

To actually apply the changes, execute dnscontrol push

You can specify the location of the credentials file and the zones file using --creds file and --config file. Check the command help for more information.

As I said earlier, dnscontrol can export your current zone definitions converting them to its DSL in the process. To do that, the command would look something like this:

dnscontrol get-zones --format=js gandi GANDI_V5 example.com

That will dump to stdout your zone in the JS DSL format. I recommend to review the export. Sometimes is not as good as you might expect.

The first argument after the format is the credential definition, the second one is the provider type and then a list of zones to export.

I should mention that it can also export in other formats, like BIND file format or TSV, so it can be used to do other kind of migrations too.

And finally, the TLS certificates.

This needs another config file, called certs,json by default. Again pretty simple, it looks like this:

 [
     {
         "cert_name": "downloads",
         "names": [
             "dl.example.com"
         ]
     },
     {
         "cert_name": "web",
         "names": [
             "example.com",
             "www.example.com"
         ]
     }
 ]

Then you can call it like this:

dnscontrol get-certs --email me@example.com --agreeTOS --dir './certs'

That will crate a hierarchy under ./certs with the generated keys and certificates (plus some json metadata).

There are some more options on this command to change the ACME issuer (which has defaults for Let's Encrypt), remaining days to renew, etc. Take a look at the command help for more info.

In combination with cron and scp/rsync/whatever it can be used to have a central point for certificate generation.

Hope it's useful.

Have any comments ? Send an email to the comments address.