at 24.05-pre 8.0 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 = 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 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 = 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 systemd.services.login-duo = lib.mkIf cfg.ssh.enable { 197 wantedBy = [ "sysinit.target" ]; 198 before = [ "sysinit.target" ]; 199 unitConfig.DefaultDependencies = false; 200 script = '' 201 if test -f "${cfg.secretKeyFile}"; then 202 mkdir -m 0755 -p /etc/duo 203 204 umask 0077 205 conf="$(mktemp)" 206 { 207 cat ${pkgs.writeText "login_duo.conf" configFileLogin} 208 printf 'skey = %s\n' "$(cat ${cfg.secretKeyFile})" 209 } >"$conf" 210 211 chown sshd "$conf" 212 mv -fT "$conf" /etc/duo/login_duo.conf 213 fi 214 ''; 215 }; 216 217 systemd.services.pam-duo = lib.mkIf cfg.ssh.enable { 218 wantedBy = [ "sysinit.target" ]; 219 before = [ "sysinit.target" ]; 220 unitConfig.DefaultDependencies = false; 221 script = '' 222 if test -f "${cfg.secretKeyFile}"; then 223 mkdir -m 0755 -p /etc/duo 224 225 umask 0077 226 conf="$(mktemp)" 227 { 228 cat ${pkgs.writeText "login_duo.conf" configFilePam} 229 printf 'skey = %s\n' "$(cat ${cfg.secretKeyFile})" 230 } >"$conf" 231 232 mv -fT "$conf" /etc/duo/pam_duo.conf 233 fi 234 ''; 235 }; 236 237 /* If PAM *and* SSH are enabled, then don't do anything special. 238 If PAM isn't used, set the default SSH-only options. */ 239 services.openssh.extraConfig = mkIf (cfg.ssh.enable || cfg.pam.enable) ( 240 if cfg.pam.enable then "UseDNS no" else '' 241 # Duo Security configuration 242 ForceCommand ${config.security.wrapperDir}/login_duo 243 PermitTunnel no 244 ${optionalString (!cfg.allowTcpForwarding) '' 245 AllowTcpForwarding no 246 ''} 247 ''); 248 }; 249}