An Ansible role created by the folks behind PowerDNS to setup the PowerDNS Recursor.
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.
None.
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: trueBy 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: falseInstall 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: falseAllow 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: falseDisable automated service restart on configuration changes.
pdns_rec_flush_handlers: falseRun 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.0setuid 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/8Note 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: falseAdditional 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.2Additional 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.
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.
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.
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 installThat 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 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.
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 }See the CHANGELOG.md.
- 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.yamlintochangelogs/fragmentsand name itnum-pr-title.yaml, where:numis the pull request numberpr-titleis the pull request title (use a short, filesystem-friendly version)
Tests are performed by Molecule.
$ pip install tox
$ toxSee 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.
See LICENSE.