1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.powerdns-admin;
7
8 configText = ''
9 ${cfg.config}
10 ''
11 + optionalString (cfg.secretKeyFile != null) ''
12 with open('${cfg.secretKeyFile}') as file:
13 SECRET_KEY = file.read()
14 ''
15 + optionalString (cfg.saltFile != null) ''
16 with open('${cfg.saltFile}') as file:
17 SALT = file.read()
18 '';
19in
20{
21 options.services.powerdns-admin = {
22 enable = mkEnableOption (lib.mdDoc "the PowerDNS web interface");
23
24 extraArgs = mkOption {
25 type = types.listOf types.str;
26 default = [ ];
27 example = literalExpression ''
28 [ "-b" "127.0.0.1:8000" ]
29 '';
30 description = lib.mdDoc ''
31 Extra arguments passed to powerdns-admin.
32 '';
33 };
34
35 config = mkOption {
36 type = types.str;
37 default = "";
38 example = ''
39 BIND_ADDRESS = '127.0.0.1'
40 PORT = 8000
41 SQLALCHEMY_DATABASE_URI = 'postgresql://powerdnsadmin@/powerdnsadmin?host=/run/postgresql'
42 '';
43 description = lib.mdDoc ''
44 Configuration python file.
45 See [the example configuration](https://github.com/ngoduykhanh/PowerDNS-Admin/blob/v${pkgs.powerdns-admin.version}/configs/development.py)
46 for options.
47 '';
48 };
49
50 secretKeyFile = mkOption {
51 type = types.nullOr types.path;
52 example = "/etc/powerdns-admin/secret";
53 description = lib.mdDoc ''
54 The secret used to create cookies.
55 This needs to be set, otherwise the default is used and everyone can forge valid login cookies.
56 Set this to null to ignore this setting and configure it through another way.
57 '';
58 };
59
60 saltFile = mkOption {
61 type = types.nullOr types.path;
62 example = "/etc/powerdns-admin/salt";
63 description = lib.mdDoc ''
64 The salt used for serialization.
65 This should be set, otherwise the default is used.
66 Set this to null to ignore this setting and configure it through another way.
67 '';
68 };
69 };
70
71 config = mkIf cfg.enable {
72 systemd.services.powerdns-admin = {
73 description = "PowerDNS web interface";
74 wantedBy = [ "multi-user.target" ];
75 after = [ "networking.target" ];
76
77 environment.FLASK_CONF = builtins.toFile "powerdns-admin-config.py" configText;
78 environment.PYTHONPATH = pkgs.powerdns-admin.pythonPath;
79 serviceConfig = {
80 ExecStart = "${pkgs.powerdns-admin}/bin/powerdns-admin --pid /run/powerdns-admin/pid ${escapeShellArgs cfg.extraArgs}";
81 # Set environment variables only for starting flask database upgrade
82 ExecStartPre = "${pkgs.coreutils}/bin/env FLASK_APP=${pkgs.powerdns-admin}/share/powerdnsadmin/__init__.py SESSION_TYPE= ${pkgs.python3Packages.flask}/bin/flask db upgrade -d ${pkgs.powerdns-admin}/share/migrations";
83 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
84 ExecStop = "${pkgs.coreutils}/bin/kill -TERM $MAINPID";
85 PIDFile = "/run/powerdns-admin/pid";
86 RuntimeDirectory = "powerdns-admin";
87 User = "powerdnsadmin";
88 Group = "powerdnsadmin";
89
90 AmbientCapabilities = "CAP_NET_BIND_SERVICE";
91 BindReadOnlyPaths = [
92 "/nix/store"
93 "-/etc/resolv.conf"
94 "-/etc/nsswitch.conf"
95 "-/etc/hosts"
96 "-/etc/localtime"
97 ]
98 ++ (optional (cfg.secretKeyFile != null) cfg.secretKeyFile)
99 ++ (optional (cfg.saltFile != null) cfg.saltFile);
100 CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
101 # ProtectClock= adds DeviceAllow=char-rtc r
102 DeviceAllow = "";
103 # Implies ProtectSystem=strict, which re-mounts all paths
104 #DynamicUser = true;
105 LockPersonality = true;
106 MemoryDenyWriteExecute = true;
107 NoNewPrivileges = true;
108 PrivateDevices = true;
109 PrivateMounts = true;
110 # Needs to start a server
111 #PrivateNetwork = true;
112 PrivateTmp = true;
113 PrivateUsers = true;
114 ProcSubset = "pid";
115 ProtectClock = true;
116 ProtectHome = true;
117 ProtectHostname = true;
118 # Would re-mount paths ignored by temporary root
119 #ProtectSystem = "strict";
120 ProtectControlGroups = true;
121 ProtectKernelLogs = true;
122 ProtectKernelModules = true;
123 ProtectKernelTunables = true;
124 ProtectProc = "invisible";
125 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
126 RestrictNamespaces = true;
127 RestrictRealtime = true;
128 RestrictSUIDSGID = true;
129 SystemCallArchitectures = "native";
130 # gunicorn needs setuid
131 SystemCallFilter = [
132 "@system-service"
133 "~@privileged @resources @keyring"
134 # These got removed by the line above but are needed
135 "@setuid @chown"
136 ];
137 TemporaryFileSystem = "/:ro";
138 # Does not work well with the temporary root
139 #UMask = "0066";
140 };
141 };
142
143 users.groups.powerdnsadmin = { };
144 users.users.powerdnsadmin = {
145 description = "PowerDNS web interface user";
146 isSystemUser = true;
147 group = "powerdnsadmin";
148 };
149 };
150
151 # uses attributes of the linked package
152 meta.buildDocsInSandbox = false;
153}