DNS Configuration

Correct DNS configuration is crucial

Many DebOps roles use the ansible_fqdn and ansible_domain variables to create correct default values. It's recommended that all hosts which are managed via DebOps have proper DNS entries, which means that they should be resolvable via DNS by their Fully Qualified Domain Name (hostname + domain name). The FQDN doesn't have to be accessible from the Internet when the hosts are on a private network, but it should be possible to resolve the FQDNS internally. This can be achieved e.g. by selecting a subdomain of your main DNS domain and configure the DNS servers to advertise the subdomain on your private subnet(s).

Default DNS Names

Some roles use default DNS FQDNs to provide various services (e.g. a web interface). These default FQDNs are listed below as a help in preparing a DNS zone for your local setup:

Role

Variable

Default

Example

debops.apt_cacher_ng

apt_cacher_ng__fqdn

software-cache. + {{ ansible_domain }}

software-cache.example.com

debops.docker_registry

docker_registry__fqdn

registry. + docker_registry__domain

registry.example.com

debops.dokuwiki

dokuwiki__fqdn

wiki. + {{ ansible_domain }}

wiki.example.com

debops.etesync

etesync__fqdn

etesync. + etesync__domain

etesync.example.com

debops.gitlab

gitlab__fqdn

code. + gitlab__domain

code.example.com

debops.icinga_web

icinga_web__fqdn

icinga. + icinga_web__domain

icinga.example.com

debops.kibana

kibana__fqdn

kibana. + kibana__domain

kibana.example.com

debops.librenms

librenms__fqdn

nms. + librenms__domain

nms.example.com

debops.lxc

lxc__net_fqdn

{{ ansible_hostname }} + lxc__net_domain

host1.lxc.example.com

debops.lxc

lxc__net_domain

lxc. + lxc__net_base_domain

lxc.example.com

debops.mailman

mailman__fqdn

lists. + mailman__domain

lists.example.com

debops.miniflux

miniflux__fqdn

miniflux. + miniflux__domain

miniflux.example.com

debops.mosquitto

mosquitto__fqdn

mqtt. + mosquitto__domain

mqtt.example.com

debops.netbox

netbox__fqdn

dcim. + netbox__domain

dcim.example.com

debops.netbox

netbox__fqdn

ipam. + netbox__domain

ipam.example.com

debops.owncloud

owncloud__fqdn

cloud. + owncloud__domain

cloud.example.com

debops.pdns

pdns__nginx_fqdn

powerdns. + {{ ansible_domain }}

powerdns.example.com

debops.phpipam

phpipam__fqdn

ipam. + {{ ansible_domain }}

ipam.example.com

debops.rabbitmq_management

rabbitmq_management__fqdn

rabbitmq. + rabbitmq_management__domain

rabbitmq.example.com

debops.roundcube

roundcube__fqdn

webmail. + roundcube__domain

webmail.example.com

debops.rspamd

rspamd__nginx_fqdns

rspamd. + {{ ansible_domain }}

rspamd.example.com

debops.rspamd

rspamd__nginx_fqdns

{{ ansible_hostname }} + -rspamd. + {{ ansible_domain }}

host1-rspamd.example.com

debops.rstudio_server

rstudio_server__fqdn

rstudio. + rstudio_server__domain

rstudio.example.com

debops.secret

secret__ldap_fqdn

ldap. + secret__ldap_domain

ldap.example.com

debops.foodsoft

foodsoft__fqdn

foodsoft. + foodsoft__domain

foodsoft.example.com

debops.homeassistant

homeassistant__fqdn

ha. + homeassistant__domain

ha.example.com

debops.volkszaehler

volkszaehler__fqdn

vz. + volkszaehler__domain

vz.example.com

DNS SRV Records

Several DebOps roles (and other software) use DNS Service (SRV) records to locate various services. The SRV record is defined in RFC 2782 and has the following form:

_service._proto.name. ttl IN SRV priority weight port target.
_service

The symbolic name of the desired service, prefixed with an underscore. A list of known service names and port numbers is maintained by the IANA and published as the Service Name and Transport Protocol Port Number Registry.

_proto

The protocol to use for the desired service, usually tcp or udp, prefixed with an underscore.

name

The domain name for which the record is valid.

ttl

The DNS time-to-live value.

IN

The DNS record class.

SRV

The DNS record type.

priority

The priority of the record. Clients should attempt to use records with the lowest priority first and then use records with higher-valued priority as a fallback.

