diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 391cc2503bd..21db81c1497 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 00000000000..989bd13d101
--- /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
+ '');
+ };
+}