$ git diff --patch-with-stat --summary cd413def865b9bde4546a2f64167d7ffa679a8cd..34a1cd5340ac140046350ee03f05b671f3cbd133
pdns-5.0.2-defaults.patch | 106 +++++++++++++++++++++++++++++++++++++
pdns-drop-privs.patch | 130 ++++++++++++++++++++++++++++++++++++++++++++++
pdns.service | 11 ++++
pdns.spec | 24 ++++++---
4 files changed, 263 insertions(+), 8 deletions(-)
create mode 100644 pdns-5.0.2-defaults.patch
create mode 100644 pdns-drop-privs.patch
diff --git a/pdns-5.0.2-defaults.patch b/pdns-5.0.2-defaults.patch
new file mode 100644
index 0000000..797d5d1
--- /dev/null
+++ b/pdns-5.0.2-defaults.patch
@@ -0,0 +1,106 @@
+diff -up pdns-5.0.2/modules/bindbackend/bindbackend2.cc.omv~ pdns-5.0.2/modules/bindbackend/bindbackend2.cc
+--- pdns-5.0.2/modules/bindbackend/bindbackend2.cc.omv~ 2026-02-17 21:00:13.731168972 +0100
++++ pdns-5.0.2/modules/bindbackend/bindbackend2.cc 2026-02-17 21:04:14.711950289 +0100
+@@ -1577,7 +1577,7 @@ public:
+ void declareArguments(const string& suffix = "") override
+ {
+ declare(suffix, "ignore-broken-records", "Ignore records that are out-of-bound for the zone.", "no");
+- declare(suffix, "config", "Location of named.conf", "");
++ declare(suffix, "config", "Location of named.conf", "/srv/powerdns/named.conf");
+ declare(suffix, "check-interval", "Interval for zonefile changes", "0");
+ declare(suffix, "autoprimary-config", "Location of (part of) named.conf where pdns can write zone-statements to", "");
+ declare(suffix, "autoprimaries", "List of IP-addresses of autoprimaries", "");
+diff -up pdns-5.0.2/modules/geoipbackend/geoipbackend.cc.omv~ pdns-5.0.2/modules/geoipbackend/geoipbackend.cc
+--- pdns-5.0.2/modules/geoipbackend/geoipbackend.cc.omv~ 2026-02-17 21:04:23.103819364 +0100
++++ pdns-5.0.2/modules/geoipbackend/geoipbackend.cc 2026-02-17 21:10:07.408826026 +0100
+@@ -1213,9 +1213,9 @@ public:
+
+ void declareArguments(const string& suffix = "") override
+ {
+- declare(suffix, "zones-file", "YAML file to load zone(s) configuration", "");
+- declare(suffix, "database-files", "File(s) to load geoip data from ([driver:]path[;opt=value]", "");
+- declare(suffix, "dnssec-keydir", "Directory to hold dnssec keys (also turns DNSSEC on)", "");
++ declare(suffix, "zones-file", "YAML file to load zone(s) configuration", "/srv/powerdns/geoip-zones.yaml");
++ declare(suffix, "database-files", "File(s) to load geoip data from ([driver:]path[;opt=value]", "/usr/share/GeoIP/GeoLite2-Country.mmdb");
++ declare(suffix, "dnssec-keydir", "Directory to hold dnssec keys (also turns DNSSEC on)", "/srv/powerdns/keys");
+ }
+
+ DNSBackend* make(const string& suffix) override
+diff -up pdns-5.0.2/modules/gmysqlbackend/gmysqlbackend.cc.omv~ pdns-5.0.2/modules/gmysqlbackend/gmysqlbackend.cc
+--- pdns-5.0.2/modules/gmysqlbackend/gmysqlbackend.cc.omv~ 2026-02-17 21:13:51.799944635 +0100
++++ pdns-5.0.2/modules/gmysqlbackend/gmysqlbackend.cc 2026-02-17 21:13:58.325326973 +0100
+@@ -85,7 +85,7 @@ public:
+ declare(suffix, "thread-cleanup", "Explicitly call mysql_thread_end() when threads end", "no");
+ declare(suffix, "ssl", "Send the SSL capability flag to the server", "no");
+
+- declare(suffix, "dnssec", "Enable DNSSEC processing", "no");
++ declare(suffix, "dnssec", "Enable DNSSEC processing", "yes");
+
+ string record_query = "SELECT content,ttl,prio,type,domain_id,disabled,name,auth FROM records WHERE";
+
+diff -up pdns-5.0.2/modules/gpgsqlbackend/gpgsqlbackend.cc.omv~ pdns-5.0.2/modules/gpgsqlbackend/gpgsqlbackend.cc
+--- pdns-5.0.2/modules/gpgsqlbackend/gpgsqlbackend.cc.omv~ 2026-02-17 21:14:05.921944717 +0100
++++ pdns-5.0.2/modules/gpgsqlbackend/gpgsqlbackend.cc 2026-02-17 21:14:10.987332062 +0100
+@@ -93,7 +93,7 @@ public:
+ declare(suffix, "extra-connection-parameters", "Extra parameters to add to connection string", "");
+ declare(suffix, "prepared-statements", "Use prepared statements instead of parameterized queries", "yes");
+
+- declare(suffix, "dnssec", "Enable DNSSEC processing", "no");
++ declare(suffix, "dnssec", "Enable DNSSEC processing", "yes");
+
+ string record_query = "SELECT content,ttl,prio,type,domain_id,disabled::int,name,auth::int FROM records WHERE";
+
+diff -up pdns-5.0.2/modules/gsqlite3backend/gsqlite3backend.cc.omv~ pdns-5.0.2/modules/gsqlite3backend/gsqlite3backend.cc
+--- pdns-5.0.2/modules/gsqlite3backend/gsqlite3backend.cc.omv~ 2026-02-17 21:10:36.898186758 +0100
++++ pdns-5.0.2/modules/gsqlite3backend/gsqlite3backend.cc 2026-02-17 21:11:21.176537223 +0100
+@@ -74,12 +74,12 @@ public:
+ //! Declares all needed arguments.
+ void declareArguments(const std::string& suffix = "") override
+ {
+- declare(suffix, "database", "Filename of the SQLite3 database", "powerdns.sqlite");
++ declare(suffix, "database", "Filename of the SQLite3 database", "/srv/powerdns/powerdns.sqlite");
+ declare(suffix, "pragma-synchronous", "Set this to 0 for blazing speed", "");
+ declare(suffix, "pragma-foreign-keys", "Enable foreign key constraints", "no");
+ declare(suffix, "pragma-journal-mode", "SQLite3 journal mode", "WAL");
+
+- declare(suffix, "dnssec", "Enable DNSSEC processing", "no");
++ declare(suffix, "dnssec", "Enable DNSSEC processing", "yes");
+
+ string record_query = "SELECT content,ttl,prio,type,domain_id,disabled,name,auth FROM records WHERE";
+
+diff -up pdns-5.0.2/modules/lmdbbackend/lmdbbackend.cc.omv~ pdns-5.0.2/modules/lmdbbackend/lmdbbackend.cc
+--- pdns-5.0.2/modules/lmdbbackend/lmdbbackend.cc.omv~ 2026-02-17 21:11:48.741173447 +0100
++++ pdns-5.0.2/modules/lmdbbackend/lmdbbackend.cc 2026-02-17 21:12:15.481036461 +0100
+@@ -3306,7 +3306,7 @@ public:
+ BackendFactory("lmdb") {}
+ void declareArguments(const string& suffix = "") override
+ {
+- declare(suffix, "filename", "Filename for lmdb", "./pdns.lmdb");
++ declare(suffix, "filename", "Filename for lmdb", "/srv/powerdns/pdns.lmdb");
+ declare(suffix, "sync-mode", "Synchronisation mode: nosync, nometasync, sync", "sync");
+ // there just is no room for more on 32 bit
+ declare(suffix, "shards", "Records database will be split into this number of shards", (sizeof(void*) == 4) ? "2" : "64");
+diff -up pdns-5.0.2/modules/lua2backend/lua2backend.cc.omv~ pdns-5.0.2/modules/lua2backend/lua2backend.cc
+--- pdns-5.0.2/modules/lua2backend/lua2backend.cc.omv~ 2026-02-17 21:12:22.189624483 +0100
++++ pdns-5.0.2/modules/lua2backend/lua2backend.cc 2026-02-17 21:12:31.306163476 +0100
+@@ -34,7 +34,7 @@ public:
+
+ void declareArguments(const string& suffix = "") override
+ {
+- declare(suffix, "filename", "Filename of the script for lua backend", "powerdns-luabackend.lua");
++ declare(suffix, "filename", "Filename of the script for lua backend", "/srv/powerdns/powerdns-luabackend.lua");
+ declare(suffix, "query-logging", "Logging of the Lua2 Backend", "no");
+ declare(suffix, "api", "Lua backend API version", "2");
+ }
+diff -up pdns-5.0.2/modules/remotebackend/remotebackend.cc.omv~ pdns-5.0.2/modules/remotebackend/remotebackend.cc
+--- pdns-5.0.2/modules/remotebackend/remotebackend.cc.omv~ 2026-02-17 21:12:55.353060347 +0100
++++ pdns-5.0.2/modules/remotebackend/remotebackend.cc 2026-02-17 21:13:08.187569893 +0100
+@@ -1030,7 +1030,7 @@ public:
+
+ void declareArguments(const std::string& suffix = "") override
+ {
+- declare(suffix, "dnssec", "Enable dnssec support", "no");
++ declare(suffix, "dnssec", "Enable dnssec support", "yes");
+ declare(suffix, "connection-string", "Connection string", "");
+ }
+
diff --git a/pdns-drop-privs.patch b/pdns-drop-privs.patch
new file mode 100644
index 0000000..9c7653b
--- /dev/null
+++ b/pdns-drop-privs.patch
@@ -0,0 +1,130 @@
+diff -up pdns-5.0.2/pdns/arguments.cc.2~ pdns-5.0.2/pdns/arguments.cc
+--- pdns-5.0.2/pdns/arguments.cc.2~ 2025-12-11 09:59:35.000000000 +0100
++++ pdns-5.0.2/pdns/arguments.cc 2026-02-18 03:31:06.518176494 +0100
+@@ -341,6 +341,8 @@ double ArgvMap::asDouble(const string& a
+ ArgvMap::ArgvMap()
+ {
+ set("ignore-unknown-settings", "Configuration settings to ignore if they are unknown") = "";
++ set("setuid", "If set, change user id") = "powerdns";
++ set("setgid", "If set, change group id") = "powerdns";
+ }
+
+ bool ArgvMap::parmIsset(const string& var)
+@@ -606,3 +608,39 @@ void ArgvMap::gatherIncludes(const std::
+ std::sort(vec.begin(), vec.end(), CIStringComparePOSIX());
+ extraConfigs.insert(extraConfigs.end(), vec.begin(), vec.end());
+ }
++
++int ArgvMap::targetUid() {
++ std::string uid_str = (*this)["setuid"];
++ if (uid_str.empty()) uid_str = "powerdns";
++
++ struct passwd *pw = getpwnam(uid_str.c_str());
++ if(pw)
++ return pw->pw_uid;
++ return 0;
++}
++
++int ArgvMap::targetGid() {
++ std::string uid_str = (*this)["setuid"];
++ if (uid_str.empty()) uid_str = "powerdns";
++
++ struct passwd *pw = getpwnam(uid_str.c_str());
++ if(pw)
++ return pw->pw_gid;
++ return 0;
++}
++
++bool ArgvMap::dropPrivileges() {
++ if (getuid() != 0)
++ return true;
++
++ std::string uid_str = (*this)["setuid"];
++ if (uid_str.empty()) uid_str = "powerdns";
++
++ struct passwd *pw = getpwnam(uid_str.c_str());
++ if (pw) {
++ initgroups(pw->pw_name, pw->pw_gid);
++ setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid);
++ return setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == 0;
++ }
++ return false;
++}
+diff -up pdns-5.0.2/pdns/arguments.hh.2~ pdns-5.0.2/pdns/arguments.hh
+--- pdns-5.0.2/pdns/arguments.hh.2~ 2025-12-11 09:59:35.000000000 +0100
++++ pdns-5.0.2/pdns/arguments.hh 2026-02-18 03:30:39.208801041 +0100
+@@ -107,6 +107,9 @@ public:
+ bool isEmpty(const string& arg); //!< checks if variable has value
+ void setDefault(const string& var, const string& value);
+ void setDefaults();
++ int targetUid();
++ int targetGid();
++ bool dropPrivileges();
+
+ vector<string> list();
+ [[nodiscard]] string getHelp(const string& item) const
+diff -up pdns-5.0.2/pdns/pdnsutil.cc.2~ pdns-5.0.2/pdns/pdnsutil.cc
+--- pdns-5.0.2/pdns/pdnsutil.cc.2~ 2025-12-11 09:59:35.000000000 +0100
++++ pdns-5.0.2/pdns/pdnsutil.cc 2026-02-18 03:31:25.434464605 +0100
+@@ -1926,6 +1926,8 @@ static int editZone(const ZoneName &zone
+ if (result != 0) {
+ throw std::runtime_error("Editing file with: '" + editor + "' returned non-zero status " + std::to_string(result));
+ }
++ chown(tmpnam, ::arg().targetUid(), ::arg().targetGid());
++ ::arg().dropPrivileges();
+ ZoneParserTNG zpt(static_cast<const char *>(tmpnam), g_rootzonename);
+ zpt.setMaxGenerateSteps(::arg().asNum("max-generate-steps"));
+ zpt.setMaxIncludes(::arg().asNum("max-include-depth"));
+@@ -5728,6 +5730,19 @@ try
+
+ loadMainConfig(g_vm["config-dir"].as<string>());
+
++ try {
++ ::arg().file(::arg()["config-file"].c_str());
++ } catch(...) {}
++ bool is_edit = false;
++ if (!cmds.empty()) {
++ if (cmds[0] == "edit-zone")
++ is_edit = true;
++ else if (cmds[0] == "zone" && cmds.size() > 1 && cmds[1] == "edit")
++ is_edit = true;
++ }
++ if (!is_edit)
++ ::arg().dropPrivileges();
++
+ std::string writtencommand;
+ if (commandEntry command; parseCommand(cmds, writtencommand, command)) {
+ if (command.requiresInitialization) {
+diff -up pdns-5.0.2/pdns/zone2json.cc.2~ pdns-5.0.2/pdns/zone2json.cc
+--- pdns-5.0.2/pdns/zone2json.cc.2~ 2026-02-18 03:32:46.239475264 +0100
++++ pdns-5.0.2/pdns/zone2json.cc 2026-02-18 03:33:00.137746277 +0100
+@@ -115,6 +115,7 @@ try
+ string zonefile="";
+
+ ::arg().parse(argc, argv);
++ ::arg().dropPrivileges();
+
+ if(::arg().mustDo("version")){
+ cerr<<"zone2json "<<VERSION<<endl;
+diff -up pdns-5.0.2/pdns/zone2ldap.cc.2~ pdns-5.0.2/pdns/zone2ldap.cc
+--- pdns-5.0.2/pdns/zone2ldap.cc.2~ 2026-02-18 03:33:28.225078930 +0100
++++ pdns-5.0.2/pdns/zone2ldap.cc 2026-02-18 03:33:35.757322464 +0100
+@@ -251,6 +251,7 @@ int main( int argc, char* argv[] )
+ args.set( "max-include-depth", "Maximum nested $INCLUDE depth when loading a zone from a file")="20";
+
+ args.parse( argc, argv );
++ args.dropPrivileges();
+
+ if(args.mustDo("version")) {
+ cerr<<"zone2ldap "<<VERSION<<endl;
+diff -up pdns-5.0.2/pdns/zone2sql.cc.2~ pdns-5.0.2/pdns/zone2sql.cc
+--- pdns-5.0.2/pdns/zone2sql.cc.2~ 2026-02-18 03:33:08.977813988 +0100
++++ pdns-5.0.2/pdns/zone2sql.cc 2026-02-18 03:33:21.386201580 +0100
+@@ -229,6 +229,7 @@ try
+ string zonefile="";
+
+ ::arg().parse(argc, argv);
++ ::arg().dropPrivileges();
+
+ if(::arg().mustDo("version")) {
+ cerr<<"zone2sql "<<VERSION<<endl;
diff --git a/pdns.service b/pdns.service
index d72f71b..d3ed6d4 100644
--- a/pdns.service
+++ b/pdns.service
@@ -7,6 +7,17 @@ Type=forking
ExecStart=/usr/bin/pdns_server --daemon --guardian=yes
ExecReload=/usr/bin/pdns_control cycle
ExecStop=/usr/bin/pdns_control quit
+WorkingDirectory=/srv/powerdns
+ProtectSystem=strict
+ProtectHome=yes
+PrivateTmp=yes
+ProtectControlGroups=yes
+ProtectKernelModules=yes
+ProtectKernelTunables=yes
+ReadWritePaths=/srv/powerdns /run/powerdns
+AmbientCapabilities=CAP_NET_BIND_SERVICE
+CapabilityBoundingSet=CAP_NET_BIND_SERVICE
+User=powerdns
[Install]
WantedBy=multi-user.target
diff --git a/pdns.spec b/pdns.spec
index 27e3c3e..57a2405 100644
--- a/pdns.spec
+++ b/pdns.spec
@@ -3,7 +3,7 @@
Summary: Versatile Database Driven Nameserver
Name: pdns
Version: 5.0.2
-Release: 2
+Release: 3
License: GPLv2+
Group: System/Servers
Url: https://www.powerdns.com/
@@ -44,6 +44,12 @@ BuildRequires: pkgconfig(zlib)
BuildRequires: pkgconfig(p11-kit-1)
BuildRequires: pkgconfig(libzmq)
BuildRequires: pkgconfig(libmaxminddb)
+# called in the default config
+Recommends: %{name}-backend-lmdb = %{EVRD}
+
+%patchlist
+pdns-5.0.2-defaults.patch
+pdns-drop-privs.patch
%description
PowerDNS is a versatile nameserver which supports a large number of different
@@ -56,13 +62,13 @@ backend', all available as external packages.
%doc COPYING README rtfm.powerdns.com
%doc %{_docdir}/pdns/*.sql
%doc %{_docdir}/pdns/*.schema
-%config(noreplace) %attr(0600,root,root) %{_sysconfdir}/powerdns/%{name}.conf
+%attr(0750,root,powerdns) %dir %{_sysconfdir}/powerdns
+%config(noreplace) %attr(0640,root,powerdns) %{_sysconfdir}/powerdns/%{name}.conf
+%attr(0750,root,powerdns) %dir %{_sysconfdir}/powerdns/conf.d
%{_tmpfilesdir}/%{name}.conf
%{_sysusersdir}/%{name}.conf
%{_unitdir}/%{name}.service
%{_unitdir}/pdns@.service
-%dir %{_sysconfdir}/powerdns
-%dir %{_sysconfdir}/powerdns/conf.d
%dir %{_libdir}/powerdns
%{_mandir}/man1/*
%{_bindir}/calidns
@@ -89,6 +95,7 @@ backend', all available as external packages.
%{_bindir}/zone2json
%{_bindir}/zone2ldap
%{_bindir}/zone2sql
+%attr(6750,powerdns,powerdns) %dir /srv/powerdns
#----------------------------------------------------------------------------
%package ixfrdist
@@ -268,6 +275,7 @@ This package contains a SQLite backend for the PowerDNS nameserver.
--with-systemd="%{_unitdir}" \
--with-sqlite3 \
--sysconfdir=%{_sysconfdir}/powerdns \
+ --with-modules-dir=%{_libdir}/pdns \
--libdir=%{_libdir}/powerdns \
--with-socketdir=/run/powerdns \
--with-dynmodules="gmysql gpgsql pipe ldap lmdb lua2 gsqlite3 geoip remote bind" \
@@ -301,17 +309,17 @@ mv %{buildroot}%{_sysconfdir}/powerdns/%{name}.conf-dist %{buildroot}%{_sysconfd
cat >> %{buildroot}%{_sysconfdir}/powerdns/%{name}.conf << EOF
include-dir=%{_sysconfdir}/powerdns/conf.d
-module-dir=%{_libdir}/powerdns
socket-dir=/run/powerdns
setuid=powerdns
setgid=powerdns
-launch=bind
+launch=lmdb
EOF
chmod 600 %{buildroot}%{_sysconfdir}/powerdns/%{name}.conf
install -d %{buildroot}%{_sysconfdir}/powerdns/conf.d
+mkdir -p %{buildroot}/srv/powerdns
# Fix per backend config files
for i in geoip gmysql gpgsql gsqlite3 ldap pipe; do
@@ -322,11 +330,11 @@ done
mkdir -p %{buildroot}%{_sysusersdir}
cat >%{buildroot}%{_sysusersdir}/%{name}.conf <<EOF
g powerdns -
-u powerdns - "PowerDNS Name Server" %{_localstatedir}/lib/powerdns -
+u powerdns - "PowerDNS Name Server" /srv/powerdns -
EOF
# Prepare tmpfiles support config
mkdir -p %{buildroot}%{_tmpfilesdir}
cat <<EOF > %{buildroot}%{_tmpfilesdir}/%{name}.conf
-d /run/powerdns 0755 powerdns powerdns
+d /run/powerdns 0755 powerdns powerdns
EOF