HomeLab Tutorials – Part 1 – Creating a DNS Server

Introduction

This series of tutorials will focus on the process of configuring a free(ish) homelab environment. No need for licensing expensive server operating systems from Microsoft. Everything is going to be opensource. In this first installment, we will focus on standing up a local DNS provider. DNS is the tool which will allow our homelab devices to talk with each other using hostnames instead of IP Addresses. This is in turn allow us to not spend extra time configuring our /etc/hosts file every time we stand up a new device. As with my other tutorials, I am using proxmox as my hypervisor (although eventually I will likely migrate over to AWS for some of my homelab).

Assumptions

  1. CentOS 8 Server with default configuration.
  2. 2 GB of Ram allocated to the server.
  3. 1 Dedicated Cores allocated to the server.
  4. 25 GB of storage space allocated to the server.
  5. SSH enabled on the server.

Step 1 – Configuring Local Network

I have named my DNS server DNS.brooksnet.lan. Begin by SSH’ing into your server like so:

$ ssh root@*Enter your current IP Address here*

Then, run the below command to update your dns server.

$ yum update -y

Once this update process finishes, we will be ready to configure our network. We start by setting a a static IP address for our DNS Server. Our DNS server will be the only machine on our network which will be referenced using a static IP:

$ nmcli connection modify ens18 ipv4.method manual
$ nmcli connection modify ens18 ipv4.addresses 10.240.245.245/16
$ nmcli connection modify ens18 ipv4.gateway 10.240.240.1
$ nmcli connection modify ens18 ipv4.dns 8.8.8.8,1.1.1.1

The first command sets the ipv4 addressing method to manual instead of using DHCP from the router. The second command sets the IP Address to a static address and gives the same IP address a NETMASK of 16. A “/16” netmask means that this host will be able to communicate with devices within the IP range 10.240.XXX.XXX. Finally, the last two commands sets the gateway address of this device to the router of the local network and provides an external DNS server for this local DNS server to communicate with. Functionally, these steps allows our DNS server to talk to the outside world. For all of these commands, note the name of the modified device “ens18”. This device may not exist on your server. To find the name of your network device, from your terminal, run the following command:

$ ip a

You should see a fairly short output list. We are looking for the device on that output list which is currently reporting “<BROADCAST,MULTICAST,UP,LOWER_UP>”. This device is usually the active network device. Below is what my “ip a” output looks like:

$ ip a

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether f2:55:7b:ea:d5:61 brd ff:ff:ff:ff:ff:ff
    inet 10.240.244.112/16 brd 10.240.255.255 scope global dynamic noprefixroute ens18
       valid_lft 5955sec preferred_lft 5955sec
    inet6 fe80::9a2b:f481:fdb5:6790/64 scope link noprefixroute
       valid_lft forever preferred_lft forever

Finally, to make these changes permanent, run the following command (you will loose your ssh connection when you run this command):

$ nmcli con up ens18.

Once this command runs, reconnect to your dns server, using the new IP address which you set earlier (in my case, 10.240.245.245).

$ ssh root@10.240.245.245

Now, lets rerun the “ip a” command and see what has changed:

$ ip a

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether f2:55:7b:ea:d5:61 brd ff:ff:ff:ff:ff:ff
    inet 10.240.245.245/16 brd 10.240.255.255 scope global noprefixroute ens18
       valid_lft forever preferred_lft forever
    inet6 fe80::9a2b:f481:fdb5:6790/64 scope link noprefixroute
       valid_lft forever preferred_lft forever

Note, the inet of ens18 has now changed to 10.240.245.245 and the brd is now 10.240.255.255. Everything appears to be configured correctly. For our final test, we will try talking to the internet. To do so, perform the following command:

$ wget www.google.com

