Skip to content

Repository files navigation

Ansible Role: PowerDNS Recursor

Build Status License Ansible Role GitHub tag

An Ansible role created by the folks behind PowerDNS to setup the PowerDNS Recursor.

Requirements

An ansible-core 2.16 or newer installation. Enterprise Linux 8 targets must be managed with ansible-core 2.16: their system Python is 3.6, which the modules of ansible-core 2.20 cannot run.

Dependencies

None.

Role Variables

Available variables are listed below, along with default values (see defaults/main.yml):

pdns_rec_install_repo: ""

By default, the PowerDNS Recursor is installed from the software repositories configured on the target hosts.

# Install the PowerDNS Recursor from the 'master' official repository
- hosts: pdns_recursors
  roles:
  - { role: powerdns.pdns_recursor,
      pdns_rec_install_repo: "{{ pdns_rec_powerdns_repo_master }}" }

# Install the PowerDNS Recursor from the '5.1.x' official repository
- hosts: pdns_recursors
  roles:
  - { role: powerdns.pdns_recursor,
      pdns_rec_install_repo: "{{ pdns_rec_powerdns_repo_51 }}" }

# Install the PowerDNS Recursor from the '5.2.x' official repository
- hosts: pdns_recursors
  roles:
  - { role: powerdns.pdns_recursor,
      pdns_rec_install_repo: "{{ pdns_rec_powerdns_repo_52 }}" }

# Install the PowerDNS Recursor from the '5.3.x' official repository
- hosts: pdns_recursors
  roles:
  - { role: powerdns.pdns_recursor,
      pdns_rec_install_repo: "{{ pdns_rec_powerdns_repo_53 }}" }

# Install the PowerDNS Recursor from the '5.4.x' official repository
- hosts: pdns_recursors
  roles:
  - { role: powerdns.pdns_recursor,
      pdns_rec_install_repo: "{{ pdns_rec_powerdns_repo_54 }}" }

The examples above, show how to install the PowerDNS Recursor from the official PowerDNS repositories (see the complete list of pre-defined repos in vars/main.yml).

The roles also supports custom repositories

- hosts: all
  vars:
    pdns_rec_install_repo:
      name: "powerdns-rec"  # the name of the repository
      apt_version: rec-master # deb822 suites suffix (appended to release codename)
      apt_repo_origin: "repo.example.com"   # used to pin the PowerDNS packages to the provided repository
      gpg_key: "http://repo.example.com/MYREPOGPGPUBKEY.asc" # repository public GPG key
      yum_repo_baseurl: "http://repo.example.com/el/$basearch/$releasever/pdns-recursor"
      yum_repo_debug_symbols_baseurl: "http://repo.example.com/el/$basearch/$releasever/pdns-recursor/debug"
  roles:
  - { role: powerdns.pdns_recursor }

It is also possible to install the PowerDNS Recursor from custom repositories as demonstrated in the example above.

pdns_rec_install_epel: true

By default, install EPEL to satisfy some PowerDNS Recursor dependencies like protobuf. To skip the installation of EPEL set pdns_rec_install_epel to False.

pdns_rec_package_name: "{{ default_pdns_rec_package_name }}"

The name of the PowerDNS Recursor package, pdns-recursor on RedHat-like Debian-like systems.

pdns_rec_package_version: ""

Optionally, allow to set a specific version of the PowerDNS Recursor package to be installed.

pdns_rec_package_state: "present"

The desired state of the PowerDNS Recursor packages. Use "present" (default) to install, "latest" to upgrade, or "absent" to uninstall.

pdns_rec_install_debug_symbols_package: false

Install the PowerDNS Recursor debug symbols.

pdns_rec_debug_symbols_package_name: "{{ default_pdns_rec_debug_symbols_package_name }}"

The name of the PowerDNS Recursor debug package to be installed when pdns_install_debug_symbols_package is true, pdns-recursor-debuginfo on RedHat-like systems and pdns-recursor-dbg on Debian-like systems.

pdns_rec_user: "{{ default_pdns_rec_user }}"
pdns_rec_group: "{{ default_pdns_rec_group }}"

The user and group the PowerDNS Recursor will run as, pdns-recursor on RedHat-like systems and pdns on Debian-like systems
NOTE: This role does not create any user or group as we assume that they're created by the package or other roles.

