DEBOPS.UNBOUND(5)

NAME
debops.unbound - Manage Unbound, local DNS resolver

SYNOPSIS

debops run service/unbound [--limit group,host,...] [--diff] [--check] [--tags tag1,tag2,...] [--skip-tags tag1,tag2,...] [<ansible-playbook options>] ...

DESCRIPTION

Unbound <https://unbound.net/> is a local DNS resolver. It supports DNSSEC <https://en.wikipedia.org/wiki/DNSSEC> validation and can be used to ensure that DNS queries protected by DNSSEC are signed by the correct DNS root zone key, verified locally.

GETTING STARTED

  Default configuration

If the debops.lxc role has set up a LXC environment on the host, the debops.unbound will configure an internal DNS zone based on the configuration provided by the debops.lxc Ansible local facts. This can be used to access the LXC containers via their DNS names instead of IP addresses.

  Usage with internal RFC 1918 private networks

This role does not configure any upstream DNS servers by default (for example Google DNS servers). This means that Unbound will try and resolve DNS and DNSSEC queries by itself, using the locally provided DNS root keys for DNSSEC verification. On the Debian/Ubuntu hosts, when the unbound service starts, it replaces the currently configured DNS servers with itself, using the resolvconf package.

This means that any private networks the host is connected to won't be resolvable anymore. If the default FQDN of the host is in a private network, the host's hostname and domain won't be resolvable either - this affects sudo and Ansible operation, among other things.

To avoid that, as long as you have configured local DNS nameserver for an internal domain with revDNS records, for example dnsmasq, you can add your private network as an insecure, transparent DNS zone:

unbound__zones:
  - name:       'internal-network'
    zone:       'nat.example.org.'
    revdns:     '192.0.2.0/24'
    nameserver: '192.0.2.1'

The above configuration will tell Unbound to forward all DNS queries for nat.example.org. and 0.2.0.192.in-addr.arpa. DNS zones to the 192.0.2.1 nameserver.

If the specified revdns subnet is in the private IP address range (RFC 1918), the configuration template will make sure that the specified subnet and domain zones are configured as transparent, which will tell Unbound to serve the responses from the upstream DNS server as usual. These zones will also be marked as insecure, to avoid issues with DNSSEC validation.

It's possible to use a bigger subnet size than /24 - subnets between /16 and /24 will have multiple /24 reverse DNS zones pointed to the specified nameserver. Bigger subnets however currently don't work correctly due to the netaddr Python library used to generate the correct DNS zone names using only class C addresses (/24 CIDR subnets).

  Example inventory

The install and configure Unbound on a host, it needs to be present in the [debops_service_unbound] Ansible inventory group:

[debops_service_unbound]
hostname

  Example playbook

If you are using this role without DebOps, here's an example Ansible playbook that uses the debops.unbound role:

---
- name: Manage Unbound, local DNS resolver
  collections: [ 'debops.debops', 'debops.roles01',
                 'debops.roles02', 'debops.roles03' ]
  hosts: [ 'debops_service_unbound' ]
  become: True
  environment: '{{ inventory__environment | d({})
                   | combine(inventory__group_environment | d({}))
                   | combine(inventory__host_environment  | d({})) }}'
  roles:
    - role: resolvconf
      tags: [ 'role::resolvconf', 'skip::resolvconf' ]
      resolvconf__dependent_services:
        - 'unbound'
    - role: python
      tags: [ 'role::python', 'skip::python' ]
      python__dependent_packages3:
        - '{{ unbound__python__dependent_packages3 }}'
      python__dependent_packages2:
        - '{{ unbound__python__dependent_packages2 }}'
    - role: etc_services
      tags: [ 'role::etc_services', 'skip::etc_services' ]
      etc_services__dependent_list:
        - '{{ unbound__etc_services__dependent_list }}'
    - role: unbound
      tags: [ 'role::unbound', 'skip::unbound' ]

  Ansible tags

You can use Ansible --tags or --skip-tags parameters to limit what tasks are performed during Ansible run. This can be used after a host was first configured to speed up playbook execution, when you are sure that most of the configuration is already in the desired state.

Available role tags:

role::unbound
    Main role tag, should be used in the playbook to execute all of the role tasks as well as role dependencies.

DEFAULT VARIABLE DETAILS

