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