pdns_rec_file_owner: "root"
pdns_rec_file_group: "{{ default_pdns_file_group }}"

User and group owning the configuration files and directories.

pdns_rec_service_name: "pdns-recursor"

The name of the PowerDNS Recursor service.

pdns_rec_bin_name: "pdns_recursor"

The name of the PowerDNS Recursor binary.

pdns_rec_service_state: "started"
pdns_rec_service_enabled: true
pdns_rec_service_masked: false

Allow to specify the desired state of the PowerDNS Recursor service. E.g. This allows to install and configure the PowerDNS Recursor without automatically starting the service.

pdns_rec_disable_handlers: false

Disable automated service restart on configuration changes.

pdns_rec_flush_handlers: false

Run the notified handlers at the end of the role instead of at the end of the play. See Handlers.

pdns_rec_config_dir: "{{ default_pdns_rec_config_dir }}"
pdns_rec_config_file: "recursor.conf"

The PowerDNS Recursor configuration files and directories, where default_pdns_rec_config_dir is /etc/powerdns on Debian and /etc/pdns-recursor on RedHat.

pdns_rec_config: {}

Dictionary containing in YAML format configuration of PowerDNS Recursor. See https://docs.powerdns.com/recursor/yamlsettings.html.

It is merged over a baseline the role carries, so only the settings that differ have to be given. The baseline declares the settings the packages ship in their own configuration file, which the role replaces:

recursor:
  include_dir: "{{ pdns_rec_config_dir }}/recursor.d"
  setuid: "{{ pdns_rec_user }}"
  setgid: "{{ pdns_rec_group }}"
  extended_resolution_errors: true
  threads: 2
  max_mthreads: 2048
incoming:
  allow_from:
    - 127.0.0.0/8
  listen:
    - 127.0.0.1
  reuseport: true
outgoing:
  source_address:
    - 0.0.0.0

setuid and setgid make the daemon drop privileges by itself. The packaged units set User= and Group= as well, so a service started through them is already unprivileged; the settings are what covers a start that does not go through the packaged unit - a hand-written unit, an ExecStart override installed through pdns_rec_service_overrides, or a run outside systemd. Without them such a start leaves the recursor running as the account that launched it.

pdns_rec_user and pdns_rec_group therefore have to name the account the service actually starts as. The packaged units already agree with them, and an override that changes User=, Group= or ExecStart has to change these two with it. Asked to become a different account than the one it was started as, an unprivileged daemon exits with Unable to set effective group id: Operation not permitted - the intended refusal rather than a silent downgrade.

incoming.allow_from is the one baseline value that takes something away. ansible.builtin.combine replaces a list rather than merging it, so the baseline narrows the recursor's own default - loopback plus the RFC 1918 ranges, link-local and ::1/128 - down to 127.0.0.0/8. A host that relied on that default to answer clients on a private range will refuse them once this baseline applies, so declare the ranges it should serve:

pdns_rec_config:
  incoming:
    allow_from:
      - 127.0.0.0/8
      - ::1/128
      - 10.0.0.0/8

Note that section placement is not free: threads and max_mthreads belong to recursor, not to outgoing. A key in the wrong section is not ignored with a warning - the recursor gives up on the YAML parser, reads the file again as an old-style configuration, rejects it there as well and does not start.

The REST API is not enabled by the baseline, because webservice.webserver defaults to false and an API key belongs to the operator. To enable it:

pdns_rec_config:
  webservice:
    webserver: true
    address: 127.0.0.1
    port: 8001
    api_dir: /var/lib/pdns-recursor/api
    api_key: "{{ vault_pdns_rec_api_key }}"

api_dir is read whether or not the webserver is enabled, and the recursor exits with No such file or directory when it names a directory that does not exist, so the role creates it - owned by pdns_rec_user, because the API writes into it, and never walked, because what is inside is state the daemon maintains. The same goes for recursor.include_dir.

Listing either of them in pdns_rec_config_additional_dirs as well is supported and takes precedence, but add recurse: false when you do. The API directory in particular is not a directory to walk: the recursor writes its apizones with mode 0644, so a recursive mode would reset it, notify a restart, and the restart would write it back - a change on every converge, for ever.

