Table of Contents

After putting our new self-hosted PowerDNS servers in production, it'd be fun to use Infra-as-Code with OctoDNS to deploy and version our DNS records, as well as to secure our DNS records with DNSSEC.


This article is one of a three parts series:

  1. A basic setup
  2. High-Availability and production
  3. Infra-as-Code and DNSSEC (this article)

OctoDNS loves PowerDNS.

Infra-as-Code with OctoDNS

Let's first talk a bit about IaC before jumping into it!

A short summary of both Infra-as-Code and OctoDNS

When you start working with any system after being used to version your code, you start to wonder "can I version my configuration as well, just like I do with my code?". Yeah, it'd be nice to be able to have a versioning system on our DNS servers: as it's a pretty critical system, we could rollback or overwrite whatever's causing problems in case of an issue.

OctoDNS is just that! A way to describe the DNS records and zones state and configuration in a readable and version-able way, using the DevOps' favorite format ever: YAML files!

(joking, we have so many products using the "Yet Another Markup Language" format that we should invent "Yet Another Product Using Yaml": "YAPUY")

Enabling PowerDNS' API

Before being able to query the PowerDNS API, we first need to enable it. Luckily, this is very easy to do, just modify these values in the /etc/powerdns/pdns.conf configuration file:

api=yes
api-key=a super secret string that YOU chose randomly
webserver-address=0.0.0.0
webserver-allow-from=0.0.0.0/0 # To customize to allow your specific range

These options enable the API and set the API key, as well as allow external queries.

I would strongly suggest NOT to expose your PowerDNS API on the wild internet without a proper firewall configured first to restrict access to your IPs.

To apply this configuration, all that is left to do is to run systemctl restart pdns.service.

Setting up the repository

Let's create a local Git repository, using the git init <path> command.

In this folder, we create a Python virtual environment using the python -m venv venv command, and immediately add the newly-created venv/ directory to our .gitignore file, just to avoid it being pushed accidentally. After that, we can activate this virtual environment by running source venv/bin/activate.

We then install the octodns library as well as our OctoDNS PowerDNS provider:

pip install octodns octodns-powerdns

To make sure that dependencies and versions are consistent for all people and environments using it in the future, we can now pip freeze them in a requirements file:

pip freeze > requirements.txt

All needed dependencies can then be reinstalled in one easy command, using the pip install -r requirements.txt command!

Creating the configuration and zone files

We can now create the root directory of our OctoDNS configuration files: config and a (YAML) file named production.yaml to describe our production environment:

---
manager:
  max_workers: 2

providers:
  config:
    class: octodns.provider.yaml.YamlProvider
    directory: ./config
    default_ttl: 60
    enforce_order: False
  ns1:
    class: octodns_powerdns.PowerDnsProvider
    host: "192.168.1.10" # Replace here with your actual Authoritative server address
    scheme: http
    port: 8081 # Replace here with your actual Authoritative server API port
    api_key: env/NS1_API_KEY
    timeout: 10

zones:
  example.com.:
    sources:
      - config
    targets:
      - ns1

This file describes that we have a default DNS record TTL of 60 seconds with only one nameserver, which can be accessed using the 192.168.1.10 IP adress, and that its API is accessible on port 80, with an API key provided through the environment variable named NS1_API_KEY.

Once the API key set to the environment variable and an empty example.com.yaml file created in the same config directory the production.yaml file is located, we can ask OctoDNS to validate our files:

octodns-validate --config-file ./config/production.yaml

If all is good, this command should return nothing.

A GIF where a group of people is talking and one is saying "Sometimes no news is good news"

Creating a few records

This time, we're going to focus on our (currently empty) example.com.yaml file, which lists the records that we want to have for this example.com zone.

First, we'll create our NS records at the root of our zone:

---
'':
  - type: NS
    values:
      - ns1.example.com.
      - ns2.example.com.

This is what should be returned if we try to apply these using octodns-sync --config-file ./config/production.yaml:

