diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 326ce8e15b1a..1c2fca1f88b5 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -61,6 +61,7 @@ ./security/apparmor.nix ./security/apparmor-suid.nix ./security/ca.nix + ./security/duosec.nix ./security/pam.nix ./security/pam_usb.nix ./security/polkit.nix diff --git a/nixos/modules/security/duosec.nix b/nixos/modules/security/duosec.nix new file mode 100644 index 000000000000..989bd13d101e --- /dev/null +++ b/nixos/modules/security/duosec.nix @@ -0,0 +1,198 @@ +{ config, pkgs, ... }: + +with pkgs.lib; + +let + cfg = config.security.duosec; + + boolToStr = b: if b then "yes" else "no"; + + configFile = '' + [duo] + ikey=${cfg.ikey} + skey=${cfg.skey} + host=${cfg.host} + ${optionalString (cfg.group != "") ("group="+cfg.group)} + failmode=${cfg.failmode} + pushinfo=${boolToStr cfg.pushinfo} + autopush=${boolToStr cfg.autopush} + motd=${boolToStr cfg.motd} + prompts=${toString cfg.prompts} + accept_env_factor=${boolToStr cfg.acceptEnvFactor} + fallback_local_ip=${boolToStr cfg.fallbackLocalIP} + ''; + + loginCfgFile = optional cfg.ssh.enable + { source = pkgs.writeText "login_duo.conf" configFile; + mode = "0600"; + uid = config.ids.uids.sshd; + target = "duo/login_duo.conf"; + }; + + pamCfgFile = optional cfg.pam.enable + { source = pkgs.writeText "pam_duo.conf" configFile; + mode = "0600"; + uid = config.ids.uids.sshd; + target = "duo/pam_duo.conf"; + }; +in +{ + options = { + security.duosec = { + ssh.enable = mkOption { + type = types.bool; + default = false; + description = "If enabled, protect SSH logins with Duo Security."; + }; + + pam.enable = mkOption { + type = types.bool; + default = false; + description = "If enabled, protect logins with Duo Security using PAM support."; + }; + + ikey = mkOption { + type = types.str; + description = "Integration key."; + }; + + skey = mkOption { + type = types.str; + description = "Secret key."; + }; + + host = mkOption { + type = types.str; + description = "Duo API hostname."; + }; + + group = mkOption { + type = types.str; + default = ""; + description = "Use Duo authentication for users only in this group."; + }; + + failmode = mkOption { + type = types.str; + default = "safe"; + description = '' + On service or configuration errors that prevent Duo + authentication, fail "safe" (allow access) or "secure" (deny + access). The default is "safe". + ''; + }; + + pushinfo = mkOption { + type = types.bool; + default = false; + description = '' + Include information such as the command to be executed in + the Duo Push message. + ''; + }; + + autopush = mkOption { + type = types.bool; + default = false; + description = '' + If true, Duo Unix will automatically send + a push login request to the user’s phone, falling back on a + phone call if push is unavailable. If + false, the user will be prompted to + choose an authentication method. When configured with + autopush = yes, we recommend setting + prompts = 1. + ''; + }; + + motd = mkOption { + type = types.bool; + default = false; + description = '' + Print the contents of /etc/motd to screen + after a succesful login. + ''; + }; + + prompts = mkOption { + type = types.int; + default = 3; + description = '' + If a user fails to authenticate with a second factor, Duo + Unix will prompt the user to authenticate again. This option + sets the maximum number of prompts that Duo Unix will + display before denying access. Must be 1, 2, or 3. Default + is 3. + + For example, when prompts = 1, the user + will have to successfully authenticate on the first prompt, + whereas if prompts = 2, if the user + enters incorrect information at the initial prompt, he/she + will be prompted to authenticate again. + + When configured with autopush = true, we + recommend setting prompts = 1. + ''; + }; + + acceptEnvFactor = mkOption { + type = types.bool; + default = false; + description = '' + Look for factor selection or passcode in the + $DUO_PASSCODE environment variable before + prompting the user for input. + + When $DUO_PASSCODE is non-empty, it will override + autopush. The SSH client will need SendEnv DUO_PASSCODE in + its configuration, and the SSH server will similarily need + AcceptEnv DUO_PASSCODE. + ''; + }; + + fallbackLocalIP = mkOption { + type = types.bool; + default = false; + description = '' + Duo Unix reports the IP address of the authorizing user, for + the purposes of authorization and whitelisting. If Duo Unix + cannot detect the IP address of the client, setting + fallbackLocalIP = yes will cause Duo Unix + to send the IP address of the server it is running on. + + If you are using IP whitelisting, enabling this option could + cause unauthorized logins if the local IP is listed in the + whitelist. + ''; + }; + }; + }; + + config = mkIf (cfg.ssh.enable || cfg.pam.enable) { + assertions = + [ { assertion = cfg.failmode == "safe" || cfg.failmode == "secure"; + message = "Invalid value for failmode (must be safe or secure)."; + } + { assertion = cfg.prompts == 1 || cfg.prompts == 2 || cfg.prompts == 3; + message = "Invalid value for prompts (must be 1, 2, or 3)."; + } + { assertion = !cfg.pam.enable; + message = "PAM support is currently not implemented."; + } + ]; + + environment.systemPackages = [ pkgs.duo-unix ]; + security.setuidPrograms = [ "login_duo" ]; + environment.etc = loginCfgFile ++ pamCfgFile; + + /* If PAM *and* SSH are enabled, then don't do anything special. + If PAM isn't used, set the default SSH-only options. */ + services.openssh.extraConfig = mkIf (cfg.ssh.enable || cfg.pam.enable) ( + if cfg.pam.enable then "UseDNS no" else '' + # Duo Security configuration + ForceCommand ${config.security.wrapperDir}/login_duo + PermitTunnel no + AllowTcpForwarding no + ''); + }; +} diff --git a/nixos/modules/system/etc/etc.nix b/nixos/modules/system/etc/etc.nix index a8f0a59b6fa9..e9c03df2ba34 100644 --- a/nixos/modules/system/etc/etc.nix +++ b/nixos/modules/system/etc/etc.nix @@ -19,6 +19,8 @@ let sources = map (x: x.source) etc'; targets = map (x: x.target) etc'; modes = map (x: x.mode) etc'; + uids = map (x: x.uid) etc'; + gids = map (x: x.gid) etc'; }; in @@ -87,6 +89,24 @@ in ''; }; + uid = mkOption { + default = 0; + type = types.int; + description = '' + UID of created file. Only takes affect when the file is + copied (that is, the mode is not 'symlink'). + ''; + }; + + gid = mkOption { + default = 0; + type = types.int; + description = '' + GID of created file. Only takes affect when the file is + copied (that is, the mode is not 'symlink'). + ''; + }; + }; config = { diff --git a/nixos/modules/system/etc/make-etc.sh b/nixos/modules/system/etc/make-etc.sh index 7cf68db9ddce..60d4ba1301a3 100644 --- a/nixos/modules/system/etc/make-etc.sh +++ b/nixos/modules/system/etc/make-etc.sh @@ -6,6 +6,8 @@ set -f sources_=($sources) targets_=($targets) modes_=($modes) +uids_=($uids) +gids_=($gids) set +f for ((i = 0; i < ${#targets_[@]}; i++)); do @@ -35,6 +37,8 @@ for ((i = 0; i < ${#targets_[@]}; i++)); do if test "${modes_[$i]}" != symlink; then echo "${modes_[$i]}" > $out/etc/$target.mode + echo "${uids_[$i]}" > $out/etc/$target.uid + echo "${gids_[$i]}" > $out/etc/$target.gid fi fi diff --git a/nixos/modules/system/etc/setup-etc.pl b/nixos/modules/system/etc/setup-etc.pl index 4b79dbaab89e..8ba9a370b27a 100644 --- a/nixos/modules/system/etc/setup-etc.pl +++ b/nixos/modules/system/etc/setup-etc.pl @@ -60,7 +60,15 @@ sub link { if ($mode eq "direct-symlink") { atomicSymlink readlink("$static/$fn"), $target or warn; } else { + open UID, "<$_.uid"; + my $uid = ; chomp $uid; + close UID; + open GID, "<$_.gid"; + my $gid = ; chomp $gid; + close GID; + copy "$static/$fn", "$target.tmp" or warn; + chown int($uid), int($gid), "$target.tmp" or warn; chmod oct($mode), "$target.tmp" or warn; rename "$target.tmp", $target or warn; } diff --git a/pkgs/tools/security/duo-unix/default.nix b/pkgs/tools/security/duo-unix/default.nix new file mode 100644 index 000000000000..a7cd61d7f67e --- /dev/null +++ b/pkgs/tools/security/duo-unix/default.nix @@ -0,0 +1,26 @@ +{ stdenv, fetchurl, pam, openssl, zlib }: + +stdenv.mkDerivation rec { + name = "duo-unix"; + version = "1.9.7"; + + src = fetchurl { + url = "https://dl.duosecurity.com/duo_unix-${version}.tar.gz"; + sha256 = "090kx9nixlhvy5nw0ywqmi7yhd4nz7wvdv38cpkgrspkridfl07j"; + }; + + buildInputs = [ pam openssl zlib ]; + configureFlags = + [ "--with-pam=$(out)/lib/security" + "--prefix=$(out)" + "--sysconfdir=$(out)/etc/duo" + ]; + + meta = { + description = "Duo Security Unix login integration"; + homepage = "https://duosecurity.com"; + license = stdenv.lib.licenses.gpl2; + platforms = stdenv.lib.platforms.unix; + maintainers = [ stdenv.lib.maintainers.thoughtpolice ]; + }; +} diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 840d538ba09f..03b7dba8b4a0 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -855,6 +855,8 @@ let dtach = callPackage ../tools/misc/dtach { }; + duo-unix = callPackage ../tools/security/duo-unix { }; + duplicity = callPackage ../tools/backup/duplicity { inherit (pythonPackages) boto lockfile; gnupg = gnupg1;