Set up Bind server with Ansible



  • Here is a role to configure and build Bind on CentOS 7. When I clone a machine, I grab the new MAC and add the reservation before I spin it up. Then in the inventory file, Ansible connects based on IP address and have a variable set up for the host name I want for the machine.

    Here's the machine in our inventory

    10.1.30.11   host_name=ns1.pa.jhbcomputers.com
    

    Here's the role structure:

    .
    ├── defaults
    │   └── main.yml
    ├── handlers
    │   └── main.yml
    ├── meta
    │   └── main.yml
    ├── README.md
    ├── tasks
    │   └── main.yml
    ├── templates
    │   ├── db.forward.j2
    │   ├── db.reverse.j2
    │   ├── named.conf.j2
    │   └── named.conf.local.j2
    ├── tests
    │   ├── inventory
    │   └── test.yml
    └── vars
        └── main.yml
    

    Here's our main tasks.yml file:

    ---
    # tasks file for Bind setup
    - name: Install bind
      yum:
        pkg: bind
        state: installed
    
    - name: Set hostname
      hostname:
        name: {{ host_name }}
    
    - name: Set hostname fact
      set_fact:
        ansible_fqdn: {{ host_name }}
    
    - name: Copy named conf file
      template:
        src: named.conf.j2
        dest: /etc/named.conf
        owner: root
        group: named
        mode: 0660
      notify: restart named
    
    - name: Make named directory
      file:
        path: /etc/named
        state: directory
        owner: root
        group: named
        mode: 0750
    
    - name: Copy named conf local file
      template:
        src: named.conf.local.j2
        dest: /etc/named/named.conf.local
        owner: root
        group: named
        mode: 0640
      notify: restart named
    
    
    - name: Make zones Directory
      file:
        path: /etc/named/zones
        state: directory
        owner: root
        group: named
        mode: 0750
    
    - name: Copy forward file
      template:
        src: db.forward.j2
        dest: /etc/named/zones/db.{{ domain }}
        owner: root
        group: named
        mode: 0640
      notify: restart named
    
    
    - name: Copy reverse file
      template:
        src: db.reverse.j2
        dest: /etc/named/zones/db.{{ rev_domain }}
        owner: root
        group: named
        mode: 0640
      notify: restart named
    
    - name: Open firewall port
      firewalld:
        service: dns
        permanent: true
        state: enabled
        immediate: yes
    

    db.forward.j2:

    $TTL 604800	; 1 week
    {{ domain }}	IN SOA	{{ ansible_fqdn }}. admin.{{ domain }}. (
    				8          ; serial
    				604800     ; refresh (1 week)
    				86400      ; retry (1 day)
    				2419200    ; expire (4 weeks)
    				604800     ; minimum (1 week)
    				)
    			NS	{{ ansible_fqdn }}.
    $ORIGIN {{ domain }}.
    $TTL 300	; 5 minutes
    $TTL 604800	; 1 week
    erl	 A 10.1.30.1
    ansible  A 10.1.30.5
    ns1      A 10.1.30.11
    

    db.reverse.j2:

    $ORIGIN .
    $TTL 604800	; 1 week
    {{ rev_domain }}	IN SOA	{{ ansible_fqdn }}. admin.{{ domain }}. (
    				7          ; serial
    				604800     ; refresh (1 week)
    				86400      ; retry (1 day)
    				2419200    ; expire (4 weeks)
    				604800     ; minimum (1 week)
    				)
    			NS	{{ ansible_fqdn }}.
    $ORIGIN {{ rev_domain }}.
    $TTL 604800	; 1 week
    1     PTR erl.30.1.10.in-addr.arpa.
    5     PTR ansible.30.1.10.in-addr.arpa.
    11    PTR ns1.30.1.10.in-addr.arpa.
    

    named.conf.j2:

    //
    // named.conf
    //
    // Provided by Red Hat bind package to configure the ISC BIND named(8) DNS
    
    
    // server as a caching only nameserver (as a localhost DNS resolver only).
    //
    // See /usr/share/doc/bind*/sample/ for example named configuration files.
    //
    
    include "/etc/rndc.key";
    
    
    options {
    	listen-on port 53 { 127.0.0.1; {{ ansible_eth0.ipv4.address }}; };
    #	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";
    	allow-query     { any; };
    
    	/*
    	 - If you are building an AUTHORITATIVE DNS server, do NOT enable recursion.
    	 - If you are building a RECURSIVE (caching) DNS server, you need to enable
    	   recursion.
    	 - If your recursive DNS server has a public IP address, you MUST enable access
    	   control to limit queries to your legitimate users. Failing to do so will
    	   cause your server to become part of large scale DNS amplification
    	   attacks. Implementing BCP38 within your network would greatly
    	   reduce such attack surface
    	*/
    	recursion yes;
    
    	dnssec-enable yes;
    	dnssec-validation yes;
    
            forwarders {
                   8.8.8.8;
            };
    
    	/* Path to ISC DLV key */
    	bindkeys-file "/etc/named.iscdlv.key";
    
    	managed-keys-directory "/var/named/dynamic";
    
    	pid-file "/run/named/named.pid";
    	session-keyfile "/run/named/session.key";
    };
    
    logging {
            channel default_debug {
                    file "data/named.run";
                    severity dynamic;
            };
    };
    
    zone "." IN {
    	type hint;
    	file "named.ca";
    };
    
    include "/etc/named/named.conf.local";
    

    named.conf.local.j2:

    zone "{{ domain }}" IN {
        type master;
        file "/etc/named/zones/db.{{ domain }}";
        allow-update { key rndc-key; };
    };
    
    zone "{{ rev_domain }}" IN {
        type master;
        file "/etc/named/zones/db.{{ rev_domain }}";
        allow-update {key rndc-key; };
    };
    
    controls {
         inet 127.0.0.1 port 953
             allow { 127.0.0.1; } keys { "rndc-key"; };
    };
    

    Here's the single simple handler main.yml:

    ---
    # handlers file for Bind setup
    - name: restart named
      service:
        name: named
        state: restarted
    

    And the vars main.yml file:

    ---
    # vars file for Bind setup
    domain: pa.jhbcomputers.com
    
    rev_domain: 30.1.10.in-addr.arpa
    

    Then you can call the role from your playbook:

    ---
    - name: Set up Bind
      hosts: 10.1.30.11
      gather_facts: true
      user: jhooks
      become: true
    
      roles:
        - bind
    


  • For this to be truly modular, I would have to make variables for all of the options in each template file, but since this is pretty much just for me I didn't worry about it.



  • I may also set up a yaml dictionary for the actual DNS entries. This way everything is in one place vs editing different files.



  • So if you would want to use a YAML dictionary for your records here's a sample that goes in vars.yml:

    records:
      ns1:
        forward: 10.1.30.11
        type: A
        last: 11
        rev: 30.1.10.in-addr.arpa.
      ansible:
        forward: 10.1.30.5
        type: A
        last: 5
        rev: 30.1.10.in-addr.arpa.
    

    Then in your templates you would have this:

    reverse:

    $ORIGIN .
    $TTL 604800	; 1 week
    {{ rev_domain }}	IN SOA	{{ ansible_fqdn }}. admin.{{ domain }}. (
    				7          ; serial
    				604800     ; refresh (1 week)
    				86400      ; retry (1 day)
    				2419200    ; expire (4 weeks)
    				604800     ; minimum (1 week)
    				)
    			NS	{{ ansible_fqdn }}.
    $ORIGIN {{ rev_domain }}.
    $TTL 604800	; 1 week
    {% for key, value in records.iteritems() %}
    {{ value.last }}   PTR   {{ value.rev }}
    {% endfor %}
    

    forward:

    $ORIGIN .
    $TTL 604800	; 1 week
    {{ domain }}	IN SOA	{{ ansible_fqdn }}. admin.{{ domain }}. (
    				8          ; serial
    				604800     ; refresh (1 week)
    				86400      ; retry (1 day)
    				2419200    ; expire (4 weeks)
    				604800     ; minimum (1 week)
    				)
    			NS	{{ ansible_fqdn }}.
    $ORIGIN {{ domain }}.
    $TTL 300	; 5 minutes
    $TTL 604800	; 1 week
    
    {% for key, value in records.iteritems() %}
    {{ key }}   {{ value.type }}   {{ value.forward }}
    {% endfor %}
    

    And then the tasks would look like this:

    - name: Copy forward file
      template:
        src: db.forward.j2
        dest: /etc/named/zones/db.{{ domain }}
        owner: root
        group: named
        mode: 0640
      with_dict: "{{ records }}"
      notify: restart named
    
    
    - name: Copy reverse file
      template:
        src: db.reverse.j2
        dest: /etc/named/zones/db.{{ rev_domain }}
        owner: root
        group: named
        mode: 0640
      with_dict: "{{ records }}"
      notify: restart named
    


  • Nice, moving DNS to a YAML file and making it platform agnostic is awesome.



  • So I also realized that if you have even a small number of records, a dictionary will become super long. So you can compact the dictionary like this:

    records:
      ns1: {forward: 10.1.30.11, type: A, last: 11, rev: 30.1.10.in-addr.arpa.}
      ansible: {forward: 10.1.30.5, type: A, last: 5, rev: 30.1.10.in-addr.arpa.}
    

    So while the other way may be easier to read, this saves a TON of space.