pdns_rec_config_additional_dirs: []
# pdns_rec_config_additional_dirs:
#   - path: "/var/lib/pdns-recursor/rpz"
#     owner: "{{ pdns_rec_user }}"
#   - path: "{{ pdns_rec_config_dir }}/zones"
#     recurse: false

Additional directories for configuration or supplementary files. An entry is either a path or a mapping that may carry path, owner, group, mode and recurse.

pdns_rec_config_additional_files: []
# pdns_rec_config_additional_files:
#   - dest: "/var/lib/pdns-recursor/rpz/test.rpz"
#     content: |
#       test.rpz.               60   IN SOA    ns.test.rpz. hostmaster.test.rpz. 1 10800 60 3600 3600
#       test-rpz.com.test.rpz.  60   IN A  127.0.0.2

Additional configuration or supplementary files for PowerDNS Recursor, e.g RPZ files.

pdns_rec_config_dirs_recurse: true
pdns_rec_config_dir_mode: "u=rwX,g=rX,o="
pdns_rec_config_file_mode: "0640"

The owner, group and mode of every entry in pdns_rec_config_additional_dirs, and of the directory named by webservice.api_dir, are applied to the whole tree, so a file another role or the recursor itself put there is still readable by the account the daemon runs as. Set pdns_rec_config_dirs_recurse to false, globally or per entry through its recurse key, to touch only the directory.

The configuration directory is never walked, whatever this is set to. It belongs to the package, which ships a recursor.yml-dist in it, and to the operator, whose drop-ins live in recursor.d; the configuration file the role writes takes its owner and mode from pdns_rec_file_owner, pdns_rec_file_group and its own task.

pdns_rec_config_dir_mode is symbolic rather than octal on purpose. The capital X in u=rwX grants the execute bit on directories, and on files that already carry one, but never adds it to a file that does not - so the tree stays traversable while an RPZ zone or an include fragment is left non-executable, which is all the recursor needs, since it reads those files and never runs them. An octal mode cannot draw that line: 0750 applied to a tree would mark every file executable as well. Override it with another symbolic mode.

pdns_rec_config_file_mode is the mode of the files in pdns_rec_config_additional_files, and matches what the file half of pdns_rec_config_dir_mode produces. Keep the two in step: if they disagree and a file sits inside one of those directories, each converge rewrites its mode twice and never reports an unchanged run.

pdns_rec_service_overrides: {}

Dict with overrides for the service (systemd only). This can be used to change any systemd settings in the [Service] category. The role merges it with the platform defaults and writes the result to /etc/systemd/system/<service name>.service.d/override.conf. When the merged result is empty that file is removed again and the service restarts on the packaged unit; other drop-ins in the same directory are left alone.

Role Tags

Tags for --tags / --skip-tags:

  • repository: repo and GPG key setup, APT pinning, removal of stale versioned repo files.
  • install: package installation and removal.
  • config: config file and directories, additional files, systemd overrides, validation.
  • service: service state.
  • always: OS variable import.

The repository tasks and the fact deriving the repository name are tagged both install and repository, so --tags install also sets up the repository it needs.

Contributors: tags belong on the tasks inside install-{{ ansible_system }}.yml, configure.yml and repo-*.yml, not only on the include_tasks in tasks/main.yml. A dynamic include_tasks does not pass its tags to included tasks, so a narrow --tags run would execute the include and skip its body, silently, with rc=0.

Check Mode

Supported only on a host where this role already ran successfully.

Converged host: --check reports real drift only. Config validation is skipped (when: not ansible_check_mode) since it needs the rendered file on disk.

Fresh host: --check is expected to fail. It installs neither the repository, python3-debian nor the pdns-recursor package, so later tasks have nothing to inspect.

Package and Service State

  • pdns_rec_package_state: present, latest, absent, ...
  • pdns_rec_service_state (started, stopped, restarted, reloaded), pdns_rec_service_enabled, pdns_rec_service_masked.

pdns_rec_package_state: absent removes the packages, but the config and service tasks still run, so a full run fails validating the config because /usr/sbin/pdns_recursor is gone. Remove via the install path only:

ansible-playbook site.yml -e pdns_rec_package_state=absent --tags install

That run also removes /etc/systemd/system/<service name>.service.d/ and reloads systemd, so a later reinstall does not inherit the drop-in of the previous installation.

Handlers

