I’ve been wanting to migrate DNS servers I have in a DMZ from Windows Core to another DNS service for some time. With the Windows Core servers, zones were being transfered to the core servers to serve records to DMZ servers. I didn’t like this approach because it was transferring the whole zone and could allow a malicious actor to enumerate the whole domain. Our DNS doesn’t change that often, so the ‘manually’ updating of a file didn’t seem to big of a deal, but it is another set of records that must be kept, but in that it does provide flexibility without the dependence of Active Directory. Hopefully with good team notifications and documentation we can ensure that mismatch and errors will be an issue. I had a few goals with this project.

  • Simple management free from Active Directory.
  • Docker based for easy deployment.
  • Lightweight
  • Resilient

I considered a couple options including BIND9, PowerDNS, Unbound, and CoreDNS. CoreDNS looked the most appealing since it seemed most geared toward containerization and easy deployment and management. Additionally it supports Kubernetes in which we’ve started exploring, so it could be a good fit for that also. I realize BIND9 is generally the defacto, but it also looks a ‘bit more complicated’ to run than I wanted for this projects. I also liked the modular approach to CoreDNS as it allowed for turning on only the features I need or wanted (acls and prometheus metrics).

Let’s start:

  1. To start, I’m assuming you have at least one host up and running. I’m also assuming you have docker and docker-composed installed. If you need to go through those steps please do so first as I won’t be covering that.

  2. Next we will want to free up your host from utilizing port 53 for DNS requests. This site was helpful in doing so on Ubuntu. You may need to search how to do this on your own OS if you are using something like CentOS, or Fedora.

  3. On your primary host, create a docker-compose.yml as follows:

version: "3.7"
services:
  coredns:
    image: coredns/coredns
    container_name: coredns
    hostname: ns1.example.net
    command: -conf /etc/coredns/Corefile
    volumes:
      - ./corednsconfig:/etc/coredns
    restart: unless-stopped
    ports:
        - '10.0.0.1:53:53'
        - '10.0.0.1:53:53/udp'
        - '10.0.0.1:9153:9153'

For any additional hosts (secondary) you plan to transfer zones to you can also use the same compose file for that setup. Just modify the ip addresses and hostname for this service according to your needs.

Note: You will need to keep ports section, but binding it to an ip addres is optional. Also, if you want to use a specific version of CoreDNS, under the image you will need to add the tag of the version you want. Something like: image: coredns/coredns:1.8.3

  1. Next create a folder next to your docker-compose file to store your configuration and zone files. If you name it different then what is in the compose file, you will need to modify the volumes location to tell docker where the files are for the CoreDNS configuration.

  2. Change directory into the folder you just created.

  3. Use your favorite editor to create Corefile and add something like this:

.:53 {
    forward . 1.1.1.1 1.0.0.1
    log
    errors
    acl {
        allow net 10.0.0.0/24
        filter net 0.0.0.0/0
    }
}
example.com:53 {
    file /etc/coredns/db.example.com
    log
    errors
    acl {
        allow net 10.0.0.0/24
        filter net 0.0.0.0/0
    }
    transfer {
      to 10.0.0.2
    }
}

What each section is doing is this - the first section:

.:53 {
    forward . 1.1.1.1 1.0.0.1
    log
    errors
    acl {
        allow net 10.0.0.0/24
        filter net 0.0.0.0/0
    }
}

In this section, you are defining any request destined for the DNS server to be forwarded to the ip addresses of 1.1.1.1 or 1.0.0.1 (Cloudflare DNS). This can be anything like Quad9, OpenDNS, or Google. Next is log and errors section. These are optional but if you run a docker logs -f container this will help you see errors and logs of requests hitting the server. The acl is another optional item and it allows you define specific subnets that can or can’t query CoreDNS. You can see this in the allow net where block net or filter net will block the the request. A block results in a REFUSED result where a filter produces a NOERROR result. You can read more about CoreDNS acl plugin allow, block, and filter by visiting the plugin page.

The next section defines a dns zone:

example.com:53 {
    file /etc/coredns/db.example.com
    log
    errors
    acl {
        allow net 10.0.0.0/24
        filter net 0.0.0.0/0
    }
    transfer {
      to 10.0.0.2
    }
}

It it’s pretty much as the first block, but it’s looking for any request that matches example.com on port 53. You can add any zones you want and define ports to listen on. CoreDNS allows for things like DoT (TLS) and DoH (HTTP/2). Next is the file plugin that defines the zone file - you will create this shortly.

Again the log, errors, and acl sections accomplish the same thing as the initial block and are optional.

