Skip to content

Creating TSIG keys with ansible

I needed to configure some hosts to do nsupdates to my authoritative DNS master server, running Bind9.

It can easily be done with the rndc-confgen utility, which output a configfile with a nicley formatted key in it.

But my usecase is different, I need to create a key for each host to use with Letsencrypt and certbot woth the rfc2136 plugin.

So I need to create a key, and save with some other stuff into various files.

  • Some Bind9 config
  • Some rfc2136 configfile for certbot

Ansible implementation

I want to implement it in Ansible, so I can easily recreate my hosts, or add another host to the environment.

Generating TSIG keys

- name: Generate unique HMAC-SHA512 TSIG key for each host (seed is the host name + more entropy)
    ansible.builtin.set_fact:
    tsig_key: "{{ lookup('ansible.builtin.password', '/dev/null chars=hexdigits seed={{ tsig_key_seed }}{{ ansible_hostname  }} length=64') | b64encode }}"

The ansible.builtin.password lookup plugin gives random-but-idempotent output, when the filename is set to /dev/null when given the same seed. To distinguish the keys from host to host, I give the hostname as seed. I also give some secret string to add to the seed, so noone out there, can guess, that the seed its the hostname, and hence, recreate my TSIG keys. The variable tsig_key_seed is configured in group_vars in my infrastructure repository.

This gives me a string with a valid TSIG key to use in templates to write eg. named.conf.options or the rfc2136.ini file for certbot.

Also, this task is so cheap, that I really dont care to save it somewhere between runs, I just include the task, whenever I need the key. I could maybe even put it directly into the templates, and save me from running this extra task in the playbook.

Templates for Bind9

Bind9 needs to be aware of these keys, for it to validate the keys, when certbot tries to add a DNS RR for the validation process. For this to happen, I template a file to the master DNS server, and include it in the Bind9 configuration.

I use this task in ansible to create the TSIG configfile:

- name: Create TSIG config file
    ansible.builtin.copy:
    content: |
        {% for host in groups["all"] %}
        key "rndc-key-{{ host }}" {
            algorithm hmac-sha512;
            secret "{{ hostvars[host]['tsig_key'] }}";
        };

        {% endfor %}
    dest: /etc/bind/tsig-keys.conf
    owner: root
    group: bind
    mode: '0640'
    loop: "{{ groups['dns'] }}"
    delegate_to: "{{ item }}"
    run_once: true

And then include /etc/bind/tsig-keys.conf in the Bind9 configuration:

include "/etc/bind/tsig-keys.conf";

Lastly, reload Bind9, to pickup the new keys and configurations.

Templates for Certbot

I use this template with Ansible to create the rfc2136 ini configuration file for Certbot:

# Target DNS server (IPv4 or IPv6 address, not a hostname)
dns_rfc2136_server = {{ hostvars['masterdnsserver']['ansible_default_ipv4']['address'] }}
# Target DNS port
dns_rfc2136_port = 53
# TSIG key name
dns_rfc2136_name = rndc-key-{{ ansible_hostname}}
# TSIG key secret
dns_rfc2136_secret = {{ tsig_key }}
# TSIG key algorithm
dns_rfc2136_algorithm = HMAC-SHA512
# TSIG sign SOA query (optional, default: false)
dns_rfc2136_sign_query = false

Then I create a small shell script wrapper to use with the Systemd units I used to time the certificate renewals:

#!/bin/bash

# Halt if something goes wrong
set -euxo pipefail

# Check if the domain is valid
if [ -z "$1" ]; then
    echo "Usage: $0 <comma seperated list of domains>"
    exit 1
fi

# Renew certificates
/opt/certbot/bin/certbot \
    certonly \
    {% if short_lived_environment %}--test-cert \
{% endif %}
    --expand \
    -c /opt/certbot/certbot_web_certificates.ini \
    -d $1

The file /opt/certbot/certbot_web_certificates.ini contains my personal information, and is not relevant for this how-to.