Run your own NameServers with CoreDNS

I have a lot of domains and I wanted an easy way to manage all of them without having to deal with some kind of API from a third party. For learning purposes I decided to give CoreDNS a try. All of my domains are for my own hobby projects and I decided to try running my own nameservers to learn. There are plenty of free options available and it is not recommended to run your own nameservers.

The things I will be using in this guide are mainly:

  • CoreDNS
  • Ansible
  • Hetzner cloud – instances are in Finland, costs around 6 euro per month
  • Namesilo (affiliate link, use code GOPHP to get $1 off ) – for registering our nameservers

Hosting

I went ahead and deployed 2 Hetzner cloud instances since they are cheap and easy to deploy. I am also based in Europe which puts the NameServers close to me which is good for me, not so good for my US users. You could also use AWS EC2 or Google Cloud Compute instances or anything else you prefer.

Domains

I wanted a super simple way to manage my domains so I decided to make use of Ansible to deploy my NameServers. This allows me to keep track of all configurations in one big YAML file. Here is an example of how I have defined my domains.

---
# roles/common/vars/main.yml
domains:
  - domain: 'uberswe.com'
    mx:
      - '10 in1-smtp.messagingengine.com'
      - '20 in2-smtp.messagingengine.com'
    txt:
      - 'v=spf1 include:spf.messagingengine.com ?all'
    sub:
      - domain: 'fm1._domainkey'
        cname:
          - 'fm1.uberswe.com.dkim.fmhosted.com'
      - domain: 'fm2._domainkey'
        cname:
          - 'fm2.uberswe.com.dkim.fmhosted.com'
      - domain: 'fm3._domainkey'
        cname:
          - 'fm3.uberswe.com.dkim.fmhosted.com'
  - # bokföring.xyz
    domain: 'xn--bokfring-q4a.xyz'
  - # bolån.xyz
    domain: 'xn--boln-soa.xyz'

Here we have a variable called domains with a list of objects. Each object needs to specify a domain and the rest is optional.

CoreDNS Configuration

I can use the domains variable to create dns zone files and a CoreDNS configuration file which our dns server will read. We can use a Jinja template to make our configuration file. Below is the template I made.

.:53 {
    forward . 8.8.8.8
    log
    errors
    cache
}

{% for d in domains %}
{{d.domain}} {% if 'sub' in d %}{% for s in d.sub %}{{s.domain}}.{{d.domain}} {% endfor %}{% endif %}{
    file /etc/coredns/zones/{{d.domain}}.db
    log
    cache
    errors
}

{% endfor %}

Our nameservers will forward any requests it doesn’t match to 8.8.8.8 as a fallback. This file will take the domains list we defined in our YAML file and loop through each domain object specifying the db file for each domain.

I decided to use files as CoreDNS has a file plugin which seemed easy to use. I would like to try using the redis plugin in the future to load zone data from a redis instance. Below is my Jinja file which takes each object in the YAML file above and uses it to generate a dns zone file.

$ORIGIN {{ item.domain }}.
@	3600 IN	SOA ns1.beubo.com. admin.beubo.com. 2017042745 7200 3600 1209600 3600
	3600 IN NS ns1.beubo.com.
	3600 IN NS ns2.beubo.com.
        IN A	95.216.12.246
        IN AAAA	2a01:4f9:2a:d09::2
{% if 'mx' in item %}{% for mx in item.mx %}        IN MX  {{mx}}.
{% endfor %}{% endif %}
{% if 'txt' in item %}{% for txt in item.txt %}        IN TXT  "{{txt}}"
{% endfor %}{% endif %}
{% if 'cname' in item %}{% for cname in item.cname %}        IN CNAME  {{cname}}
{% endfor %}{% endif %}

*       IN A     95.216.12.246
        IN AAAA  2a01:4f9:2a:d09::2

{% if 'sub' in item %}{% for d in item.sub %}
{% if 'ipv4' in d %}{{d.domain}}       IN A  {{d.ipv4}}
{% endif %}
{% if 'ipv6' in d %}{{d.domain}}       IN AAAA  {{d.ipv6}}
{% endif %}
{% if 'mx' in d %}{% for mx in d.mx %}{{d.domain}}        IN MX  {{mx}}.
{% endfor %}{% endif %}
{% if 'txt' in d %}{% for txt in d.txt %}{{d.domain}}        IN TXT  "{{txt}}"
{% endfor %}{% endif %}
{% if 'cname' in d %}{% for cname in d.cname %}{{d.domain}}        IN CNAME  {{cname}}.
{% endfor %}{% endif %}

{% endfor %}{% endif %}

Currently the nameservers, SOA record and IP addresses for the root domains are hardcoded but could easily be replaced with variables. The basic idea here is that I check if an item has a key then I echo the value. I also added the sub item which contains any sub domains so I use a loop for that item. Here is an example zone file which I have generated for my dns server.

