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 = lib.mdDoc "If enabled, protect SSH logins with Duo Security.";
40 };
41
42 pam.enable = mkOption {
43 type = types.bool;
44 default = false;
45 description = lib.mdDoc "If enabled, protect logins with Duo Security using PAM support.";
46 };
47
48 integrationKey = mkOption {
49 type = types.str;
50 description = lib.mdDoc "Integration key.";
51 };
52
53 secretKeyFile = mkOption {
54 type = types.nullOr types.path;
55 default = null;
56 description = lib.mdDoc ''
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 = lib.mdDoc "Duo API hostname.";
65 };
66
67 groups = mkOption {
68 type = types.str;
69 default = "";
70 example = "users,!wheel,!*admin guests";
71 description = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 system.activationScripts = {
197 login_duo = mkIf cfg.ssh.enable ''
198 if test -f "${cfg.secretKeyFile}"; then
199 mkdir -m 0755 -p /etc/duo
200
201 umask 0077
202 conf="$(mktemp)"
203 {
204 cat ${pkgs.writeText "login_duo.conf" configFileLogin}
205 printf 'skey = %s\n' "$(cat ${cfg.secretKeyFile})"
206 } >"$conf"
207
208 chown sshd "$conf"
209 mv -fT "$conf" /etc/duo/login_duo.conf
210 fi
211 '';
212 pam_duo = mkIf cfg.pam.enable ''
213 if test -f "${cfg.secretKeyFile}"; then
214 mkdir -m 0755 -p /etc/duo
215
216 umask 0077
217 conf="$(mktemp)"
218 {
219 cat ${pkgs.writeText "login_duo.conf" configFilePam}
220 printf 'skey = %s\n' "$(cat ${cfg.secretKeyFile})"
221 } >"$conf"
222
223 mv -fT "$conf" /etc/duo/pam_duo.conf
224 fi
225 '';
226 };
227
228 /* If PAM *and* SSH are enabled, then don't do anything special.
229 If PAM isn't used, set the default SSH-only options. */
230 services.openssh.extraConfig = mkIf (cfg.ssh.enable || cfg.pam.enable) (
231 if cfg.pam.enable then "UseDNS no" else ''
232 # Duo Security configuration
233 ForceCommand ${config.security.wrapperDir}/login_duo
234 PermitTunnel no
235 ${optionalString (!cfg.allowTcpForwarding) ''
236 AllowTcpForwarding no
237 ''}
238 '');
239 };
240}