This command does two things for us. First, it confirms that our server can talk to the outside world (by talking to Google’s web server, and it confirms that external DNS is working by using the domain name www.google.com, instead of 142.250.68.68 (the load balancer I hit when I ping www.google.com). If you receive the following output when you run the above command, then the first half of our networking configuration is complete:

$ wget www.google.com

--2020-11-21 06:13:36--  http://www.google.com/
Resolving www.google.com (www.google.com)... 142.250.68.36, 2607:f8b0:4007:816::2004
Connecting to www.google.com (www.google.com)|142.250.68.36|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: ‘index.html’

index.html                             [ <=>                                                             ]  12.65K  --.-KB/s    in 0s

2020-11-21 06:13:37 (419 MB/s) - ‘index.html’ saved [12950]

[root@dns ~]# nmcli connection modify ens18 ipv4.dns 8.8.8.8,1.1.1.1
[root@dns ~]# nmcli connection up ens18
Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/4)
[root@dns ~]# wget www.google.com
--2020-11-21 06:14:12--  http://www.google.com/
Resolving www.google.com (www.google.com)... 142.250.68.100, 2607:f8b0:4007:802::2004
Connecting to www.google.com (www.google.com)|142.250.68.100|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: ‘index.html.1’

index.html.1                           [ <=>                                                             ]  12.62K  --.-KB/s    in 0.001s

Now that our server is able to correctly talk to the outside world, we are ready to set up our Firewall Rules to allow our DNS server to talk to our networked devices. This is a very straight-forward process. Simply run the commands below:

$ systemctl enable --now firewalld
$ firewall-cmd --permanent--add-service=dns
$ firewall-cmd --reload 

The first command makes sure that Firewalld (the default firewall service in CentOS) is enabled and running. The second command configures rules for DNS traffic to and from the server through firewalld. The final command reloads the firewalld configuration and begins the allow rule for dns traffic.

Step 2 – Installing and Configuring BIND DNS

We will be using BIND (Berkeley Internet Name Daemon) as our DNS Service on our dns server. To install the BIND service on the server, run the following command:

$ yum install bind bind-utils -y

Once the installation completes, we are ready to begin configuring BIND. using your favorite text editor (Mine is VIM) modify the /etc/named.conf file. Here is what the name.d.conf file looks like by default:

options {
        listen-on port 53 { 127.0.0.1; };
        listen-on-v6 port 53 { ::1; };
        directory       "/var/named";
        dump-file       "/var/named/data/cache_dump.db";
        statistics-file "/var/named/data/named_stats.txt";
        memstatistics-file "/var/named/data/named_mem_stats.txt";
        secroots-file   "/var/named/data/named.secroots";
        recursing-file  "/var/named/data/named.recursing";
        allow-query     { localhost; };

        recursion yes;

        dnssec-enable yes;
        dnssec-validation yes;

        managed-keys-directory "/var/named/dynamic";

        pid-file "/run/named/named.pid";
        session-keyfile "/run/named/session.key";

        /* https://fedoraproject.org/wiki/Changes/CryptoPolicy */
        include "/etc/crypto-policies/back-ends/bind.config";
};

logging {
        channel default_debug {
                file "data/named.run";
                severity dynamic;
        };
};

zone "." IN {
        type hint;
        file "named.ca";
};

include "/etc/named.rfc1912.zones";
include "/etc/named.root.key";

We are going to modify this script to change how name.d listens and routes traffic, in accordance with the IP address settings we configured in Step 1. First, change the “listen-on port 53” line to use the IP address we set previously (10.240.245.245). Then, change the “allow-query” line to allow requests across our home-lab IP space (10.240.0.0/16). Finally, add a new include line at the bottom of the file with the following, ‘ include “/etc/named/named.conf.local”; ‘ (not including the single quotes). Your named.conf file should now look like the output below:

options {
        listen-on port 53 { 10.240.245.245; };
        listen-on-v6 port 53 { ::1; };
        directory       "/var/named";
        dump-file       "/var/named/data/cache_dump.db";
        statistics-file "/var/named/data/named_stats.txt";
        memstatistics-file "/var/named/data/named_mem_stats.txt";
        secroots-file   "/var/named/data/named.secroots";
        recursing-file  "/var/named/data/named.recursing";
        allow-query     { 10.240.0.0/16; };

        recursion yes;

        dnssec-enable yes;
        dnssec-validation yes;

        managed-keys-directory "/var/named/dynamic";

        pid-file "/run/named/named.pid";
        session-keyfile "/run/named/session.key";

        /* https://fedoraproject.org/wiki/Changes/CryptoPolicy */
        include "/etc/crypto-policies/back-ends/bind.config";
};

logging {
        channel default_debug {
                file "data/named.run";
                severity dynamic;
        };
};

zone "." IN {
        type hint;
        file "named.ca";
};