$ORIGIN uberswe.com.
@	3600 IN	SOA ns1.beubo.com. admin.beubo.com. 2017042745 7200 3600 1209600 3600
	3600 IN NS ns1.beubo.com.
	3600 IN NS ns2.beubo.com.
        IN A	95.216.12.246
        IN AAAA	2a01:4f9:2a:d09::2
        IN MX  10 in1-smtp.messagingengine.com.
        IN MX  20 in2-smtp.messagingengine.com.
        IN TXT  "v=spf1 include:spf.messagingengine.com ?all"

*       IN A     95.216.12.246
        IN AAAA  2a01:4f9:2a:d09::2

fm1._domainkey        IN CNAME  fm1.uberswe.com.dkim.fmhosted.com.

fm2._domainkey        IN CNAME  fm2.uberswe.com.dkim.fmhosted.com.

fm3._domainkey        IN CNAME  fm3.uberswe.com.dkim.fmhosted.com.

We can write an ansible test to ensure that this is working as it should before we try to run this on our nameservers. Below is my test file.

---
# tests/test.yml
- hosts: 127.0.0.1
  vars_files:
    - ../roles/common/vars/main.yml
  tasks:
    - name: Create a directory
      file:
        path: zones
        state: directory
      delegate_to: localhost

    - name: Test dns zone files generation
      template: src=../roles/dnstier/templates/dnszone.db.j2 dest=zones/{{ item.domain }}.db
      with_items: "{{ domains }}"
    
    - name: Create a directory
      file:
        path: files
        state: directory
      delegate_to: localhost

    - name: Test Corefile for coredns generation
      template: src=../roles/dnstier/templates/Corefile.j2 dest=files/Corefile

You can run this with ansible-playbook tests/dnszones.yml and it should make a folder called zones which will contain a .db file for each domain. A files directory with the Corefile will also be created.

Deployment Configuration

To setup and configure CoreDNS I found ansible role which was already ready to use at https://github.com/cloudalchemy/ansible-coredns. We can install this role using the command ansible-galaxy install cloudalchemy.coredns.

We need to generate the zone files locally so I created a role called dnstier which will prepare these files using the following task file.

---
# file: roles/dnstier/tasks/main.yml
- name: Create a directory
  file:
    path: /tmp/zones
    state: directory
  delegate_to: localhost

- name: dns zone files generation
  template: src=../templates/dnszone.db.j2 dest=/tmp/zones/{{ item.domain }}.db
  with_items: "{{ domains }}"
  delegate_to: localhost

Now we can set up our main playbook which will run both of these roles.

---
# file: dnsservers.yml
- hosts: dnsservers
  roles:
    - dnstier
    - cloudalchemy.coredns

We also need to define our variables for the cloudalchemy.coredns role. This is how I defined them.

---
# file: group_vars/all.yml

coredns_config_file: "roles/dnstier/templates/Corefile.j2"

coredns_zone_files_paths:
  - "/tmp/zones/*"

coredns_system_group: "coredns"
coredns_system_user: "{{ coredns_system_group }}"

The variables I have defined here come from the readme of the coredns role we installed with galaxy. The config file is the path to the template file we defined earlier and the zone file path is the same as we defined in our dnstier task. I also have a hosts file defined like so.

---
# host_vars/all.yml
all:
  children:
    dnsservers:
      hosts:
        beubo:
        ns2beubo:

Here beubo and ns2beubo are the host names I have defined in my ssh config for my two nameservers. We could add more hosts here and ansible will deploy this same setup to each one.

Deploying the Nameservers

Now we can finally go ahead and deploy using the command ansible-playbook dnsservers.yml which will:

  • Run the dnstier role, generating the zone files.
  • Run the cloudalchemy.coredns role which installs our coredns server .

After the deployment is done we can check that it is working by specifying the IP of each dns server with a @ symbol in the dig command. You can run this for example to query my domain and server dig @95.216.12.246 uberswe.com

We can now register our nameservers. I am doing this with Namesilo but most registrars should provide an easy way of doing this. I will enter ns1 and ns2 as the prefixes and enter a ipv4 and ipv6 address for each nameserver. You can find the ip addresses on your Hetzner cloud instance page.

Namesilo page to add a new registered nameserver.

Once you have done that and waited some time for the dns to propagate you can query a domain using your server. We can replace the ip in our previous dig query with the domain like so dig @ns1.beubo.com uberswe.com.

If that is working like it should, you should then be ready to point your domains to your new nameservers. Feel free to leave a comment if you have questions, see something I did wrong or just want to wish me a good day.

If this stuff interests you, you may be interested in some relevant topics I found while setting up my nameservers:

How Switching Domain Registrars Saved Me $300+/Year

When registering a domain it’s typically easy to find domain registrars with good deals, perhaps just $1 to get any domain name you want. That’s a trick to reel you in in the hopes that you will keep that domain for many years without switching registrar. The renewal fees and any additional offerings such as hosting and email is where most domain registrars earn the most money. Today, however, I want to simply take a look at renewal fees and how much extra we spend on renewals. If you are a company or someone like me with 100+ domains these renewal fees can get very expensive. Here I will show how much you can actually save on renewal fees by switching domain registrars and how I went about figuring this out.

Continue reading “How Switching Domain Registrars Saved Me $300+/Year”