********************************************************************************
* example.com.
********************************************************************************
* ns1 (PowerDnsProvider)
*   Delete <ARecord A 3600, ns1.example.com., ['<your primary DNS server IP>']>
*   Delete <ARecord A 3600, ns2.example.com., ['<your secondary DNS server IP>']>
*   Update
*     <NsRecord NS 3600, example.com., ['ns1.example.com.', 'ns2.example.com.']> ->
*     <NsRecord NS 60, example.com., ['ns1.example.com.', 'ns2.example.com.']> (config)
*   Summary: Creates=0, Updates=1, Deletes=2, Existing=3, Meta=False
********************************************************************************

This tells us that we are going to update two NS records on the server ns1, going from TTL 3600 to 60, and that we are about to erase the A records for said NS servers.

As we didn't provide the --doit argument, octodns is only performing a dry-run, which is good because here we forgot to add the IP adresses of our nameservers, so let's add them:

---
'':
  - type: NS
    ttl: 3600
    values:
      - ns1.example.com.
      - ns2.example.com.

ns1:
  - type: A
    value: <your primary DNS server IP>

ns2:
  - type: A
    value: <your secondary DNS server IP>

Here, we added a TTL of 3600 for the NS records, but left the default of 60 for the A records associated with these nameservers.

We can now validate this configuration and apply it on our production servers:

octodns-sync --config-file ./config/production.yaml --doit

A GIF with a woman saying "We are live."

Setting up DNSSEC

Enabling DNSSEC is a good thing, as it ensures both:

  • Data origin authentication: it validates that the records come from a cryptographically trusted source
  • Data integrity: it cryptographically signs the presence (or absence) of records for a given request

Enabling DNSSEC in PowerDNS

To enable DNSSEC for our example.com. zone, we just need to run the following command on our primary server:

pdnsutil secure-zone example.com

We don't need to run this command on the secondary server as it's getting the changes from our primary through its database replication.

If all is good, the command should return this:

Zone example.com secured
Adding NSEC ordering information for zone 'example.com', 3 updates

If it does, then congratulations! You just enabled DNSSEC on your nameservers for this zone!

To go a bit further, an improvement would be to setup NSEC3, which is more advanced and better suited:

pdnsutil set-nsec3 louis-vallat.dev '1 0 0 -'
pdnsutil rectify-zone example.com

This should return the following output:

NSEC3 set, Done, please secure and rectify your zone (or reload it if you are using the bindbackend)
Adding NSEC ordering information for zone 'example.com', 1 updates

Now, our zone is fully DNSSEC-configured, with NSEC3 aswell! Although, the work is not done, as we now need to tell our registrar some information to ensure a full chain of trust from the root servers to our records.

Adding the needed records for the registrar

Now that we are signing our records on our authoritative servers, we need to tell our registrar which key to trust, so that a full chain of trust from the root servers to our records can be established.

For this, we need to find a way to set something called DS records in our registrar's configuration panel for this domain.

When using OVH, you can just open the "DS records" tab in your domain view:

A screenshot from the OVH domain management panel

We can get the value to set using the pdnsutil show-zone example.com command. This will return a description of your zone but should also end with a bunch of lines starting with something like this:

ID = 1 (CSK), flags = 257, tag = <a number between 0 and 65536>, algo = 13, bits = 256	  Active	 Published  ( ECDSAP256SHA256 )
CSK DNSKEY = example.com. IN DNSKEY 257 3 13 <a long base64-encoded string> ; ( ECDSAP256SHA256 )

Most of the information needed for your registrar is on the first of these two lines, especially the flag, tag and algo. The base64-encoded public key is shown on the second line after IN DNSKEY 257 3 13.

Once everything is configured on our registrar's configuration panel, we can wait a bit for it to propagate, before testing that our DNSSEC configuration is correct with a tool like DNSViz.

If you did everything right, you shouldn't get any error or any warning, just like this domain of mine:

A screenshot from my domain analized on DNSViz

As you can see, this domain is fully secured with a correct and complete chain of trust from the root server to my records.

You also get a more precise and verbose result using DNSSEC Debugger, which shows something like this for the same domain:

A screenshot from my domain analized on DNSSEC Debugger

The end

This is the end of this article series! You just configured your own name servers, hosting your own domains with high availability and replicated servers, in production, with DNSSEC, all while managing it using Infra-as-Code!

Congratulations, and see you in another article!