1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.security.duosec;
9
10 boolToStr = b: if b then "yes" else "no";
11
12 configFilePam = ''
13 [duo]
14 ikey=${cfg.integrationKey}
15 host=${cfg.host}
16 ${lib.optionalString (cfg.groups != "") ("groups=" + cfg.groups)}
17 failmode=${cfg.failmode}
18 pushinfo=${boolToStr cfg.pushinfo}
19 autopush=${boolToStr cfg.autopush}
20 prompts=${toString cfg.prompts}
21 fallback_local_ip=${boolToStr cfg.fallbackLocalIP}
22 '';
23
24 configFileLogin =
25 configFilePam
26 + ''
27 motd=${boolToStr cfg.motd}
28 accept_env_factor=${boolToStr cfg.acceptEnvFactor}
29 '';
30in
31{
32 imports = [
33 (lib.mkRenamedOptionModule [ "security" "duosec" "group" ] [ "security" "duosec" "groups" ])
34 (lib.mkRenamedOptionModule [ "security" "duosec" "ikey" ] [ "security" "duosec" "integrationKey" ])
35 (lib.mkRemovedOptionModule [ "security" "duosec" "skey" ]
36 "The insecure security.duosec.skey option has been replaced by a new security.duosec.secretKeyFile option. Use this new option to store a secure copy of your key instead."
37 )
38 ];
39
40 options = {
41 security.duosec = {
42 ssh.enable = lib.mkOption {
43 type = lib.types.bool;
44 default = false;
45 description = "If enabled, protect SSH logins with Duo Security.";
46 };
47
48 pam.enable = lib.mkOption {
49 type = lib.types.bool;
50 default = false;
51 description = "If enabled, protect logins with Duo Security using PAM support.";
52 };
53
54 integrationKey = lib.mkOption {
55 type = lib.types.str;
56 description = "Integration key.";
57 };
58
59 secretKeyFile = lib.mkOption {
60 type = lib.types.nullOr lib.types.path;
61 default = null;
62 description = ''
63 A file containing your secret key. The security of your Duo application is tied to the security of your secret key.
64 '';
65 example = "/run/keys/duo-skey";
66 };
67
68 host = lib.mkOption {
69 type = lib.types.str;
70 description = "Duo API hostname.";
71 };
72
73 groups = lib.mkOption {
74 type = lib.types.str;
75 default = "";
76 example = "users,!wheel,!*admin guests";
77 description = ''
78 If specified, Duo authentication is required only for users
79 whose primary group or supplementary group list matches one
80 of the space-separated pattern lists. Refer to
81 <https://duo.com/docs/duounix> for details.
82 '';
83 };
84
85 failmode = lib.mkOption {
86 type = lib.types.enum [
87 "safe"
88 "secure"
89 ];
90 default = "safe";
91 description = ''
92 On service or configuration errors that prevent Duo
93 authentication, fail "safe" (allow access) or "secure" (deny
94 access). The default is "safe".
95 '';
96 };
97
98 pushinfo = lib.mkOption {
99 type = lib.types.bool;
100 default = false;
101 description = ''
102 Include information such as the command to be executed in
103 the Duo Push message.
104 '';
105 };
106
107 autopush = lib.mkOption {
108 type = lib.types.bool;
109 default = false;
110 description = ''
111 If `true`, Duo Unix will automatically send
112 a push login request to the user’s phone, falling back on a
113 phone call if push is unavailable. If
114 `false`, the user will be prompted to
115 choose an authentication method. When configured with
116 `autopush = yes`, we recommend setting
117 `prompts = 1`.
118 '';
119 };
120
121 motd = lib.mkOption {
122 type = lib.types.bool;
123 default = false;
124 description = ''
125 Print the contents of `/etc/motd` to screen
126 after a successful login.
127 '';
128 };
129
130 prompts = lib.mkOption {
131 type = lib.types.enum [
132 1
133 2
134 3
135 ];
136 default = 3;
137 description = ''
138 If a user fails to authenticate with a second factor, Duo
139 Unix will prompt the user to authenticate again. This option
140 sets the maximum number of prompts that Duo Unix will
141 display before denying access. Must be 1, 2, or 3. Default
142 is 3.
143
144 For example, when `prompts = 1`, the user
145 will have to successfully authenticate on the first prompt,
146 whereas if `prompts = 2`, if the user
147 enters incorrect information at the initial prompt, he/she
148 will be prompted to authenticate again.
149
150 When configured with `autopush = true`, we
151 recommend setting `prompts = 1`.
152 '';
153 };
154
155 acceptEnvFactor = lib.mkOption {
156 type = lib.types.bool;
157 default = false;
158 description = ''
159 Look for factor selection or passcode in the
160 `$DUO_PASSCODE` environment variable before
161 prompting the user for input.
162
163 When $DUO_PASSCODE is non-empty, it will override
164 autopush. The SSH client will need SendEnv DUO_PASSCODE in
165 its configuration, and the SSH server will similarly need
166 AcceptEnv DUO_PASSCODE.
167 '';
168 };
169
170 fallbackLocalIP = lib.mkOption {
171 type = lib.types.bool;
172 default = false;
173 description = ''
174 Duo Unix reports the IP address of the authorizing user, for
175 the purposes of authorization and whitelisting. If Duo Unix
176 cannot detect the IP address of the client, setting
177 `fallbackLocalIP = yes` will cause Duo Unix
178 to send the IP address of the server it is running on.
179
180 If you are using IP whitelisting, enabling this option could
181 cause unauthorized logins if the local IP is listed in the
182 whitelist.
183 '';
184 };
185
186 allowTcpForwarding = lib.mkOption {
187 type = lib.types.bool;
188 default = false;
189 description = ''
190 By default, when SSH forwarding, enabling Duo Security will
191 disable TCP forwarding. By enabling this, you potentially
192 undermine some of the SSH based login security. Note this is
193 not needed if you use PAM.
194 '';
195 };
196 };
197 };
198
199 config = lib.mkIf (cfg.ssh.enable || cfg.pam.enable) {
200 environment.systemPackages = [ pkgs.duo-unix ];
201
202 security.wrappers.login_duo = {
203 setuid = true;
204 owner = "root";
205 group = "root";
206 source = "${pkgs.duo-unix.out}/bin/login_duo";
207 };
208
209 systemd.services.login-duo = lib.mkIf cfg.ssh.enable {
210 wantedBy = [ "sysinit.target" ];
211 before = [
212 "sysinit.target"
213 "shutdown.target"
214 ];
215 conflicts = [ "shutdown.target" ];
216 unitConfig.DefaultDependencies = false;
217 script = ''
218 if test -f "${cfg.secretKeyFile}"; then
219 mkdir -p /etc/duo
220 chmod 0755 /etc/duo
221
222 umask 0077
223 conf="$(mktemp)"
224 {
225 cat ${pkgs.writeText "login_duo.conf" configFileLogin}
226 printf 'skey = %s\n' "$(cat ${cfg.secretKeyFile})"
227 } >"$conf"
228
229 chown sshd "$conf"
230 mv -fT "$conf" /etc/duo/login_duo.conf
231 fi
232 '';
233 };
234
235 systemd.services.pam-duo = lib.mkIf cfg.ssh.enable {
236 wantedBy = [ "sysinit.target" ];
237 before = [
238 "sysinit.target"
239 "shutdown.target"
240 ];
241 conflicts = [ "shutdown.target" ];
242 unitConfig.DefaultDependencies = false;
243 script = ''
244 if test -f "${cfg.secretKeyFile}"; then
245 mkdir -p /etc/duo
246 chmod 0755 /etc/duo
247
248 umask 0077
249 conf="$(mktemp)"
250 {
251 cat ${pkgs.writeText "login_duo.conf" configFilePam}
252 printf 'skey = %s\n' "$(cat ${cfg.secretKeyFile})"
253 } >"$conf"
254
255 mv -fT "$conf" /etc/duo/pam_duo.conf
256 fi
257 '';
258 };
259
260 /*
261 If PAM *and* SSH are enabled, then don't do anything special.
262 If PAM isn't used, set the default SSH-only options.
263 */
264 services.openssh.extraConfig = lib.mkIf (cfg.ssh.enable || cfg.pam.enable) (
265 if cfg.pam.enable then
266 "UseDNS no"
267 else
268 ''
269 # Duo Security configuration
270 ForceCommand ${config.security.wrapperDir}/login_duo
271 PermitTunnel no
272 ${lib.optionalString (!cfg.allowTcpForwarding) ''
273 AllowTcpForwarding no
274 ''}
275 ''
276 );
277 };
278}