PowerDNS Authoritative Server
Unbound handles recursive resolution and DNSSEC validation. PowerDNS handles the authoritative side: it holds the zone files for the internal domain and (as a hidden primary) for the public domain. When Unbound encounters a name under internal.yourdomain.net, it forwards to PowerDNS on port 5300. When a public resolver anywhere in the world looks up a record for yourdomain.net, it ultimately reaches the registrar’s nameservers, which get their data from PowerDNS via zone transfer.
This article installs and configures the PowerDNS Authoritative Server with a MariaDB backend, creates the initial zones, and verifies the setup. DNSSEC signing is covered in the next article. Zone management via the PowerDNS API and PowerDNS-Admin are covered in the articles after that.
Prerequisites
MariaDB must be installed and running. The MariaDB article in this series covers setup. The database server should be listening on localhost before proceeding.
Unbound should already be configured as described in the previous article. PowerDNS and Unbound share the server but listen on different ports: Unbound owns port 53, PowerDNS listens on port 5300.
Installation
PowerDNS is in Ubuntu’s default repositories as pdns-server. Install the server and the MySQL/MariaDB backend:
sudo apt install pdns-server pdns-backend-mysql
During installation, the package may try to start PowerDNS immediately. This will fail because the database backend is not yet configured. That is expected. Do not worry about it.
Disable the default pdns.simplebind.conf backend. PowerDNS can only use one backend at a time, and the MySQL backend replaces the default BIND file backend:
sudo rm /etc/powerdns/pdns.d/pdns.simplebind.conf
Database setup
Create the database and user in MariaDB:
sudo mariadb << 'EOF'
CREATE DATABASE IF NOT EXISTS powerdns
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS 'pdns'@'localhost'
IDENTIFIED BY 'CHANGE-THIS-TO-A-STRONG-PASSWORD';
GRANT ALL PRIVILEGES ON powerdns.* TO 'pdns'@'localhost';
FLUSH PRIVILEGES;
EOF
Import the PowerDNS schema. The schema file is installed by the pdns-backend-mysql package:
sudo mariadb powerdns < /usr/share/pdns-backend-mysql/schema/schema.mysql.sql
Confirm the tables were created:
sudo mariadb powerdns -e "SHOW TABLES;"
Expected output:
+--------------------+
| Tables_in_powerdns |
+--------------------+
| comments |
| cryptokeys |
| domainmetadata |
| domains |
| records |
| supermasters |
| tsigkeys |
+--------------------+
Main configuration
The main configuration file is /etc/powerdns/pdns.conf. Back up the default and write a clean version:
sudo cp /etc/powerdns/pdns.conf /etc/powerdns/pdns.conf.bak
sudo tee /etc/powerdns/pdns.conf << 'EOF'
#######################################################################
# PowerDNS Authoritative Server configuration
# /etc/powerdns/pdns.conf
#######################################################################
# Include files from the pdns.d directory
config-dir=/etc/powerdns
include-dir=/etc/powerdns/pdns.d
# Run as the pdns user and group (created by the package)
setuid=pdns
setgid=pdns
# Listen on loopback only on port 5300.
# Port 53 is owned by Unbound.
# Unbound forwards internal zone queries here.
# Do not expose this port externally.
local-address=127.0.0.1
local-port=5300
# Allow external queries from the public internet on standard port 53
# only if this server will also act as a secondary for the public zone.
# Leave commented out unless explicitly needed.
# local-address=0.0.0.0
# local-port=53
# Disable recursion. PowerDNS Authoritative Server does not recurse.
# Recursion is Unbound's job.
recursor=no
# This server is the primary (master) for its zones.
primary=yes
# Logging
loglevel=4
log-dns-details=no
log-dns-queries=no
# Security: do not advertise the version string
version-string=powerdns
# Disable AXFR (zone transfers) by default.
# Enable selectively per zone when setting up secondary servers.
disable-axfr=yes
# Cache settings
cache-ttl=20
query-cache-ttl=20
negquery-cache-ttl=60
EOF
sudo chmod 640 /etc/powerdns/pdns.conf
sudo chown pdns:pdns /etc/powerdns/pdns.conf
Database backend configuration
Create the backend configuration file separately. This keeps credentials out of the main config:
sudo tee /etc/powerdns/pdns.d/pdns.local.gmysql.conf << 'EOF'
# gmysql (MySQL/MariaDB) backend configuration
launch=gmysql
gmysql-host=127.0.0.1
gmysql-port=3306
gmysql-dbname=powerdns
gmysql-user=pdns
gmysql-password=CHANGE-THIS-TO-A-STRONG-PASSWORD
gmysql-dnssec=yes
EOF
sudo chmod 640 /etc/powerdns/pdns.d/pdns.local.gmysql.conf
sudo chown pdns:pdns /etc/powerdns/pdns.d/pdns.local.gmysql.conf
The password must match the one set during the database setup step.
gmysql-dnssec=yes enables the DNSSEC-related columns in the schema. This is needed even if DNSSEC is not configured yet, because the schema tables are always present.
REST API configuration
The PowerDNS REST API is how zone records will be managed programmatically, and how PowerDNS-Admin communicates with the server. Enable it now even though PowerDNS-Admin is set up in a later article:
# Generate a strong API key
PDNS_API_KEY=$(openssl rand -hex 32)
echo "API key: $PDNS_API_KEY"
echo "(Save this in KeePassXC under Infrastructure > DNS > PowerDNS API Key)"
sudo tee /etc/powerdns/pdns.d/pdns.local.api.conf << EOF
# PowerDNS REST API configuration
api=yes
api-key=${PDNS_API_KEY}
# Webserver for the API. Bind to loopback only.
# nginx proxies external access when needed.
webserver=yes
webserver-address=127.0.0.1
webserver-port=8081
webserver-allow-from=127.0.0.1
EOF
sudo chmod 640 /etc/powerdns/pdns.d/pdns.local.api.conf
sudo chown pdns:pdns /etc/powerdns/pdns.d/pdns.local.api.conf
systemd override: MariaDB dependency
PowerDNS needs the database to be running before it starts. The package’s systemd unit does not express this dependency by default. Add an override:
sudo systemctl edit pdns.service
This opens a text editor. Add the following:
[Unit]
BindsTo=mariadb.service
After=mariadb.service
Save and close. The override is written to /etc/systemd/system/pdns.service.d/override.conf. Confirm it was applied:
systemctl show pdns.service | grep -E "After=|BindsTo="
Start PowerDNS
Test the configuration by running PowerDNS in the foreground first:
sudo systemctl stop pdns
sudo pdns_server --daemon=no --guardian=no --loglevel=9
Look for these lines in the output:
gmysql Connection successful. Connected to database 'powerdns' on '127.0.0.1'.
Done launching threads, ready to distribute questions
If the connection fails, check the MariaDB credentials and that the powerdns database exists. Press Ctrl-C to stop the test run.
If the test run succeeded, start the service properly:
sudo systemctl enable --now pdns
sudo systemctl status pdns
Confirm it is listening on port 5300:
sudo ss -tlnup | grep 5300
Expected output shows pdns_server bound to 127.0.0.1:5300.
Creating zones with pdnsutil
pdnsutil is the command-line tool for managing PowerDNS zones and records. It communicates directly with the database rather than going through the API, making it suitable for initial setup.
Create the internal zone
sudo pdnsutil create-zone internal.yourdomain.net ns1.internal.yourdomain.net
Add the initial SOA and NS records:
# SOA record
sudo pdnsutil add-record internal.yourdomain.net @ SOA \
"ns1.internal.yourdomain.net. hostmaster.yourdomain.net. 1 10800 3600 604800 3600"
# NS record
sudo pdnsutil add-record internal.yourdomain.net @ NS \
"ns1.internal.yourdomain.net."
# The nameserver's own A record
sudo pdnsutil add-record internal.yourdomain.net ns1 A \
"10.1.0.10"
# The primary server's hostname
sudo pdnsutil add-record internal.yourdomain.net server A \
"10.1.0.10"
Replace 10.1.0.10 with the actual IP address of the primary server.
Create the reverse zone
Reverse DNS for the primary site’s subnet:
sudo pdnsutil create-zone 1.10.in-addr.arpa ns1.internal.yourdomain.net
sudo pdnsutil add-record 1.10.in-addr.arpa @ SOA \
"ns1.internal.yourdomain.net. hostmaster.yourdomain.net. 1 10800 3600 604800 3600"
sudo pdnsutil add-record 1.10.in-addr.arpa @ NS \
"ns1.internal.yourdomain.net."
# PTR for the primary server (last octet of 10.1.0.10)
sudo pdnsutil add-record 1.10.in-addr.arpa 10.0 PTR \
"server.internal.yourdomain.net."
The reverse zone name for 10.1.0.0/16 is 1.10.in-addr.arpa. Adjust the zone name and PTR records to match the actual subnet structure.
Create the public zone
The public zone is a hidden primary: PowerDNS holds the records, but the registrar’s nameservers are the ones that appear in public DNS. Zone transfers from this server to the registrar’s nameservers (or a secondary provider) make the data public.
sudo pdnsutil create-zone yourdomain.net ns1.yourdomain.net
sudo pdnsutil add-record yourdomain.net @ SOA \
"ns1.yourdomain.net. hostmaster.yourdomain.net. 1 10800 3600 604800 3600"
# NS records point to the registrar's nameservers, not this server
sudo pdnsutil add-record yourdomain.net @ NS "ns1.registrar.example."
sudo pdnsutil add-record yourdomain.net @ NS "ns2.registrar.example."
Replace the NS records with the actual nameserver addresses from the registrar’s delegation. Add public-facing records as the services that need them are set up:
# Mail exchanger
sudo pdnsutil add-record yourdomain.net @ MX "10 mail.yourdomain.net."
# SPF record for mail authentication
sudo pdnsutil add-record yourdomain.net @ TXT "v=spf1 mx -all"
Verify with dig
Query PowerDNS directly on port 5300:
# Internal zone SOA
dig @127.0.0.1 -p 5300 internal.yourdomain.net SOA
# A record lookup
dig @127.0.0.1 -p 5300 server.internal.yourdomain.net A
# Reverse lookup
dig @127.0.0.1 -p 5300 -x 10.1.0.10
# Public zone SOA
dig @127.0.0.1 -p 5300 yourdomain.net SOA
All queries should return answers with status: NOERROR. The aa (Authoritative Answer) flag should appear in the response header.
Now test via Unbound on port 53, which should forward internal names to PowerDNS:
dig @127.0.0.1 server.internal.yourdomain.net A
If this returns the correct answer, Unbound’s forward zone configuration is working.
Verify the API
PDNS_API_KEY=$(sudo grep api-key /etc/powerdns/pdns.d/pdns.local.api.conf | awk '{print $NF}')
curl -s \
-H "X-API-Key: ${PDNS_API_KEY}" \
http://127.0.0.1:8081/api/v1/servers/localhost \
| python3 -m json.tool
The response should show the PowerDNS version, backend type, and available API endpoints. If the API key cannot be retrieved with grep, paste it manually.
List zones via the API:
curl -s \
-H "X-API-Key: ${PDNS_API_KEY}" \
http://127.0.0.1:8081/api/v1/servers/localhost/zones \
| python3 -m json.tool
Adding records day-to-day
pdnsutil add-record is the quickest way to add records without needing the API or web interface. The general form is:
sudo pdnsutil add-record ZONE NAME TYPE [TTL] CONTENT
Examples:
# Add a new internal host
sudo pdnsutil add-record internal.yourdomain.net nextcloud A 10.1.0.20
# Add a CNAME alias
sudo pdnsutil add-record internal.yourdomain.net cloud CNAME nextcloud.internal.yourdomain.net.
# Add a PTR record
sudo pdnsutil add-record 1.10.in-addr.arpa 20.0 PTR nextcloud.internal.yourdomain.net.
# Add a public TXT record
sudo pdnsutil add-record yourdomain.net @ TXT "v=spf1 mx include:mailprovider.example -all"
After adding records, increment the SOA serial manually or use pdnsutil rectify-zone to let PowerDNS handle it:
sudo pdnsutil rectify-zone internal.yourdomain.net
Listing and editing records
List all records in a zone:
sudo pdnsutil list-zone internal.yourdomain.net
Delete a specific record:
sudo pdnsutil delete-rrset internal.yourdomain.net nextcloud A
Replace a record (delete then add):
sudo pdnsutil delete-rrset internal.yourdomain.net nextcloud A
sudo pdnsutil add-record internal.yourdomain.net nextcloud A 10.1.0.25
What is next
PowerDNS is now running as the authoritative server for internal and public zones. The next steps in the DNS section are:
DNSSEC: sign the public zone with PowerDNS and publish the DS record at the registrar to establish the chain of trust that Unbound can validate.
PowerDNS-Admin: a web interface for managing zones without dropping to the command line. Useful for adding records from a browser and giving a visual overview of what is in each zone.
Dynamic DNS: a script to update the public A record automatically when the public IP address changes.
PowerDNS is listening on
127.0.0.1:5300, not on the network. External devices cannot reach it directly, which is correct. Unbound forwards internal queries to it over loopback. If a service on the network cannot resolve an internal name, the cause is always in Unbound’s forward zone configuration or Unbound itself, not PowerDNS.