Handlers run at the end of the play, and Ansible shares them between invocations of the same role. A role parameter read inside a handler resolves to the value of the last invocation, so with more than one invocation in a play the restart targets the wrong service or is collapsed into a single run. Set pdns_rec_flush_handlers: true to run meta: flush_handlers as the last task of the role, which restarts pdns_rec_service_name of that invocation:

Every instance needs its own service name and configuration file; pdns-recursor@<instance> runs pdns_recursor --config-name=<instance>, which reads <config dir>/recursor-<instance>.conf:

- hosts: recursors
  tasks:
    - name: Instance a, port 5301
      ansible.builtin.include_role:
        name: PowerDNS.pdns_recursor
      vars:
        pdns_rec_service_name: pdns-recursor@a
        pdns_rec_config_file: recursor-a.conf
        pdns_rec_flush_handlers: true
        pdns_rec_config:
          incoming:
            listen: [127.0.0.1]
            port: "5301"

    - name: Instance b, port 5302
      ansible.builtin.include_role:
        name: PowerDNS.pdns_recursor
      vars:
        pdns_rec_service_name: pdns-recursor@b
        pdns_rec_config_file: recursor-b.conf
        pdns_rec_flush_handlers: true
        pdns_rec_config:
          incoming:
            listen: [127.0.0.1]
            port: "5302"

meta: flush_handlers is play-wide: it also runs handlers that earlier roles in the same play notified. pdns_rec_disable_handlers: true skips the restart handlers entirely.

pdns_rec_flush_handlers defaults to false, which is correct for a single invocation and wrong for more than one: without it the pending restarts of every instance run once, at the end of the play, against the service name of the last invocation.

On systemd hosts the restart handler reloads the units in the same task, so a restart never runs against a unit systemd has not read. The reload happens even when pdns_rec_service_state: stopped keeps the service down, so the next manual start uses the drop-in this run wrote.

Ansible does not filter handlers by tag, so the restart handlers read ansible_skip_tags themselves: under --skip-tags service the service task is skipped and the handler restarts nothing, while the systemd units of that run are still reloaded. --tags config is unaffected and still restarts. pdns_rec_disable_handlers: true remains the way to apply configuration without restarting in a run that is not tag-filtered.

Example Playbooks

Bind to 203.0.113.53 on port 5300 and allow only traffic from the 198.51.100.0/24 subnet:

- hosts: pdns_recursors
  vars:
    pdns_rec_config:
      incoming:
        listen:
          - 203.0.113.53:5300
        allow_from: [ 198.51.100.0/24 ]
  roles:
    - { role: powerdns.pdns_recursor }

Allow traffic from multiple networks and set some custom ulimits overriding the default systemd service:

- hosts: pdns_recursors
  vars:
    pdns_rec_config:
      incoming:
        listen:
          - 203.0.113.53:5300
        allow_from: [ 198.51.100.0/24, 198.51.100.0/24 ]
    pdns_rec_service_overrides:
      LimitNOFILE: 10000
  roles:
    - { role: powerdns.pdns_recursor }

Forward queries for corp.example.net to a nameserver on localhost and queries for foo.example to other nameservers:

- hosts: pdns_recursors
  vars:
    pdns_rec_config:
      recursor:
        forward_zones:
          - zone: corp.example.net
            forwarders:
              - 127.0.0.1:5300
          - zone: foo.example
            forwarders:
              - "192.0.2.3"
              - "[2001:db8::2:3]"
  roles:
    - { role: powerdns.pdns_recursor }

Release notes

See the CHANGELOG.md.

Contributing

  • Every pull request must include a changelog fragment.
  • Each changelog entry must include a link to the pull request.
  • If the pull request addresses an issue, also include a link to the related issue.
  • Ensure each changelog entry starts with a lowercase letter (immediately after the dash) and ends with a period.
  • Copy fragment-example.yaml into changelogs/fragments and name it num-pr-title.yaml, where:
    • num is the pull request number
    • pr-title is the pull request title (use a short, filesystem-friendly version)

Testing

Tests are performed by Molecule.

$ pip install tox
$ tox

See molecule/README.md for the test layout, how to run a single leg, and what to change when a new release or operating system has to be covered.

License

See LICENSE.

About

PowerDNS Recursor Ansible role

Topics

Resources

Stars

61 stars

Watchers

8 watching

Forks

Releases

Packages

Used by

Contributors

Languages