include "/etc/named.rfc1912.zones";
include "/etc/named.root.key";
include "/etc/named/named.conf.local";

Next, we are going to go configure that named.conf.local file we included at the end of our name.d configuration file. I created mine with the following contents using VIM:

zone "brooksnet.lan" {
        type master;
        file "/etc/named/zones/brooksnet.lan";
};

zone "240.10.in-addr.arpa" {
        type master;
        file "/etc/named/zones/db.10.240";
};

The first block configures a file which holds dns records for devices on the brooksnet.lan domain. The second block configures a file which holds our reverse lookup table for the IP address range which this DNS server is responsible (refer to this article for more information about reverse lookup tables).

Now that we have configured a location for our records to be maintained, we need that location to be accessible by BIND services. To do so we first have to make the directory with the following command:

$ mkdir /etc/named/zones

Next, we need to create the /etc/named/zones/brooksnet.lan dns record keeping table. Here is what mine looks like:

$ vim /etc/named/zones/brooksnet.lan

$TTL 604800
@       IN      SOA     brooksnet.lan. littleseneca.brooksnet.lan. (
        1               ; Serial
        604800          ; Refresh
        86400           ; Retry
        2419200         ; Expire
        604800 )        ; Negative Cache TTL

; NS records
@       IN      NS      dns.brooksnet.lan.

; NS A records
dns.brooksnet.lan.          IN      A       10.240.245.245

; Other Hosts A records
shiloh.brooksnet.lan.       IN      A       10.240.245.100
jordan.brooksnet.lan.       IN      A       10.240.245.200

For a reference of what everything in this configuration does and how the syntax works, refer to this excellent documentation from serverlab.ca. For now, just know that you will need to replace the set IP addresses with your own, and change the hostnames I have set to your own. Note, I have include DNS records for two other servers on my homelab domain. The first (shiloh.brooksnet.lan) is the hostname of my proxmox hypervisor. The other (jordan.brooksnet.lan) does not exist yet. This will be my domain controller. I am aware of “cattle not pet” naming conventions. But, this is a home lab and I so I’m going to do what I want.

Next, we are going to configure the reverse zone file, which we will create at /etc/named/zones/db.10.240:

vim /etc/named/zones/db.10.240

$TTL 604800
@       IN      SOA     brooksnet.lan. root.brooksnet.lan. (
        1               ; Serial
        604800          ; Refresh
        86400           ; Retry
        2419200         ; Expire
        604800 )        ; Negative Cache TTL
; name servers
@       IN      NS      dns.brooksnet.lan.

; PTR Records
2    IN      PTR     dns.brooksnet.lan.     ; 10.240.245.245
3    IN      PTR     shiloh.brooksnet.lan. ; 10.240.245.100
4    IN      PTR     jordan.brooksnet.lan. ; 10.240.245.200

Again, change the IP Addresses and Hostnames to match your needs. Now that both the forward and reverse tables are configured, we will check our configurations for errors, using the command below:

$  named-checkconf -z

zone localhost.localdomain/IN: loaded serial 0
zone localhost/IN: loaded serial 0
zone 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa/IN: loaded serial 0
zone 1.0.0.127.in-addr.arpa/IN: loaded serial 0
zone 0.in-addr.arpa/IN: loaded serial 0
zone brooksnet.lan/IN: loaded serial 1
zone 240.10.in-addr.arpa/IN: loaded serial 1

If the command returned no errors, then the command was successful. The above output shows that the named service was able to check our zone files without finding any errors. We can confirm this by running the two commands below:

$ named-checkzone brooksnet.lan /etc/named/zones/brooksnet.lan

zone brooksnet.lan/IN: loaded serial 1
OK

$ named-checkzone db.10.240 /etc/named/zones/db.10.240

zone db.10.240/IN: loaded serial 1
OK

Both named-checkzone commands came back with an “OK” output, so the configuration files are in order. Next, we need to enable the named service so that these configurations can be put into use!

systemctl enable --now named.service

To test that your configuration was successful, simply change the DNS provider on any of your machines and set them to the IP address of the DNS server we just configured. If you are able to reach any of the devices listed in the DNS record by their hostname instead of their IP Address, then the configuration was successful! If you found this tutorial useful, please do consider leaving a comment below so I know you read this far.

Leave a Comment