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