some of debops.unbound default variables have more extensive configuration than simple strings or lists, here you can find documentation and examples for them.

  unbound__server

Configuration of the unbound__*_server variables is described in a separate document, Default variable details: unbound__server.

  unbound__zones

The unbound__*_zones lists are used to configure forward or stub DNS zones published by Unbound service. Each DNS zone delegation is configured in its own /etc/unbound/unbound.conf.d/zone_<name>.conf configuration file.

Each list entry is a YAML dictionary with specific parameters:

name
    Required. Name of the DNS zone, used in the filename. This parameter is used as an identifier during the variable parsing.

zone
    Optional. If specified, this string will be used as the DNS zone name. With this parameter specified, name can be used as a general identifier of a particular delegation.

type
    Optional. The zone type to use, either forward (default if not specified), local or stub. See the unbound.conf(5) <https://manpages.debian.org/unbound.conf(5)> for details about stub and forward zones.

local_zone_type
    Optional. If the type parameter is set to local, this parameter can be used to define the type of the local zone (static (default), transparent, etc. See unbound.conf(5) <https://manpages.debian.org/unbound.conf(5)> manual page, local-zone: keyword for the details about local zone types.

local_zone_data
    Optional. If the type parameter is set to local, this parameter can be used to define the data of a given local zone. This is a YAML list of entries, each entry can specify a DNS Resource Record as a string. See the examples section for an example local zone configuration.

nameserver, nameservers
    Optional. IP address or list of IP addresses of the DNS nameservers of a particular zone. You can specify the port using the @ character, for example 192.0.2.1@5353.

revdns
    Optional. Specify a CIDR subnet or multiple subnets for a given DNS zone. If specified, a revDNS zones will be included in the generated zone file; each revDNS zone will use the same nameserver IP addresses and other options specified for the main DNS zone. Currently only IPv4 C-class subnets (/16 to /24) are supported best.

    If specified subnet is in a RFC 1918 private network range, the main DNS zone and revDNS zones will be set as local, insecure zones to avoid issues with DNSSEC. This can be overridden by setting the private_domain, domain_insecure and/or local_zone parameters to False.

state
    Optional. If not specified or present, the zone file will be generated.

    If absent, the configuration file will be removed.

    If ignore, the given entry will not be evaluated by the role, and no changes will be done to the preceding parameters with the same name. This can be used to conditionally activate entries with different configuration.

    If append, the given entry will be evaluated only if an entry with the same name already exists. The current state will not be changed.

comment
    Optional. String or a YAML dictionary with additional comments for a given DNS zone.

options
    Optional. List of configuration options for a particular zone. The format is the same as Default variable details: unbound__server configuration options. For a list of supported options, see the stub zone and forward zone sections of the unbound.conf(5) <https://manpages.debian.org/unbound.conf(5)> manual page.

server_options
    Optional. List of server: configuration options associated with a particular zone. The format is the same as Default variable details: unbound__server configuration options.

  Examples

Forward all queries to external Google DNS servers:

---
unbound__zones:
  # https://en.wikipedia.org/wiki/Google_Public_DNS
  - name: 'forward-all-to-google'
    comment: 'Forward all DNS queries to Google Public DNS'
    zone: '.'
    nameservers: [ '8.8.8.8', '8.8.4.4',
                   '2001:4860:4860::8888',
                   '2001:4860:4860::8844' ]

Create custom forward zone for internal network:

unbound__zones:
  - name: 'internal-net'
    zone: 'nat.example.org'
    revdns: '192.0.2.0/24'
    nameserver: '192.0.2.1'
    options:
      - 'forward-first': True

Define a local DNS entry example.test. with a few resource records:

unbound__zones:
  - name: 'example.test'
    zone: 'example.test.'
    type: 'local'
    local_zone_type: 'static'
    local_zone_data:
      - 'NS localhost.'
      - 'SOA localhost. nobody.invalid. 1 3600 1200 604800 10800'
      - 'PTR localhost.'
      - 'A 192.0.2.1'
      - 'AAAA 2001:db8::1'

Configure Unbound to support a stub DNS zone using an external DNS server, for example a home router with dnsmasq nameserver. Ensure that both normal and reverse lookups work as expected. The local zone is not signed with DNSSEC so we need to mark it as insecure.

unbound__zones:
  - name: 'example.net'
    zone: 'example.net.'
    type: 'stub'
    options:
      - 'stub-addr': '192.0.2.1'
    server_options:
      - 'domain-insecure': '"example.net"'
      - 'local-zone':      '"example.net." nodefault'
  - name: '2.0.192.in-addr.arpa'
    zone: '2.0.192.in-addr.arpa.'
    type: 'stub'
    options:
      - 'stub-addr': '192.0.2.1'
    server_options:
      - 'domain-insecure': '"2.0.192.in-addr.arpa."'
      - 'local-zone':      '"2.0.192.in-addr.arpa." nodefault'

DEFAULT VARIABLE DETAILS: UNBOUND__SERVER

The unbound__*_server variables are used to define the contents of the /etc/unbound/unbound.conf.d/ansible.conf configuration file. The variables are YAML lists, concatenated together into unbound__combined_server variable, which is passed to the configuration template. Only the server section of the configuration is managed by these variables.

Each list entry is a YAML dictionary, which can be written in a simple or complex form. Entries that control Unbound parameters of the same name will be combined together in order of appearance. Since most of the Unbound configuration options use dashes in their names, you might want to quote the YAML dictionary keys to avoid issues with Jinja templating.

  Simple form of the configuration parameters

Simple form of the Unbound configuration uses the dictionary key as a option name, and its value as that option's parameters:

unbound__server:
  # Option with boolean value
  - 'extended-statistics': True
  # Option with integer value
  - verbosity: 1
  # Option with string value
  - 'private-domain': 'example.org'
  # Option with multiple values in a list
  - 'domain-insecure': [ 'example.org', 'example.com' ]

The result of the above configuration in /etc/unbound/unbound.conf.d/ansible.conf:

server:
    extended-statistics:           yes
    verbosity:                     1
    private-domain:                "example.org"
    domain-insecure:               "example.org"
    domain-insecure:               "example.com"

The parameters in the configuration file will be present in the order they were first defined in the variables.

  Complex form of the configuration parameters

Complex form of the Unbound configuration is detected when a dictionary key contains a name parameter. In that case, the role will interpret the entry using specific parameters:

name
    The name of the configuration option to manage. This parameter is used as an identifier during the variable parsing.

value
    Required. A value which should be set for a given option. Values can be YAML strings, integers, booleans and lists (not dictionaries). Lists can contain simple strings, numbers, or YAML dictionaries that describe each value in greater detail. See Configuration values and their interactions for more details.

option
    Optional. If specified, the option will use this string as the "name" instead of the name value. This is useful to create examples in the configuration file that have the same name as existing configuration options.

comment
    Optional. String or a YAML dictionary with additional comments for a given configuration option.

separator
    Optional, boolean. if True, an empty line will be added above a given option, useful for readability.

state
    Optional. If not specified or present, the option will be present in the finished configuration file.

    If absent, the option will not be included in the configuration file.

    If ignore, the given entry will not be evaluated by the role, and no changes will be done to the preceding parameters with the same name. This can be used to conditionally activate entries with different configuration.

    If hidden, the option will not be displayed in the configuration file, but any comments will be present. This can be used to add free-form comments in the Postfix configuration file.

    If comment, the option will be present, but it will be commented out. This can be used to add examples in the configuration file.

    If append, the given entry will be evaluated only if an entry with the same name already exists. The current state will not be changed.

weight
    Optional. A positive or negative number which affects the position of a given option in the configuration file. The higher the number, the more a given option "weighs" and the lower it will be placed in the finished configuration file. Negative numbers make the option "lighter" and it will be placed higher.

copy_id_from
    Optional. This is an internal role parameter which can be used to change the relative position of a given option in the configuration file. If you specify a name of an option, it's internal "id" number (used for sorting) will be copied to the current option. This can be used to move options around to different configuration file sections.

  Examples

Define the previous example using complex form:

unbound__server:
  - name: 'extended-statistics'
    comment: 'Enable extended server statistics'
    value: True
  - name: 'verbosity'
    value: 1
  - name: 'private-domain'
    value: 'example.org'
  - name: 'domain-insecure'
    value: [ 'example.org', 'example.com' ]

Unbound supports more complex parameters with arguments. You can define them using extended syntax as well:

unbound__server:
  - 'define-tag': 'tag1 tag2 tag3'
  - name: 'access-control'
    value:
      - name: '127.0.0.0/8'
        args: 'allow_snoop'
      - name: '::1/128'
        args: 'allow_snoop'
      - name: '192.0.2.0/24'
        args: 'allow'
  - name: 'access-control-tag'
    value:
      - name: '192.0.2.0/24'
        args: '"tag1 tag2"'

The result of the above configuration in /etc/unbound/unbound.conf.d/ansible.conf:

server:
    # Enable extended server statistics
    extended-statistics:           yes
    verbosity:                     1
    private-domain:                "example.org"
    domain-insecure:               "example.org"
    domain-insecure:               "example.com"
    define-tag:                    "tag1 tag2 tag3"
    access-control:                127.0.0.0/8 allow_snoop
    access-control:                192.0.2.0/24 allow
    access-control:                ::1/128 allow_snoop
    access-control-tag:            192.0.2.0/24 "tag1 tag2"

The parameters in the configuration file will be present in the order they were first defined in the variables, unless the weight parameter is added, which will change the order.

  Configuration values and their interactions

The Unbound configuration file <https://unbound.net/documentation/unbound.conf.html> uses key-value format, with values being either strings, numbers, booleans or lists. The first three types are handled by the debops.unbound role as normal.

List values are by default concatenated to allow easy extension of existing values. The values in a list are either YAML strings, numbers, or can be defined as YAML dictionaries with specific parameters:

name or param
    Required. The value itself, usually a string.

state
    Optional. If not defined or present, the value will be included in the list.

    If absent, the value will be removed from the list.

    If ignore, the given entry will not be evaluated by the role, and will not change the state of the value. This can be used to enable or disable values conditionally.

weight
    Optional. A positive or negative number which affects the position of a given value in the list. The higher the number, the more a given value "weighs" and the lower it will be placed in the finished list. Negative numbers make the value "lighter" and it will be placed higher.

args
    Optional. Some Unbound values can have additional arguments. They should be added using this parameter which will be appended to a given value "as-is". Any quotes needed by Unbound (for example, tags) need to be included inside the quoted string itself.

  Example list

Define a list with conditional values:

unbound__server:
  - name: 'domain-insecure'
    value:
      - 'example.com'
      - name: 'example.org'
        state: '{{ "present"
                   if (ansible_domain.split(".")|count > 1)
                   else "ignore" }}'
      - name: 'example.net'
        weight: 100

  Base value replacement

