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;