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.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