Repeating the string, number or boolean option will result in the latter entry replacing the former entry:

unbound__server:
  # Old value
  - verbosity: 1
  # New, active value
  - verbosity: 2

The result of the above configuration in /etc/unbound/unbound.conf.d/ansible.conf:

verbosity:          2

  Lists are merged together

The list parameters behave differently. Specifying the same option multiple times, if the preceding option was a list, will add the specified parameters to the list:

unbound__server:
  - 'domain-insecure': [ 'example.com', 'example.org' ]
  - 'domain-insecure': [ 'example.net' ]

The result of the above configuration in /etc/unbound/unbound.conf.d/ansible.conf:

domain-insecure:     "example.org"
domain-insecure:     "example.net"
domain-insecure:     "example.com"

  How to reset a list

If the option was a list, and subsequent option specified a boolean, string or a number, the value will replace the previous one, instead of adding to a list. This can be used to reset the list instead of appending to it.

unbound__server:
  - 'domain-insecure': [ 'example.com', 'example.org' ]
  - 'domain-insecure': 'example.net'

The result of the above configuration in /etc/unbound/unbound.conf.d/ansible.conf:

domain-insecure:     "example.net"

  Lists don't add duplicates

The role checks if a given list element is already present, and it won't add a duplicate value to the list:

unbound__server:
  - 'domain-insecure': [ 'example.org', 'example.com' ]
  - 'domain-insecure': [ 'example.org' ]

The result of the above configuration in /etc/unbound/unbound.conf.d/ansible.conf:

domain-insecure:       "example.org"
domain-insecure:       "example.com"

AUTHOR
Maciej Delmanowski

COPYRIGHT
2014-2024, Maciej Delmanowski, Nick Janetakis, Robin Schneider and others