at 24.11-pre 8.1 kB view raw
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 users 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}