at master 8.4 kB view raw
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 users 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}