236 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			236 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
{ config, lib, pkgs, ... }:
 | 
						||
 | 
						||
with lib;
 | 
						||
 | 
						||
let
 | 
						||
  cfg = config.security.duosec;
 | 
						||
 | 
						||
  boolToStr = b: if b then "yes" else "no";
 | 
						||
 | 
						||
  configFilePam = ''
 | 
						||
    [duo]
 | 
						||
    ikey=${cfg.integrationKey}
 | 
						||
    host=${cfg.host}
 | 
						||
    ${optionalString (cfg.groups != "") ("groups="+cfg.groups)}
 | 
						||
    failmode=${cfg.failmode}
 | 
						||
    pushinfo=${boolToStr cfg.pushinfo}
 | 
						||
    autopush=${boolToStr cfg.autopush}
 | 
						||
    prompts=${toString cfg.prompts}
 | 
						||
    fallback_local_ip=${boolToStr cfg.fallbackLocalIP}
 | 
						||
  '';
 | 
						||
 | 
						||
  configFileLogin = configFilePam + ''
 | 
						||
    motd=${boolToStr cfg.motd}
 | 
						||
    accept_env_factor=${boolToStr cfg.acceptEnvFactor}
 | 
						||
  '';
 | 
						||
in
 | 
						||
{
 | 
						||
  imports = [
 | 
						||
    (mkRenamedOptionModule [ "security" "duosec" "group" ] [ "security" "duosec" "groups" ])
 | 
						||
    (mkRenamedOptionModule [ "security" "duosec" "ikey" ] [ "security" "duosec" "integrationKey" ])
 | 
						||
    (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.")
 | 
						||
  ];
 | 
						||
 | 
						||
  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.";
 | 
						||
      };
 | 
						||
 | 
						||
      integrationKey = mkOption {
 | 
						||
        type = types.str;
 | 
						||
        description = "Integration key.";
 | 
						||
      };
 | 
						||
 | 
						||
      secretKeyFile = mkOption {
 | 
						||
        type = types.path;
 | 
						||
        default = null;
 | 
						||
        description = ''
 | 
						||
          A file containing your secret key. The security of your Duo application is tied to the security of your secret key.
 | 
						||
        '';
 | 
						||
        example = "/run/keys/duo-skey";
 | 
						||
      };
 | 
						||
 | 
						||
      host = mkOption {
 | 
						||
        type = types.str;
 | 
						||
        description = "Duo API hostname.";
 | 
						||
      };
 | 
						||
 | 
						||
      groups = mkOption {
 | 
						||
        type = types.str;
 | 
						||
        default = "";
 | 
						||
        example = "users,!wheel,!*admin guests";
 | 
						||
        description = ''
 | 
						||
          If specified, Duo authentication is required only for users
 | 
						||
          whose primary group or supplementary group list matches one
 | 
						||
          of the space-separated pattern lists. Refer to
 | 
						||
          <link xlink:href="https://duo.com/docs/duounix"/> for details.
 | 
						||
        '';
 | 
						||
      };
 | 
						||
 | 
						||
      failmode = mkOption {
 | 
						||
        type = types.enum [ "safe" "secure" ];
 | 
						||
        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 <literal>true</literal>, 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
 | 
						||
          <literal>false</literal>, the user will be prompted to
 | 
						||
          choose an authentication method. When configured with
 | 
						||
          <literal>autopush = yes</literal>, we recommend setting
 | 
						||
          <literal>prompts = 1</literal>.
 | 
						||
        '';
 | 
						||
      };
 | 
						||
 | 
						||
      motd = mkOption {
 | 
						||
        type = types.bool;
 | 
						||
        default = false;
 | 
						||
        description = ''
 | 
						||
          Print the contents of <literal>/etc/motd</literal> to screen
 | 
						||
          after a successful login.
 | 
						||
        '';
 | 
						||
      };
 | 
						||
 | 
						||
      prompts = mkOption {
 | 
						||
        type = types.enum [ 1 2 3 ];
 | 
						||
        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 <literal>prompts = 1</literal>, the user
 | 
						||
          will have to successfully authenticate on the first prompt,
 | 
						||
          whereas if <literal>prompts = 2</literal>, if the user
 | 
						||
          enters incorrect information at the initial prompt, he/she
 | 
						||
          will be prompted to authenticate again.
 | 
						||
 | 
						||
          When configured with <literal>autopush = true</literal>, we
 | 
						||
          recommend setting <literal>prompts = 1</literal>.
 | 
						||
        '';
 | 
						||
      };
 | 
						||
 | 
						||
      acceptEnvFactor = mkOption {
 | 
						||
        type = types.bool;
 | 
						||
        default = false;
 | 
						||
        description = ''
 | 
						||
          Look for factor selection or passcode in the
 | 
						||
          <literal>$DUO_PASSCODE</literal> 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 similarly 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
 | 
						||
          <literal>fallbackLocalIP = yes</literal> 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.
 | 
						||
        '';
 | 
						||
      };
 | 
						||
 | 
						||
      allowTcpForwarding = mkOption {
 | 
						||
        type = types.bool;
 | 
						||
        default = false;
 | 
						||
        description = ''
 | 
						||
          By default, when SSH forwarding, enabling Duo Security will
 | 
						||
          disable TCP forwarding. By enabling this, you potentially
 | 
						||
          undermine some of the SSH based login security. Note this is
 | 
						||
          not needed if you use PAM.
 | 
						||
        '';
 | 
						||
      };
 | 
						||
    };
 | 
						||
  };
 | 
						||
 | 
						||
  config = mkIf (cfg.ssh.enable || cfg.pam.enable) {
 | 
						||
    environment.systemPackages = [ pkgs.duo-unix ];
 | 
						||
 | 
						||
    security.wrappers.login_duo.source = "${pkgs.duo-unix.out}/bin/login_duo";
 | 
						||
 | 
						||
    system.activationScripts = {
 | 
						||
      login_duo = mkIf cfg.ssh.enable ''
 | 
						||
        if test -f "${cfg.secretKeyFile}"; then
 | 
						||
          mkdir -m 0755 -p /etc/duo
 | 
						||
 | 
						||
          umask 0077
 | 
						||
          conf="$(mktemp)"
 | 
						||
          {
 | 
						||
            cat ${pkgs.writeText "login_duo.conf" configFileLogin}
 | 
						||
            printf 'skey = %s\n' "$(cat ${cfg.secretKeyFile})"
 | 
						||
          } >"$conf"
 | 
						||
 | 
						||
          chown sshd "$conf"
 | 
						||
          mv -fT "$conf" /etc/duo/login_duo.conf
 | 
						||
        fi
 | 
						||
      '';
 | 
						||
      pam_duo = mkIf cfg.pam.enable ''
 | 
						||
        if test -f "${cfg.secretKeyFile}"; then
 | 
						||
          mkdir -m 0755 -p /etc/duo
 | 
						||
 | 
						||
          umask 0077
 | 
						||
          conf="$(mktemp)"
 | 
						||
          {
 | 
						||
            cat ${pkgs.writeText "login_duo.conf" configFilePam}
 | 
						||
            printf 'skey = %s\n' "$(cat ${cfg.secretKeyFile})"
 | 
						||
          } >"$conf"
 | 
						||
 | 
						||
          mv -fT "$conf" /etc/duo/pam_duo.conf
 | 
						||
        fi
 | 
						||
      '';
 | 
						||
    };
 | 
						||
 | 
						||
    /* 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
 | 
						||
      ${optionalString (!cfg.allowTcpForwarding) ''
 | 
						||
        AllowTcpForwarding no
 | 
						||
      ''}
 | 
						||
    '');
 | 
						||
  };
 | 
						||
}
 |