PowerDNS Authoritative Server answers DNS queries for the zones you host, and it can store those zones in a SQL database instead of zone files. That makes it a good fit when records change often or are managed by software, because you can edit them with pdnsutil, SQL or a built-in REST API without reloading anything. In this tutorial you will install PowerDNS on Ubuntu 24.04 with a MariaDB backend, create a zone, manage records through the API and enable DNSSEC.

Prerequisites

To follow this tutorial, you will need:

  • A server running Ubuntu 24.04 LTS with a public IPv4 address, for example a CubePath VPS.
  • A non-root user with sudo privileges.
  • UFW enabled with SSH allowed.
  • A domain name you control. This tutorial uses your_domain and a name server called ns1.your_domain at your_server_ip.

For a domain to be served by your server in production, the registrar must list its name servers and you should run at least two of them on different networks. You can follow every step here, and test with dig, before changing anything at the registrar.

Step 1 - Installing MariaDB and PowerDNS

Install PowerDNS with the MySQL backend (which also works with MariaDB), the MariaDB server and dig for testing:

sudo apt update
sudo apt install pdns-server pdns-backend-mysql mariadb-server bind9-dnsutils

Check the installed version:

pdns_server --version 2>&1 | head -n 1
Sep 25 10:40:02 PowerDNS Authoritative Server 4.8.3 (C) 2001-2022 PowerDNS.COM BV

Ubuntu 24.04 ships PowerDNS 4.8. All commands in this tutorial work with it. If you need a newer release, PowerDNS publishes its own repositories at repo.powerdns.com.

The package starts PowerDNS with a BIND-file backend and no zones. Stop it while you configure the database:

sudo systemctl stop pdns

Step 2 - Creating the database

Open the MariaDB shell as root. On Ubuntu, the root account authenticates through the Unix socket, so no password is needed with sudo:

sudo mariadb

Create the database and a user that only PowerDNS will use. Replace your_db_password with a strong password:

CREATE DATABASE powerdns CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'pdns'@'localhost' IDENTIFIED BY 'your_db_password';
GRANT ALL PRIVILEGES ON powerdns.* TO 'pdns'@'localhost';
EXIT;

The backend package includes the table definitions. Find the schema file:

dpkg -L pdns-backend-mysql | grep 'schema.mysql.sql'
/usr/share/pdns-backend-mysql/schema/schema.mysql.sql

Load it into the new database, using the path printed above:

sudo mariadb powerdns < /usr/share/pdns-backend-mysql/schema/schema.mysql.sql

Verify that the tables exist:

sudo mariadb powerdns -e 'SHOW TABLES;'
+--------------------+
| Tables_in_powerdns |
+--------------------+
| comments           |
| cryptokeys         |
| domainmetadata     |
| domains            |
| records            |
| supermasters       |
| tsigkeys           |
+--------------------+

Step 3 - Configuring PowerDNS

Ubuntu's /etc/powerdns/pdns.conf includes every file in /etc/powerdns/pdns.d/. The package puts the BIND backend there; remove it so only the database backend is loaded:

sudo rm -f /etc/powerdns/pdns.d/bind.conf

Create the backend configuration:

sudo nano /etc/powerdns/pdns.d/gmysql.conf
launch+=gmysql
gmysql-host=127.0.0.1
gmysql-port=3306
gmysql-dbname=powerdns
gmysql-user=pdns
gmysql-password=your_db_password
gmysql-dnssec=yes

The file contains the database password, so make it readable only by root and the pdns group that the service runs as:

sudo chown root:pdns /etc/powerdns/pdns.d/gmysql.conf
sudo chmod 640 /etc/powerdns/pdns.d/gmysql.conf

Next, tell PowerDNS which addresses to listen on. Ubuntu's systemd-resolved already listens on 127.0.0.53:53 for the server's own lookups, so binding PowerDNS only to the public address avoids a port conflict without touching the system resolver. Open the main configuration:

sudo nano /etc/powerdns/pdns.conf

Set these options, replacing the commented defaults if present:

local-address=your_server_ip
local-port=53

Start PowerDNS and check its log:

sudo systemctl start pdns
sudo journalctl -u pdns -n 20 --no-pager
... gmysql Connection successful. Connected to database 'powerdns' on '127.0.0.1'.
... Listening on your_server_ip:53
... Creating backend connection for TCP
...

sudo pdns_control rping answers PONG when the server is up. Open port 53 over UDP and TCP; DNS uses TCP for large answers and zone transfers:

sudo ufw allow 53/udp
sudo ufw allow 53/tcp

Step 4 - Creating a zone and records

pdnsutil writes zones directly to the database. Create the zone with ns1.your_domain as its primary name server. This creates the SOA and NS records:

sudo pdnsutil create-zone your_domain ns1.your_domain

Add the address of the name server itself, plus the records for a website and mail. The syntax is pdnsutil add-record ZONE NAME TYPE [TTL] CONTENT, where @ means the zone apex:

sudo pdnsutil add-record your_domain ns1 A 3600 your_server_ip
sudo pdnsutil add-record your_domain @ A 3600 203.0.113.10
sudo pdnsutil add-record your_domain www CNAME 3600 your_domain.
sudo pdnsutil add-record your_domain @ MX 3600 "10 mail.your_domain."
sudo pdnsutil add-record your_domain mail A 3600 203.0.113.20
sudo pdnsutil add-record your_domain @ TXT 3600 '"v=spf1 mx -all"'