weight

The relative weight for records with the same priority. A higher weight means a higher change that the record will be picked. Weights do not have to add up to any particular sum.

port

The UDP/TCP port on which the service is provided (see the _service field above).

target

The canonical FQDN of the host providing the service, ending with a dot.

Example SRV Configuration

As an an example, assume that we have a hypothetical service foo, which uses TCP port 4242. The corresponding DNS records might look something like this (using the ISC BIND zone file format):

# name                   ttl   class A   IPv4 address
foo1.example.com.        86400 IN    A   192.0.2.1
foo2.example.com.        86400 IN    A   192.0.2.2
foo3.example.com.        86400 IN    A   192.0.2.3
foobackup.example.com.   86400 IN    A   192.0.3.1

# _service._proto.name.  ttl   class SRV priority weight port target.
_foo._tcp.example.com.   86400 IN    SRV 10       80     4242 foo1.example.com.
_foo._tcp.example.com.   86400 IN    SRV 10       40     4242 foo2.example.com.
_foo._tcp.example.com.   86400 IN    SRV 10       40     4242 foo3.example.com.
_foo._tcp.example.com.   86400 IN    SRV 20       0      4242 foobackup.example.com.

Correctly configured clients would then alternate between using the first three hosts (which all have priority 10). 50% of requests would go to foo1 while 25% of requests would go to foo2 and foo3, respectively. If all three hosts with priority 10 are unavailable, clients would be expected to connect to foobackup.

Note

Many existing clients (including the DebOps roles) will employ a more simplistic scheme, e.g. by picking the server with the lowest priority and highest weight, or just pick a random server. SRV records can therefore not guarantee proper load balancing.

Example SRV Configuration using CNAMEs

Or, if you want to add another layer of indirection by using CNAME records to make it easier to swap out servers without having to reconfigure all clients (in case the SRV records are used to create the initial configuration, as is done for several DebOps roles):

# name                   ttl   class A      IPv4 address
foo-server1.example.com. 86400 IN    A      192.0.2.1
foo-server2.example.com. 86400 IN    A      192.0.2.2
foo-server3.example.com. 86400 IN    A      192.0.2.3
foo-server4.example.com. 86400 IN    A      192.0.3.1
foo-server5.example.com. 86400 IN    A      192.0.4.1

# _service._proto.name.  ttl   class SRV    priority weight port target.
_foo._tcp.example.com.   86400 IN    SRV    10       80     4242 foo1.example.com.
_foo._tcp.example.com.   86400 IN    SRV    10       40     4242 foo2.example.com.
_foo._tcp.example.com.   86400 IN    SRV    10       40     4242 foo3.example.com.
_foo._tcp.example.com.   86400 IN    SRV    20       0      4242 foobackup.example.com.

# alias                  ttl   class CNAME  canonical name
foo1.example.com.        86400 IN    CNAME  foo-server1.example.com.
foo2.example.com.        86400 IN    CNAME  foo-server2.example.com.
foo3.example.com.        86400 IN    CNAME  foo-server3.example.com.
foobackup.example.com.   86400 IN    CNAME  foo-server4.example.com.
foo.example.com.         86400 IN    CNAME  foo-server1.example.com.
foo-test.example.com.    86400 IN    CNAME  foo-server5.example.com.

In the above example, any clients that are used for testing and development should be configured to connect directly to the foo-test.example.com server and not use the SRV records.

Warning

The DNS SRV specification requires the hostnames used as targets in SRV records to be canonical names, and not aliases (i.e. the target must point to a hostname with an A or AAAA record and not to a CNAME). Often it will anyway work to point a SRV record to a CNAME, but strictly speaking, it is not RFC compliant (see the "Target" definition on page 3 of RFC 2782).

SRV Records using dnsmasq

Here's how the dnsmasq(8) configuration could look for the first example:

host-record = foo1.example.com,192.0.2.1
host-record = foo2.example.com,192.0.2.2
host-record = foo3.example.com,192.0.2.3
host-record = foobackup.example.com,192.0.3.1

srv-host = _foo._tcp.example.com,foo1.example.com,4242,10,80
srv-host = _foo._tcp.example.com,foo2.example.com,4242,10,40
srv-host = _foo._tcp.example.com,foo3.example.com,4242,10,40
srv-host = _foo._tcp.example.com,foobackup.example.com,4242,20,0

Or, for the second example:

