1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.security.duosec;
7
8 boolToStr = b: if b then "yes" else "no";
9
10 configFile = ''
11 [duo]
12 ikey=${cfg.ikey}
13 skey=${cfg.skey}
14 host=${cfg.host}
15 ${optionalString (cfg.group != "") ("group="+cfg.group)}
16 failmode=${cfg.failmode}
17 pushinfo=${boolToStr cfg.pushinfo}
18 autopush=${boolToStr cfg.autopush}
19 motd=${boolToStr cfg.motd}
20 prompts=${toString cfg.prompts}
21 accept_env_factor=${boolToStr cfg.acceptEnvFactor}
22 fallback_local_ip=${boolToStr cfg.fallbackLocalIP}
23 '';
24
25 loginCfgFile = optional cfg.ssh.enable
26 { source = pkgs.writeText "login_duo.conf" configFile;
27 mode = "0600";
28 user = "sshd";
29 target = "duo/login_duo.conf";
30 };
31
32 pamCfgFile = optional cfg.pam.enable
33 { source = pkgs.writeText "pam_duo.conf" configFile;
34 mode = "0600";
35 user = "sshd";
36 target = "duo/pam_duo.conf";
37 };
38in
39{
40 options = {
41 security.duosec = {
42 ssh.enable = mkOption {
43 type = types.bool;
44 default = false;
45 description = "If enabled, protect SSH logins with Duo Security.";
46 };
47
48 pam.enable = mkOption {
49 type = types.bool;
50 default = false;
51 description = "If enabled, protect logins with Duo Security using PAM support.";
52 };
53
54 ikey = mkOption {
55 type = types.str;
56 description = "Integration key.";
57 };
58
59 skey = mkOption {
60 type = types.str;
61 description = "Secret key.";
62 };
63
64 host = mkOption {
65 type = types.str;
66 description = "Duo API hostname.";
67 };
68
69 group = mkOption {
70 type = types.str;
71 default = "";
72 description = "Use Duo authentication for users only in this group.";
73 };
74
75 failmode = mkOption {
76 type = types.enum [ "safe" "enum" ];
77 default = "safe";
78 description = ''
79 On service or configuration errors that prevent Duo
80 authentication, fail "safe" (allow access) or "secure" (deny
81 access). The default is "safe".
82 '';
83 };
84
85 pushinfo = mkOption {
86 type = types.bool;
87 default = false;
88 description = ''
89 Include information such as the command to be executed in
90 the Duo Push message.
91 '';
92 };
93
94 autopush = mkOption {
95 type = types.bool;
96 default = false;
97 description = ''
98 If <literal>true</literal>, Duo Unix will automatically send
99 a push login request to the user’s phone, falling back on a
100 phone call if push is unavailable. If
101 <literal>false</literal>, the user will be prompted to
102 choose an authentication method. When configured with
103 <literal>autopush = yes</literal>, we recommend setting
104 <literal>prompts = 1</literal>.
105 '';
106 };
107
108 motd = mkOption {
109 type = types.bool;
110 default = false;
111 description = ''
112 Print the contents of <literal>/etc/motd</literal> to screen
113 after a successful login.
114 '';
115 };
116
117 prompts = mkOption {
118 type = types.enum [ 1 2 3 ];
119 default = 3;
120 description = ''
121 If a user fails to authenticate with a second factor, Duo
122 Unix will prompt the user to authenticate again. This option
123 sets the maximum number of prompts that Duo Unix will
124 display before denying access. Must be 1, 2, or 3. Default
125 is 3.
126
127 For example, when <literal>prompts = 1</literal>, the user
128 will have to successfully authenticate on the first prompt,
129 whereas if <literal>prompts = 2</literal>, if the user
130 enters incorrect information at the initial prompt, he/she
131 will be prompted to authenticate again.
132
133 When configured with <literal>autopush = true</literal>, we
134 recommend setting <literal>prompts = 1</literal>.
135 '';
136 };
137
138 acceptEnvFactor = mkOption {
139 type = types.bool;
140 default = false;
141 description = ''
142 Look for factor selection or passcode in the
143 <literal>$DUO_PASSCODE</literal> environment variable before
144 prompting the user for input.
145
146 When $DUO_PASSCODE is non-empty, it will override
147 autopush. The SSH client will need SendEnv DUO_PASSCODE in
148 its configuration, and the SSH server will similarly need
149 AcceptEnv DUO_PASSCODE.
150 '';
151 };
152
153 fallbackLocalIP = mkOption {
154 type = types.bool;
155 default = false;
156 description = ''
157 Duo Unix reports the IP address of the authorizing user, for
158 the purposes of authorization and whitelisting. If Duo Unix
159 cannot detect the IP address of the client, setting
160 <literal>fallbackLocalIP = yes</literal> will cause Duo Unix
161 to send the IP address of the server it is running on.
162
163 If you are using IP whitelisting, enabling this option could
164 cause unauthorized logins if the local IP is listed in the
165 whitelist.
166 '';
167 };
168
169 allowTcpForwarding = mkOption {
170 type = types.bool;
171 default = false;
172 description = ''
173 By default, when SSH forwarding, enabling Duo Security will
174 disable TCP forwarding. By enabling this, you potentially
175 undermine some of the SSH based login security. Note this is
176 not needed if you use PAM.
177 '';
178 };
179 };
180 };
181
182 config = mkIf (cfg.ssh.enable || cfg.pam.enable) {
183 assertions =
184 [ { assertion = !cfg.pam.enable;
185 message = "PAM support is currently not implemented.";
186 }
187 ];
188
189 environment.systemPackages = [ pkgs.duo-unix ];
190
191 security.wrappers.login_duo.source = "${pkgs.duo-unix.out}/bin/login_duo";
192 environment.etc = loginCfgFile ++ pamCfgFile;
193
194 /* If PAM *and* SSH are enabled, then don't do anything special.
195 If PAM isn't used, set the default SSH-only options. */
196 services.openssh.extraConfig = mkIf (cfg.ssh.enable || cfg.pam.enable) (
197 if cfg.pam.enable then "UseDNS no" else ''
198 # Duo Security configuration
199 ForceCommand ${config.security.wrapperDir}/login_duo
200 PermitTunnel no
201 ${optionalString (!cfg.allowTcpForwarding) ''
202 AllowTcpForwarding no
203 ''}
204 '');
205 };
206}