Two details are easy to get wrong: host names inside record content must end with a dot, and TXT content needs its own double quotes, which is why it is wrapped in single quotes for the shell.

add-record does not change the SOA serial, which secondary servers use to detect updates. Increase it, then check the zone for errors:

sudo pdnsutil increase-serial your_domain
sudo pdnsutil check-zone your_domain
Checked 8 records of 'your_domain', 0 errors, 0 warnings.

List the zone:

sudo pdnsutil list-zone your_domain

Query the server directly. The aa flag in the answer means it is authoritative for the zone:

dig @your_server_ip www.your_domain
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
...
;; ANSWER SECTION:
www.your_domain.     3600    IN      CNAME   your_domain.
your_domain.         3600    IN      A       203.0.113.10

To edit records interactively in a text editor instead, run sudo pdnsutil edit-zone your_domain, which checks the zone before applying your changes.

Step 5 - Enabling the REST API

The API lets scripts and control panels manage zones over HTTP. Generate a random API key:

openssl rand -hex 32

Add the API settings to the main configuration:

sudo nano /etc/powerdns/pdns.conf
api=yes
api-key=your_api_key
webserver=yes
webserver-address=127.0.0.1
webserver-port=8081
webserver-allow-from=127.0.0.1,::1

This binds the API to localhost only. To use it from another machine, reach it through an SSH tunnel or a reverse proxy with TLS rather than exposing port 8081. Protect the file, since it now contains the key, and restart PowerDNS:

sudo chown root:pdns /etc/powerdns/pdns.conf
sudo chmod 640 /etc/powerdns/pdns.conf
sudo systemctl restart pdns

Store the key in a shell variable for the next commands, and list the zones:

PDNS_KEY=your_api_key
curl -s -H "X-API-Key: $PDNS_KEY" http://127.0.0.1:8081/api/v1/servers/localhost/zones | python3 -m json.tool
[
    {
        "account": "",
        "dnssec": false,
        "id": "your_domain.",
        "kind": "Native",
        "name": "your_domain.",
        ...
    }
]

Add or replace a record with a PATCH request. The API always uses fully qualified names with a trailing dot, and REPLACE sets the complete list of records for that name and type:

curl -s -X PATCH -H "X-API-Key: $PDNS_KEY" -H "Content-Type: application/json" \
  http://127.0.0.1:8081/api/v1/servers/localhost/zones/your_domain. \
  -d '{"rrsets": [{"name": "api.your_domain.", "type": "A", "ttl": 300, "changetype": "REPLACE", "records": [{"content": "203.0.113.30", "disabled": false}]}]}'

A successful change returns HTTP 204 with an empty body. The record is live immediately:

dig @your_server_ip api.your_domain +short
203.0.113.30

To remove it, send the same request with "changetype": "DELETE" and no ttl or records.

Step 6 - Signing the zone with DNSSEC

DNSSEC lets resolvers verify that answers really come from your server. PowerDNS signs answers on the fly, so enabling it takes one command. It creates a signing key using ECDSA P-256 by default:

sudo pdnsutil secure-zone your_domain
sudo pdnsutil rectify-zone your_domain

rectify-zone recalculates the ordering and authentication data that DNSSEC needs. Changes made through the API are rectified automatically, but run it again after adding records with pdnsutil add-record or SQL.

Show the keys and the DS records:

sudo pdnsutil show-zone your_domain
This is a Native zone
Metadata items: None
Zone has following allowed TSIG key(s): 
Zone is not presigned
keys: 
ID = 1 (CSK), flags = 257, tag = 34512, algo = 13, bits = 256    Active   Published  ( ECDSAP256SHA256 ) 
CSK DNSKEY = your_domain. IN DNSKEY 257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ== ; ( ECDSAP256SHA256 )
DS = your_domain. IN DS 34512 13 2 2e1cdd0a3e8a5d3c7b8a0f4e2d1c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c ; ( SHA256 digest )
...

Confirm that answers are now signed; the response includes an RRSIG record:

dig @your_server_ip your_domain SOA +dnssec +multi

Troubleshooting

PowerDNS does not start and the log says Unable to bind UDP socket to 'your_server_ip:53': Address already in use: another DNS server is listening on that address. Find it with sudo ss -ulpn 'sport = :53'.

gmysql Connection failed: Could not connect to database: the credentials in gmysql.conf do not match the database user, or MariaDB is not running. Test them with mariadb -u pdns -p -h 127.0.0.1 powerdns -e 'SELECT COUNT(*) FROM domains;'.

Queries return REFUSED: the server is not authoritative for that name. Check the zone exists with sudo pdnsutil list-all-zones and that the name you query is inside it. PowerDNS Authoritative never performs recursive lookups for other domains.

The API returns 401 Unauthorized: the X-API-Key header does not match api-key. A Connection refused means webserver=yes is missing or PowerDNS was not restarted.

check-zone reports errors after SQL edits: run sudo pdnsutil rectify-zone your_domain and check again.

Conclusion

You installed PowerDNS Authoritative Server on Ubuntu 24.04 with a MariaDB backend, served a zone with pdnsutil, managed records over the REST API and signed the zone with DNSSEC. To take it to production, add a second name server on a different network using MariaDB replication or PowerDNS zone transfers, delegate the domain at your registrar, then publish the DS record. You can also put PowerDNS Admin or your own tooling on top of the API to manage records without shell access.