host-record = foo-server1.example.com,192.0.2.1
host-record = foo-server2.example.com,192.0.2.2
host-record = foo-server3.example.com,192.0.2.3
host-record = foo-server4.example.com,192.0.3.1
host-record = foo-server5.example.com,192.0.4.1

cname = foo1.example.com,foo-server1.example.com
cname = foo2.example.com,foo-server2.example.com
cname = foo3.example.com,foo-server3.example.com
cname = foobackup.example.com,foo-server4.example.com
cname = foo.example.com,foo-server1.example.com
cname = foo-test.example.com,foo-server5.example.com

srv-host = _foo._tcp.example.com,foo1.example.com,4242,10,80
srv-host = _foo._tcp.example.com,foo2.example.com,4242,10,40
srv-host = _foo._tcp.example.com,foo3.example.com,4242,10,40
srv-host = _foo._tcp.example.com,foobackup.example.com,4242,20,0

SRV Records using debops.dnsmasq

If you are using the debops.dnsmasq role, the above configuration can be set in the Ansible inventory, for the first example:

dnsmasq__dns_records:

  - host: 'foo1.example.com'
    address: '192.0.2.1'

  - host: 'foo2.example.com'
    address: '192.0.2.2'

  - host: 'foo3.example.com'
    address: '192.0.2.3'

  - host: 'foobackup.example.com'
    address: '192.0.3.1'

  - srv: '_foo._tcp.example.com'
    target: 'foo1.example.com'
    port: 4242
    priority: 10
    weight: 80

  - srv: '_foo._tcp.example.com'
    target: 'foo2.example.com'
    port: 4242
    priority: 10
    weight: 40

  - srv: '_foo._tcp.example.com'
    target: 'foo3.example.com'
    port: 4242
    priority: 10
    weight: 40

  - srv: '_foo._tcp.example.com'
    target: 'foobackup.example.com'
    port: 4242
    priority: 20
    weight: 0

Or for the second example:

dnsmasq__dns_records:

  - host: 'foo-server1.example.com'
    address: '192.0.2.1'

  - host: 'foo-server2.example.com'
    address: '192.0.2.2'

  - host: 'foo-server3.example.com'
    address: '192.0.2.3'

  - host: 'foo-server4.example.com'
    address: '192.0.3.1'

  - cname: 'foo1.example.com'
    target: 'foo-server1.example.com'

  - cname: 'foo2.example.com'
    target: 'foo-server2.example.com'

  - cname: 'foo3.example.com'
    target: 'foo-server3.example.com'

  - cname: 'foobackup.example.com'
    target: 'foo-server4.example.com'

  - cname: 'foo.example.com'
    target: 'foo-server1.example.com'

  - srv: '_foo._tcp.example.com'
    target: 'foo1.example.com'
    port: 4242
    priority: 10
    weight: 80

  - srv: '_foo._tcp.example.com'
    target: 'foo2.example.com'
    port: 4242
    priority: 10
    weight: 40

  - srv: '_foo._tcp.example.com'
    target: 'foo3.example.com'
    port: 4242
    priority: 10
    weight: 40

  - srv: '_foo._tcp.example.com'
    target: 'foo4.example.com'
    port: 4242
    priority: 20
    weight: 0

dnsmasq__dhcp_hosts:
  - name: 'foo-server5'
    comment: 'Development foo server'
    domain: 'example.com'
    mac: '00:00:5e:00:53:04'
    ip: '192.0.4.1'
    cname: [ 'foo-test' ]

Note

The above example demonstrates how host addresses can be defined either as a separate host record in dnsmasq__dns_records or as part of a DHCP record in dnsmasq__dhcp_hosts.

SRV Records used by DebOps roles

The following table lists the DNS SRV records used for autoconfiguration by various DebOps roles:

Role

SRV

Variable

Fallback

debops.gitlab_runner

_gitlab._tcp

gitlab_runner__gitlab_srv_rr

code. + gitlab_runner__domain + :443

debops.icinga

_icinga-master._tcp

icinga__master_nodes

icinga-master. + icinga__domain + icinga__api_port

debops.icinga

_icinga-director._tcp

icinga__director_nodes

icinga-director. + icinga__domain + icinga__api_port

debops.imapproxy

_imap._tcp

imapproxy__imap_srv_rr

imap. + imapproxy__domain + :143

debops.imapproxy

_imaps._tcp

imapproxy__imaps_srv_rr

imap. + imapproxy__domain + :993

debops.ldap

