This guide provides a complete Ansible playbook to install PowerDNS Recursor with official repository setup, distro-aware package handling, and production-ready configuration for Debian 10+, Ubuntu 20.04+, and RHEL 9+ compatible systems.
---
- name: Install PowerDNS Recursor with official repository
hosts: powerdns-recursor
become: true
vars:
# PowerDNS Recursor version track (53 = 5.3.x, 52 = 5.2.x, etc.)
powerdns_recursor_version: "53"
# Network settings
recursor_allow_from:
- "127.0.0.0/8"
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
# Forwarding settings
recursor_forward_zones: "=223.5.5.5,1.0.0.1"
# DNSSEC
recursor_dnssec: "validate"
# Web server/API
recursor_webserver: true
recursor_webserver_address: "127.0.0.1"
recursor_webserver_port: 8082
recursor_webserver_password: "change_this_secure_password"
# Performance
recursor_threads: 4
recursor_cache_size: 1000000
# Logging
recursor_loglevel: 6
handlers:
- name: restart pdns-recursor
systemd:
name: pdns-recursor
state: restarted
daemon_reload: true
tasks:
- name: Add PowerDNS repository (Debian/Ubuntu)
block:
- name: Install prerequisites
apt:
name:
- gnupg
- curl
- apt-transport-https
- software-properties-common
state: present
update_cache: true
when: ansible_distribution in ['Debian', 'Ubuntu']
- name: Create keyrings directory
file:
path: /etc/apt/keyrings
state: directory
mode: "0755"
when: ansible_distribution in ['Debian', 'Ubuntu']
- name: Import PowerDNS GPG key (Key ID: FD380FBB)
ansible.builtin.key:
url: https://repo.powerdns.com/FD380FBB-pub.asc
keyring: /etc/apt/keyrings/pdns-archive-keyring.gpg
state: present
when: ansible_distribution in ['Debian', 'Ubuntu']
- name: Add PowerDNS Recursor repository
apt_repository:
repo: "deb [signed-by=/etc/apt/keyrings/pdns-archive-keyring.gpg] https://repo.powerdns.com/{{ ansible_distribution|lower }}-{{ ansible_distribution_release }}-rec-{{ powerdns_recursor_version }}/ {{ ansible_distribution_release }} main"
state: present
filename: pdns-recursor
when: ansible_distribution in ['Debian', 'Ubuntu']
- name: Add APT pinning for PowerDNS packages
copy:
dest: /etc/apt/preferences.d/pdns-recursor
content: |
Package: pdns-recursor
Pin: origin repo.powerdns.com
Pin-Priority: 600
mode: "0644"
when: ansible_distribution in ['Debian', 'Ubuntu']
when: ansible_os_family == "Debian"
- name: Add PowerDNS repository (RHEL/CentOS/AlmaLinux/Rocky)
block:
- name: Install EPEL release
package:
name: epel-release
state: present
when: ansible_os_family == "RedHat"
- name: Add PowerDNS Recursor repository (Enterprise Linux)
get_url:
url: "https://repo.powerdns.com/repo-files/el-rec-{{ powerdns_recursor_version }}.repo"
dest: "/etc/yum.repos.d/powerdns-recursor.repo"
mode: "0644"
when: ansible_os_family == "RedHat"
when: ansible_os_family == "RedHat"
- name: Update package cache (Debian/Ubuntu)
apt:
update_cache: true
when: ansible_os_family == "Debian"
- name: Install PowerDNS Recursor
package:
name:
- pdns-recursor
state: present
notify: restart pdns-recursor
- name: Create configuration directory
file:
path: /etc/powerdns
state: directory
mode: "0755"
owner: pdns-recursor
group: pdns-recursor
- name: Configure PowerDNS Recursor
template:
src: recursor.conf.j2
dest: /etc/powerdns/recursor.conf
owner: pdns-recursor
group: pdns-recursor
mode: "0640"
notify: restart pdns-recursor
- name: Enable and start PowerDNS Recursor service
systemd:
name: pdns-recursor
state: started
enabled: true
daemon_reload: true
- name: Wait for service to be ready
wait_for:
port: 53
host: "{{ ansible_default_ipv4.address | default('127.0.0.1') }}"
delay: 5
timeout: 30
delegate_to: localhost
- name: Verify PowerDNS Recursor installation
command: "pdns_recursor --version"
register: recursor_version_output
changed_when: false
failed_when: false
- name: Display PowerDNS Recursor version
debug:
msg: "PowerDNS Recursor version: {{ recursor_version_output.stdout }}"
when: recursor_version_output.stdout is defined
Create this template file in your Ansible roles/templates directory:
# PowerDNS Recursor Configuration
# Generated by Ansible
# Network settings
local-address=0.0.0.0,::
local-port=53
# Access control - allow from trusted networks only
allow-from={{ recursor_allow_from | join(',') }}
# Forwarding configuration
{% if recursor_forward_zones is defined %}
forward-zones={{ recursor_forward_zones }}
{% endif %}
{% if recursor_forward_zones_recurse is defined %}
forward-zones-recurse={{ recursor_forward_zones_recurse }}
{% endif %}
# DNSSEC validation (recommended for production)
dnssec={{ recursor_dnssec | default('validate') }}
# Performance tuning
threads={{ recursor_threads | default(4) }}
max-cache-entries={{ recursor_cache_size | default(1000000) }}
# Web server and API
{% if recursor_webserver %}
webserver=yes
webserver-address={{ recursor_webserver_address | default('127.0.0.1') }}
webserver-port={{ recursor_webserver_port | default(8082) }}
webserver-password={{ recursor_webserver_password }}
{% else %}
webserver=no
{% endif %}
# Logging
loglevel={{ recursor_loglevel | default(6) }}
log-common-troubles=yes
# Security settings
setuid=pdns-recursor
setgid=pdns-recursor
daemon=yes
socket-dir=/var/run/powerdns
# Rate limiting basics
max-mthreads=2000
max-qperq=50
max-tcp-clients=128
# Group vars for enterprise deployment
recursor_allow_from:
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
recursor_forward_zones_recurse: "=223.5.5.5,1.0.0.1"
recursor_dnssec: "validate"
recursor_threads: 8
recursor_cache_size: 2000000
recursor_webserver: true
recursor_webserver_address: "10.0.0.1"
recursor_webserver_allow_from:
- "10.0.0.0/8"
# Simple home resolver
recursor_allow_from:
- "127.0.0.0/8"
- "192.168.0.0/16"
recursor_forward_zones: "=223.5.5.5,1.0.0.1,8.8.8.8"
recursor_dnssec: "validate"
recursor_threads: 2
recursor_cache_size: 500000
recursor_webserver: true
recursor_webserver_address: "127.0.0.1"
# Public resolver - requires Lua rate limiting
recursor_allow_from:
- "0.0.0.0/0"
- "::/0"
recursor_dnssec: "validate"
recursor_threads: 16
recursor_cache_size: 5000000
recursor_webserver: true
recursor_webserver_address: "127.0.0.1"
# Additional hardening
recursor_max_mthreads: 10000
recursor_max_tcp_clients: 500
# Check for community DNS roles
ansible-galaxy search powerdns
# Or create your own role structure
ansible-galaxy init roles/powerdns-recursor
roles/powerdns-recursor/
├── defaults/main.yml
├── handlers/main.yml
├── meta/main.yml
├── tasks/
│ ├── main.yml
│ ├── install.yml
│ ├── configure.yml
│ └── service.yml
├── templates/
│ └── recursor.conf.j2
└── vars/main.yml
---
# PowerDNS Recursor version
powerdns_recursor_version: "53"
# Network settings
recursor_allow_from:
- "127.0.0.0/8"
- "10.0.0.0/8"
- "192.168.0.0/16"
# Forwarding
recursor_forward_zones: "=223.5.5.5,1.0.0.1"
# DNSSEC
recursor_dnssec: "validate"
# Performance
recursor_threads: 4
recursor_cache_size: 1000000
# Web server
recursor_webserver: true
recursor_webserver_address: "127.0.0.1"
recursor_webserver_port: 8082
recursor_webserver_password: "changeme"
# Logging
recursor_loglevel: 6
all:
children:
powerdns-recursor:
hosts:
resolver1.example.com:
ansible_host: 192.168.1.10
recursor_threads: 8
recursor_cache_size: 2000000
resolver2.example.com:
ansible_host: 192.168.1.11
recursor_threads: 8
recursor_cache_size: 2000000
# Run the complete playbook
ansible-playbook -i hosts.yml powerdns-recursor-playbook.yml
# Run with specific tags
ansible-playbook -i hosts.yml powerdns-recursor-playbook.yml --tags "install"
# Run with vault password (if using encrypted variables)
ansible-playbook -i hosts.yml powerdns-recursor-playbook.yml --ask-vault-pass
# Dry-run to check what would change
ansible-playbook -i hosts.yml powerdns-recursor-playbook.yml --check
# Limit to specific hosts
ansible-playbook -i hosts.yml powerdns-recursor-playbook.yml --limit resolver1.example.com
Use Ansible Vault for sensitive data:
ansible-vault encrypt_string --name 'recursor_webserver_password' 'your_secret_password'
Limit API exposure:
recursor_webserver_address: "127.0.0.1"
Restrict query sources:
recursor_allow_from:
- "10.0.0.0/8"
- "192.168.0.0/16"
Enable DNSSEC validation:
recursor_dnssec: "validate"
Add monitoring configuration:
- name: Install Prometheus exporter configuration
copy:
content: |
# PowerDNS Recursor metrics
webserver=yes
webserver-address=127.0.0.1
webserver-port=8082
dest: /etc/powerdns/prometheus.conf
owner: pdns-recursor
group: pdns-recursor
mode: "0640"
notify: restart pdns-recursor
- name: Install backup script
template:
src: backup_recursor.sh.j2
dest: /usr/local/bin/backup_recursor.sh
mode: "0755"
owner: root
group: root
- name: Install cron job for backups
cron:
name: "PowerDNS Recursor config backup"
minute: "0"
hour: "2"
job: "/usr/local/bin/backup_recursor.sh"
user: root
For advanced rate limiting or query handling:
- name: Configure Lua scripting
block:
- name: Create Lua configuration directory
file:
path: /etc/powerdns/lua
state: directory
mode: "0755"
- name: Deploy Lua rate limiting script
template:
src: ratelimit.lua.j2
dest: /etc/powerdns/lua/ratelimit.lua
mode: "0644"
notify: restart pdns-recursor
- name: Enable Lua configuration in recursor.conf
lineinfile:
path: /etc/powerdns/recursor.conf
line: "lua-config-file=/etc/powerdns/lua/ratelimit.lua"
insertafter: "# Lua configuration"
notify: restart pdns-recursor
Example ratelimit.lua.j2:
-- Rate limiting configuration
local rates = {
["10.0.0.0/8"] = 1000, -- Internal: 1000 qps
["192.168.0.0/16"] = 1000,
["0.0.0.0/0"] = 50, -- Default: 50 qps
}
function preresolve(dq)
-- Rate limiting logic here
return false
end
Repository access issues:
# Verify GPG key is installed correctly
gpg --show-keys /etc/apt/keyrings/pdns-archive-keyring.gpg
# Check repository configuration
ansible powerdns-recursor -m setup -a "filter=ansible_distribution*"
# Verify repository is accessible
ansible powerdns-recursor -a "apt update" --become
Service startup failures:
# Check service status
ansible powerdns-recursor -a "systemctl status pdns-recursor" --become
# Check logs
ansible powerdns-recursor -a "journalctl -u pdns-recursor --no-pager" --become
Configuration validation:
# Validate configuration
ansible powerdns-recursor -a "pdns_recursor --config" --become
DNSSEC validation issues:
# Test DNSSEC validation
dig @resolver1.example.com dnssec-failed.org
# Should return SERVFAIL if validation working
Forwarding not working:
# Check forward-zones configuration
ansible powerdns-recursor -a "grep forward-zones /etc/powerdns/recursor.conf" --become
- name: Test PowerDNS Recursor deployment
hosts: powerdns-recursor
become: false
tasks:
- name: Test basic DNS resolution
command: "dig @{{ ansible_default_ipv4.address | default('127.0.0.1') }} example.com +short"
register: dns_test
changed_when: false
failed_when: false
- name: Display DNS test result
debug:
msg: "DNS resolution test: {{ dns_test.stdout }}"
when: dns_test.stdout is defined
- name: Test API endpoint
uri:
url: "http://{{ ansible_default_ipv4.address | default('127.0.0.1') }}:8082/api/v1/servers/localhost"
method: GET
headers:
X-API-Key: "{{ recursor_webserver_password }}"
status_code: 200
register: api_test
changed_when: false
failed_when: false
delegate_to: localhost
- name: Display API test result
debug:
msg: "API test successful"
when: api_test.status == 200
Beyond this playbook, we offer:
Contact our automation team: office@linux-server-admin.com | Contact Page