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