_ldap._tcp

ldap__servers_srv_rr

ldap. + ldap__domain + :389

debops.nullmailer

_smtp._tcp

nullmailer__smtp_srv_rr

smtp. + nullmailer__domain + :25

debops.roundcube

_imaps._tcp

roundcube__imap_srv_rr

imap. + roundcube__domain + :993

debops.roundcube

_submissions._tcp

roundcube__smtp_srv_rr

smtp. + roundcube__domain + :465

debops.roundcube

_sieve._tcp

roundcube__sieve_srv_rr

sieve. + roundcube__domain + :4190

debops.rsyslog

_syslog._tcp

rsyslog__syslog_srv_rr

syslog. + rsyslog__domain + :6514

The DebOps dig_srv plugin

DebOps roles use a slightly modified version of the Ansible dig lookup plugin to perform DNS SRV record lookups. The reason that a custom plugin is used is that the Ansible version does not make it possible to distinguish between errors which should halt the operation of a play (e.g. if the DNS server returns SERVFAIL) and errors which should not (e.g. NXDOMAIN).

In addition, the ansible plugin does not sort the returned resource records, meaning that idempotency is not ensured (unless the results are sorted manually) in case several SRV records are returned.

The custom dig_srv plugin generally works in a manner similar to the Ansible dig lookup plugin, but removes parameters which are not necessary for looking up SRV resource records and also provides a slightly different return value.

If we assume a role called bar, which wishes to lookup SRV records for the service foo, using foo.<domain> and port 4242 as a fallback (in case no SRV records are defined), the plugin would be called like this:

bar__domain: '{{ ansible_domain }}'

bar__foo_srv_rr: '{{ q("debops.debops.dig_srv", "_foo._tcp." + bar__domain,
                       "foo." + bar__domain, 4242) }}'

The return value from the lookup would be a list of YAML dictionaries, where each dictionary corresponds to one SRV record. Something like this:

bar__foo_srv_rr:

  - target: 'foo1.example.com'
    class: 'IN'
    owner: '_foo._tcp.example.com'
    port: 4242
    priority: 10
    ttl: 86400
    type: 'SRV'
    weight: 40
    dig_srv_src: 'dns'
    target_port: 'foo1.example.com:4242'

  - target: 'foo2.example.com'
    class: 'IN'
    owner: '_foo._tcp.example.com'
    port: 4242
    priority: 20
    ttl: 86400
    type: 'SRV'
    weight: 40
    dig_srv_src: 'dns'
    target_port: 'foo2.example.com:4242'

The dig_srv_src field will be either dns if resource records were returned by the DNS server and fallback otherwise. See DNS SRV Records for a definition of the other fields. The resource records will be sorted on priority, weight (reverse order, i.e. higher weight first), target and port.

In case no SRV records are available, the lookup will return something like this:

bar__foo_srv_rr:

  - target: 'foo.example.com'
    class: 'IN'
    owner: '_foo._tcp.example.com'
    port: 4242
    priority: 0
    ttl: 0
    type: 'SRV'
    weight: 0
    dig_srv_src: 'fallback'
    target_port: 'foo.example.com:4242'

Overriding DNS SRV Queries

The format described in the previous section can also be used to override the variables used by the various roles to lookup the DNS SRV records (generally named <role>__*_srv_rr, see SRV Records used by DebOps roles for a list), for example if you plan to add DNS SRV records later:

# ansible/inventory/group_vars/all/bar.yml

bar__foo_srv_rr:
  - target: 'foo.example.org'
    port: '1234'
    priority: '1'

Alternatively, most roles set separate variables on the basis of the results of the SRV lookup. In such cases, it might also be more straightforward to override these "dependent" variables straight away.

For example, the debops.nullmailer role performs the SRV lookup using the nullmailer__smtp_srv_rr variable, which is then used to create default values for nullmailer__relayhost and nullmailer__smtp_port:

nullmailer__smtp_srv_rr: '{{ q("debops.debops.dig_srv"... }}'

nullmailer__relayhost: '{{ nullmailer__smtp_srv_rr[0]["target"] }}'

nullmailer__smtp_port: '{{ nullmailer__smtp_srv_rr[0]["port"] }}'

Which means that the autoconfiguration can also be overridden by setting these variables directly:

# ansible/inventory/group_vars/all/nullmailer.yml

nullmailer__relayhost: 'notfoo.example.com'

nullmailer__smtp_port: 42

Further Reading