Any of these plugins and be defined any way you want for each ‘block’ so it is flexible with what you want to do per section.

If you plan to transfer a zone to another host, you will need the following section. This tells CoreDNS that this zone should be transfered to this CoreDNS server, you can add more ips as needed.

transfer {
      to 10.0.0.2
    }
  1. Save and close the file after you are done editing it.

  2. Next create a zone file such as db.example.com in the same folder as the Corefile.

  3. Edit the zone file and set it up to have a SOA (start of authority record) and other records you might need. At a minimum create it as such:

$ORIGIN example.com.
example.com.  3600   IN       SOA     ns1.example.com. webmaster.example.com. 2120051007 3600 600 604800 1800
; Serial is based on date UTC time

; Name Servers
example.com.  3600    IN     NS    ns1.example.com. ; CoreDNS
example.com.  3600    IN     NS    ns2.example.com. ; CoreDNS

; A Records
@               IN     A       10.0.0.20
ns1             IN     A       10.0.0.1
ns2             IN     A       10.0.0.2
mail            IN     A       10.0.0.100
; CNAME Records
www         IN     CNAME   example.com.

I won’t explain this file to in depth - I’ve provided a link with more information about Zone Files below, but essentially defines that any example.com record is authoritative at the ns1.exmaple.com host. Next is the email address - webmaster.example.com - this is the way the zone file defines - no @ here. After the email address is the serial that tracks changes. This number needs to change after each modification of the zone file. Most people use the YYYYMMDD format. I wanted to use YYYYMMDDHHmm but found that it was to long and CoreDNS would not load the zone properly and cause the container to crash. So I’ve opted for YYMMDDHHmm as I might make multiple changes a day I wanted to determine how to tick the serial forward without problems. It can be anything really, 1, 2, 3, 4, etc. but it should be something easy to remember the formating for.

The other number define the refresh, retry, expire, and minimum respectively (in seconds).

The ; Name Servers section defines the name servers within the zone. As you add or remove name servers, you can adjust those records here. I’m using two so I’ve defined those as ns1 and ns2.

Under the A Records, I’ve defined A (IPv4) records for both name servers as well as for the root domain (example.com) and mail.example.com. You can add any other records like CNAME, TXT, or MX records to this list also.

Once that file is complete, save it and exit your editor.

  1. Run:

    docker-compose pull

    to pull the CoreDNS image. This just downloads it, it doesn’t run the container yet.

  2. Now you can run your compose file to bring the whole thing up. You can do this by issuing the following command.

    docker-compose up -d

    This will run the compose file in a daemon mode so you can log off the machine without killing the container. If you just want to test it, you can leave the -d off the command.

12 . You should be able to run dig commands against this server now.

dig @10.0.0.1 www.google.com

dig @10.0.0.1 www.example.com

You can see logs of the CoreDNS container to see information about transfers and request handling by running:

docker logs containername or docker logs -f containername

Secondary Hosts

If you intend to run zone transfers to other CoreDNS servers you will want to follow the above instructions on the secondary host (with the exception of defining the file plugin in the zone (ex. example.com) you want transfered and not creating the db.exmaple.com file for the zone). The key thing is in the Corefile on the secondary hosts to get the zone defined on your setup. We will want to define the following in the Corefile:

}
example.com:53 {
    log
    errors
    acl {
        allow net 10.0.0.0/24
        filter net 0.0.0.0/0
    }
    secondary {
        transfer from 10.0.0.1
    }
}

You will want to define secondary rather than the transfer plugin and enter the ip address of the primary dns name server.

Note: On your secondary servers, you do not need to create a zone file as it is stored in memory. If the server is rebooted, it will wait for a transfer from the primary. Read about that on the secondary plugin page.

Resolving DNS on the host running CoreDNS:

In my environment I wanted the host that was running CoreDNS to use itself to retrieve records and forward on my requests to Cloudflare for anything else. In Ubuntu I first modified the following file: /etc/systemd/resolved.conf so that the following line was changed to the localhost.

DNS=127.0.0.1

I then modified my netplan configuration (as I’m running Ubuntu 20.04 server) the file for that is: /etc/netplan/01-netcfg.yaml. Under the nameservers: section I changed the following:

      nameservers:
          search: [ example.com ]
          addresses:
              - "172.0.0.1"
              - "10.0.0.2"

Reboot after these changes - your CoreDNS container should automatically restart if using the ‘unless-stopped’ option in the container restart policy. This will use the localhost and the secondary server to query for DNS lookups.

Best of luck!

Resources: