diff --git a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
index 8897febc2023..f0da210f6580 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
@@ -36,6 +36,13 @@
services.filebeat.
+
+
+ PowerDNS-Admin,
+ a web interface for the PowerDNS server. Available at
+ services.powerdns-admin.
+
+
diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md
index 211c64c02899..2c008a67dd72 100644
--- a/nixos/doc/manual/release-notes/rl-2205.section.md
+++ b/nixos/doc/manual/release-notes/rl-2205.section.md
@@ -12,6 +12,8 @@ In addition to numerous new and upgraded packages, this release has the followin
- [filebeat](https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-overview.html), a lightweight shipper for forwarding and centralizing log data. Available as [services.filebeat](#opt-services.filebeat.enable).
+- [PowerDNS-Admin](https://github.com/ngoduykhanh/PowerDNS-Admin), a web interface for the PowerDNS server. Available at [services.powerdns-admin](options.html#opt-services.powerdns-admin.enable).
+
## Backward Incompatibilities {#sec-release-22.05-incompatibilities}
- `pkgs.ghc` now refers to `pkgs.targetPackages.haskellPackages.ghc`.
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index cb2dd530de15..30ad0db459ef 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1026,6 +1026,7 @@
./services/web-apps/plantuml-server.nix
./services/web-apps/plausible.nix
./services/web-apps/pgpkeyserver-lite.nix
+ ./services/web-apps/powerdns-admin.nix
./services/web-apps/matomo.nix
./services/web-apps/moinmoin.nix
./services/web-apps/openwebrx.nix
diff --git a/nixos/modules/services/web-apps/powerdns-admin.nix b/nixos/modules/services/web-apps/powerdns-admin.nix
new file mode 100644
index 000000000000..ce99b606c318
--- /dev/null
+++ b/nixos/modules/services/web-apps/powerdns-admin.nix
@@ -0,0 +1,149 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.powerdns-admin;
+
+ configText = ''
+ ${cfg.config}
+ ''
+ + optionalString (cfg.secretKeyFile != null) ''
+ with open('${cfg.secretKeyFile}') as file:
+ SECRET_KEY = file.read()
+ ''
+ + optionalString (cfg.saltFile != null) ''
+ with open('${cfg.saltFile}') as file:
+ SALT = file.read()
+ '';
+in
+{
+ options.services.powerdns-admin = {
+ enable = mkEnableOption "the PowerDNS web interface";
+
+ extraArgs = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = literalExpression ''
+ [ "-b" "127.0.0.1:8000" ]
+ '';
+ description = ''
+ Extra arguments passed to powerdns-admin.
+ '';
+ };
+
+ config = mkOption {
+ type = types.str;
+ default = "";
+ example = ''
+ BIND_ADDRESS = '127.0.0.1'
+ PORT = 8000
+ SQLALCHEMY_DATABASE_URI = 'postgresql://powerdnsadmin@/powerdnsadmin?host=/run/postgresql'
+ '';
+ description = ''
+ Configuration python file.
+ See the example configuration
+ for options.
+ '';
+ };
+
+ secretKeyFile = mkOption {
+ type = types.nullOr types.path;
+ example = "/etc/powerdns-admin/secret";
+ description = ''
+ The secret used to create cookies.
+ This needs to be set, otherwise the default is used and everyone can forge valid login cookies.
+ Set this to null to ignore this setting and configure it through another way.
+ '';
+ };
+
+ saltFile = mkOption {
+ type = types.nullOr types.path;
+ example = "/etc/powerdns-admin/salt";
+ description = ''
+ The salt used for serialization.
+ This should be set, otherwise the default is used.
+ Set this to null to ignore this setting and configure it through another way.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.services.powerdns-admin = {
+ description = "PowerDNS web interface";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "networking.target" ];
+
+ environment.FLASK_CONF = builtins.toFile "powerdns-admin-config.py" configText;
+ environment.PYTHONPATH = pkgs.powerdns-admin.pythonPath;
+ serviceConfig = {
+ ExecStart = "${pkgs.powerdns-admin}/bin/powerdns-admin --pid /run/powerdns-admin/pid ${escapeShellArgs cfg.extraArgs}";
+ ExecStartPre = "${pkgs.coreutils}/bin/env FLASK_APP=${pkgs.powerdns-admin}/share/powerdnsadmin/__init__.py ${pkgs.python3Packages.flask}/bin/flask db upgrade -d ${pkgs.powerdns-admin}/share/migrations";
+ ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+ ExecStop = "${pkgs.coreutils}/bin/kill -TERM $MAINPID";
+ PIDFile = "/run/powerdns-admin/pid";
+ RuntimeDirectory = "powerdns-admin";
+ User = "powerdnsadmin";
+ Group = "powerdnsadmin";
+
+ AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+ BindReadOnlyPaths = [
+ "/nix/store"
+ "-/etc/resolv.conf"
+ "-/etc/nsswitch.conf"
+ "-/etc/hosts"
+ "-/etc/localtime"
+ ]
+ ++ (optional (cfg.secretKeyFile != null) cfg.secretKeyFile)
+ ++ (optional (cfg.saltFile != null) cfg.saltFile);
+ CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
+ # ProtectClock= adds DeviceAllow=char-rtc r
+ DeviceAllow = "";
+ # Implies ProtectSystem=strict, which re-mounts all paths
+ #DynamicUser = true;
+ LockPersonality = true;
+ MemoryDenyWriteExecute = true;
+ NoNewPrivileges = true;
+ PrivateDevices = true;
+ PrivateMounts = true;
+ # Needs to start a server
+ #PrivateNetwork = true;
+ PrivateTmp = true;
+ PrivateUsers = true;
+ ProcSubset = "pid";
+ ProtectClock = true;
+ ProtectHome = true;
+ ProtectHostname = true;
+ # Would re-mount paths ignored by temporary root
+ #ProtectSystem = "strict";
+ ProtectControlGroups = true;
+ ProtectKernelLogs = true;
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ ProtectProc = "invisible";
+ RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ RestrictSUIDSGID = true;
+ SystemCallArchitectures = "native";
+ # gunicorn needs setuid
+ SystemCallFilter = [
+ "@system-service"
+ "~@privileged @resources @keyring"
+ # These got removed by the line above but are needed
+ "@setuid @chown"
+ ];
+ TemporaryFileSystem = "/:ro";
+ # Does not work well with the temporary root
+ #UMask = "0066";
+ };
+ };
+
+ users.groups.powerdnsadmin = { };
+ users.users.powerdnsadmin = {
+ description = "PowerDNS web interface user";
+ isSystemUser = true;
+ group = "powerdnsadmin";
+ };
+ };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index f86cc2544dab..5c8342f0bb7b 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -381,6 +381,7 @@ in
postgresql = handleTest ./postgresql.nix {};
postgresql-wal-receiver = handleTest ./postgresql-wal-receiver.nix {};
powerdns = handleTest ./powerdns.nix {};
+ powerdns-admin = handleTest ./powerdns-admin.nix {};
power-profiles-daemon = handleTest ./power-profiles-daemon.nix {};
pppd = handleTest ./pppd.nix {};
predictable-interface-names = handleTest ./predictable-interface-names.nix {};
diff --git a/nixos/tests/powerdns-admin.nix b/nixos/tests/powerdns-admin.nix
new file mode 100644
index 000000000000..4d763c9c6f6e
--- /dev/null
+++ b/nixos/tests/powerdns-admin.nix
@@ -0,0 +1,117 @@
+# Test powerdns-admin
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+let
+ defaultConfig = ''
+ BIND_ADDRESS = '127.0.0.1'
+ PORT = 8000
+ '';
+
+ makeAppTest = name: configs: makeTest {
+ name = "powerdns-admin-${name}";
+ meta = with pkgs.lib.maintainers; {
+ maintainers = [ Flakebi zhaofengli ];
+ };
+
+ nodes.server = { pkgs, config, ... }: mkMerge ([
+ {
+ services.powerdns-admin = {
+ enable = true;
+ secretKeyFile = "/etc/powerdns-admin/secret";
+ saltFile = "/etc/powerdns-admin/salt";
+ };
+ # It's insecure to have secrets in the world-readable nix store, but this is just a test
+ environment.etc."powerdns-admin/secret".text = "secret key";
+ environment.etc."powerdns-admin/salt".text = "salt";
+ environment.systemPackages = [
+ (pkgs.writeShellScriptBin "run-test" config.system.build.testScript)
+ ];
+ }
+ ] ++ configs);
+
+ testScript = ''
+ server.wait_for_unit("powerdns-admin.service")
+ server.wait_until_succeeds("run-test", timeout=10)
+ '';
+ };
+
+ matrix = {
+ backend = {
+ mysql = {
+ services.powerdns-admin = {
+ config = ''
+ ${defaultConfig}
+ SQLALCHEMY_DATABASE_URI = 'mysql://powerdnsadmin@/powerdnsadmin?unix_socket=/run/mysqld/mysqld.sock'
+ '';
+ };
+ systemd.services.powerdns-admin = {
+ after = [ "mysql.service" ];
+ serviceConfig.BindPaths = "/run/mysqld";
+ };
+
+ services.mysql = {
+ enable = true;
+ package = pkgs.mariadb;
+ ensureDatabases = [ "powerdnsadmin" ];
+ ensureUsers = [
+ {
+ name = "powerdnsadmin";
+ ensurePermissions = {
+ "powerdnsadmin.*" = "ALL PRIVILEGES";
+ };
+ }
+ ];
+ };
+ };
+ postgresql = {
+ services.powerdns-admin = {
+ config = ''
+ ${defaultConfig}
+ SQLALCHEMY_DATABASE_URI = 'postgresql://powerdnsadmin@/powerdnsadmin?host=/run/postgresql'
+ '';
+ };
+ systemd.services.powerdns-admin = {
+ after = [ "postgresql.service" ];
+ serviceConfig.BindPaths = "/run/postgresql";
+ };
+
+ services.postgresql = {
+ enable = true;
+ ensureDatabases = [ "powerdnsadmin" ];
+ ensureUsers = [
+ {
+ name = "powerdnsadmin";
+ ensurePermissions = {
+ "DATABASE powerdnsadmin" = "ALL PRIVILEGES";
+ };
+ }
+ ];
+ };
+ };
+ };
+ listen = {
+ tcp = {
+ services.powerdns-admin.extraArgs = [ "-b" "127.0.0.1:8000" ];
+ system.build.testScript = ''
+ curl -sSf http://127.0.0.1:8000/
+ '';
+ };
+ unix = {
+ services.powerdns-admin.extraArgs = [ "-b" "unix:/run/powerdns-admin/http.sock" ];
+ system.build.testScript = ''
+ curl -sSf --unix-socket /run/powerdns-admin/http.sock http://somehost/
+ '';
+ };
+ };
+ };
+in
+with matrix; {
+ postgresql = makeAppTest "postgresql" [ backend.postgresql listen.tcp ];
+ mysql = makeAppTest "mysql" [ backend.mysql listen.tcp ];
+ unix-listener = makeAppTest "unix-listener" [ backend.postgresql listen.unix ];
+}
diff --git a/pkgs/applications/networking/powerdns-admin/default.nix b/pkgs/applications/networking/powerdns-admin/default.nix
index 9dba30e3d884..8cc2ec83cd50 100644
--- a/pkgs/applications/networking/powerdns-admin/default.nix
+++ b/pkgs/applications/networking/powerdns-admin/default.nix
@@ -1,4 +1,4 @@
-{ lib, stdenv, fetchFromGitHub, mkYarnPackage, writeText, python3 }:
+{ lib, stdenv, fetchFromGitHub, mkYarnPackage, nixosTests, writeText, python3 }:
let
version = "0.2.3";
@@ -23,8 +23,8 @@ let
pythonDeps = with python.pkgs; [
flask flask_assets flask_login flask_sqlalchemy flask_migrate flask-seasurf flask_mail flask-sslify
- mysqlclient sqlalchemy
- configobj bcrypt requests ldap pyotp qrcode dnspython
+ mysqlclient psycopg2 sqlalchemy
+ cffi configobj cryptography bcrypt requests ldap pyotp qrcode dnspython
gunicorn python3-saml pyopenssl pytz cssmin jsmin authlib bravado-core
lima pytimeparse pyyaml
];
@@ -91,6 +91,7 @@ in stdenv.mkDerivation rec {
postPatch = ''
rm -r powerdnsadmin/static powerdnsadmin/assets.py
+ sed -i "s/id:/'id':/" migrations/versions/787bdba9e147_init_db.py
'';
installPhase = ''
@@ -100,7 +101,7 @@ in stdenv.mkDerivation rec {
wrapPythonPrograms
mkdir -p $out/share $out/bin
- cp -r powerdnsadmin $out/share/powerdnsadmin
+ cp -r migrations powerdnsadmin $out/share/
ln -s ${assets} $out/share/powerdnsadmin/static
ln -s ${assetsPy} $out/share/powerdnsadmin/assets.py
@@ -114,6 +115,12 @@ in stdenv.mkDerivation rec {
runHook postInstall
'';
+ passthru = {
+ # PYTHONPATH of all dependencies used by the package
+ pythonPath = python3.pkgs.makePythonPath pythonDeps;
+ tests = nixosTests.powerdns-admin;
+ };
+
meta = with lib; {
description = "A PowerDNS web interface with advanced features";
homepage = "https://github.com/ngoduykhanh/PowerDNS-Admin";