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