From 074051573feeb9344dad0c31eb7fc3411c5042d6 Mon Sep 17 00:00:00 2001
From: "A. Karl Kornel" <akkornel@stanford.edu>
Date: Thu, 18 Aug 2016 14:52:22 -0700
Subject: [PATCH] duo: Rework Duo to be a type, that can be brought in by other
 code

---
 README.duo                     | 41 ++++++++++++++
 manifests/duo.pp               | 26 ---------
 manifests/duo/config.pp        | 99 ++++++++++++++++++++++++++++++++++
 manifests/duo/config/common.pp | 37 +++++++++++++
 manifests/duo/package.pp       | 50 +++++++++++++++++
 templates/duo/duo.erb          | 21 ++++++++
 6 files changed, 248 insertions(+), 26 deletions(-)
 create mode 100644 README.duo
 delete mode 100644 manifests/duo.pp
 create mode 100644 manifests/duo/config.pp
 create mode 100644 manifests/duo/config/common.pp
 create mode 100644 manifests/duo/package.pp
 create mode 100644 templates/duo/duo.erb

diff --git a/README.duo b/README.duo
new file mode 100644
index 0000000..18c1a0a
--- /dev/null
+++ b/README.duo
@@ -0,0 +1,41 @@
+This README file explains how to use base::duo.
+
+VERSION 4->5 UPGRADE NOTE: If your code involves base::duo, that's not going to 
+work anymore.  Have a look at base::duo::config instead, or README.ssh.
+
+The term Duo uses is "integration", to refer to a set of credentials that a 
+client (like your system) will use to authenticate itself to Duo.  base::duo 
+manages the process of fetching and customizing 'duo-unix' integrations, which 
+is the type of integration that can be used for things like login, sudo, and 
+web-based two-step (such as something that your web application might trigger).
+
+Duo integrations are keyed on the system name, so if you have multiple Duo uses 
+on a single system (e.g. SSH and sudo), all uses will share the same Duo 
+integration, but _may_ use different Duo configuration files: If the Duo uses 
+on a single system have different needs (such as one failing safe and one 
+failing secure), that will require separate Duo configuration files.
+
+Duo integration keys do not change or expire, unless a `wallet destroy` or a 
+Duo administrator manually deletes a Duo integration.
+
+If you do ever need to destroy a Duo integration, here's the command to use:
+
+        wallet destroy pam-duo hostname.stanford.edu
+
+To generate a Duo configuration file, instantiate an object of 
+base::duo::config, where the name is the path to the Duo configuration file.  
+For example:
+
+base::duo::config { '/etc/secure/duo_webapp.conf':
+  ensure     => present,
+  failsecure => true,
+}
+
+To see the options available, have a look at the header text in 
+manifests/duo/config.pp.  There are other classes in the base::duo namespace, 
+but they're all invoked as needed by base::duo::config.
+
+To be honest, the only time you'll need to invoke base::duo::config directly is 
+when you have a custom thing that wants to leverage Duo.  If you're interested 
+in using Duo to authenticate SSH or sudo, have a look in README.ssh or 
+README.sudo instead.
diff --git a/manifests/duo.pp b/manifests/duo.pp
deleted file mode 100644
index f08bc21..0000000
--- a/manifests/duo.pp
+++ /dev/null
@@ -1,26 +0,0 @@
-# Set up Duo. Note that this class does not _enable_ Duo for any service,
-# rather, it simply downloads the pam_duo software and the appropriate
-# wallet files that allow Duo to be used.
-
-# See base::sudo and base::ssh for services that leverage this class.
-
-# wallet_name: the name for the duo wallet object. Defaults to the
-# fully-qualified domain name of the host.
-
-class base::duo(
-  $wallet_name = $::fqdn
-){
-  # Pull in Duo's PAM integration package
-  package { 'libpam-duo': ensure => present }
-
-  # Install the duo configuration.  The object is not written to the
-  # default loaction because base::wallet will not overwrite the
-  # configuration file supplied with the package install.
-  $wallet_name_downcase = downcase($wallet_name)
-  base::wallet { $wallet_name_downcase:
-    ensure  => present,
-    type    => 'duo-pam',
-    path    => '/etc/security/pam_duo_su.conf',
-    require => Package['libpam-duo'],
-  }
-}
diff --git a/manifests/duo/config.pp b/manifests/duo/config.pp
new file mode 100644
index 0000000..bf2902b
--- /dev/null
+++ b/manifests/duo/config.pp
@@ -0,0 +1,99 @@
+# Set up a custom Duo configuration. Note that this class does not _enable_ Duo.
+# Instead, this type downloads a common Duo integration, copies it, and then 
+# customizes it according to the parameters you specify.
+#
+# Your client code is responsible for leveraging the configuration, such as by 
+# using PAM.
+#
+# See base::sudo and base::ssh for services that leverage this class.
+#
+# Here are the parameters:
+#
+# name: (namevar) The path to place the customized Duo configuration file.
+#
+# ensure: Set to "present" (the default) or "absent".
+#
+# wallet_name: the name for the common Duo wallet object. Defaults to the
+# fully-qualified domain name of the host.
+#
+# use_gecos: A boolean, defaults to false.  When true, Duo will get the 
+# username from the GECOS field (known in Puppet as the comment field) in the 
+# system passwd file.  When false, Duo will use the user's username.  This is 
+# used when a user is logging in with an account where their username does not 
+# match their Duo username.
+#
+# fail_secure: A boolean, defaults to false.  When false, a Duo timeout will 
+# cause the Duo authentication to pass, allowing the user to continue logging 
+# in.  When true, a Duo timeout will cause the Duo authentication to fail, 
+# blocking the user from logging in.
+
+define base::duo::config (
+  $ensure      = 'present',
+  $wallet_name = $::fqdn,
+  $use_gecos   = false,
+  $fail_secure = false,
+) {
+  # Validate $ensure, $use_gecos, and $fail_secure
+  if ($ensure != 'present') and ($ensure != 'absent') {
+    fail('ensure may only be "present" or "absent"')
+  }
+  if !is_bool($use_gecos) {
+    fail('base::duo::use_gecos must be true or false')
+  }
+  if !is_bool($fail_secure) {
+    fail('base::duo::fail_secure must be true or false')
+  }
+
+  # Translate $use_gecos from boolean into present/absent
+  if $use_gecos {
+    $include_gecos_line = present
+  } else {
+    $include_gecos_line = absent
+  }
+
+  # Translate $fail_secure from boolean into present/absent
+  if $fail_secure {
+    $include_fail_line = present
+  } else {
+    $include_fail_line = absent
+  }
+
+  # Decide if we even need to do anything!
+  if $ensure == 'present' {
+    # If the common config hasn't been loaded, do that now
+    if !defined(Base::Duo::Config::Common[$wallet_name]) {
+      base::duo::config::common { $wallet_name:
+        ensure => present,
+      }
+    }
+
+    # First, copy our common Duo config file
+    file { $name:
+      ensure  => present,
+      source  => "/etc/security/pam_duo_${wallet_name}.conf",
+      replace => false,
+      require => Base::Duo::Config::Common[$wallet_name],
+    }
+
+    # Next, add our GECOS and failmode lines
+    file_line { "duo_gecos_${name}":
+      ensure  => $include_gecos_line,
+      path    => $name,
+      line    => 'send_gecos = yes',
+      require => File[$name],
+    }
+    file_line { "duo_fail_secure_${name}":
+      ensure  => $include_fail_line,
+      path    => $name,
+      line    => 'failmode = secure',
+      require => File[$name],
+    }
+  } # Done handling $ensure == 'present'
+
+  else {
+    # Make sure our custom Duo config file is gone
+    file { $name:
+      ensure => absent,
+    }
+  } # Done handling $ensure == 'absent'
+}
diff --git a/manifests/duo/config/common.pp b/manifests/duo/config/common.pp
new file mode 100644
index 0000000..3da41f1
--- /dev/null
+++ b/manifests/duo/config/common.pp
@@ -0,0 +1,37 @@
+# Set up some common Duo configuration.  Normally each host has its own Duo 
+# key, but some hosts have multiple Duo keys (for example, one for the host and 
+# one for an application running on the host).  So, we need a separate type 
+# that can be called to pull the Duo credentials from Wallet.
+#
+# The Wallet object will be stored to /etc/security/pam_duo_$name.conf
+# If you don't need to customize anything, then your code can use it directly, 
+# otherwise your code will need to copy it, and then make changes as 
+# appropriate.
+
+define base::duo::config::common (
+  $ensure = 'present',
+) {
+  # Validate $ensure
+  if ($ensure != 'present') and ($ensure != 'absent') {
+    fail('ensure may only be "present" or "absent"')
+  }
+
+  # Lowercase the name, and then pull the object from Wallet
+  $wallet_name_downcase = downcase($name)
+
+  # Bring in the Wallet object, or get rid of it!
+  # Also, bring in the Duo package, as a convenience to the client.
+  if ($ensure == present) {
+    require base::duo::package
+
+    base::wallet { $wallet_name_downcase:
+      ensure  => present,
+      type    => 'duo-pam',
+      path    => "/etc/security/pam_duo_${name}.conf",
+    }
+  } else {
+    file { "/etc/security/pam_duo_${name}.conf":
+      ensure => absent,
+    }
+  }
+}
diff --git a/manifests/duo/package.pp b/manifests/duo/package.pp
new file mode 100644
index 0000000..3f3e241
--- /dev/null
+++ b/manifests/duo/package.pp
@@ -0,0 +1,50 @@
+# base::duo::package is used to bring in the packages that provide Duo 
+# functionality.  This does not activate Duo, nor does it configure Duo.  
+# Clients should `require base::duo::package` in their code, so that multiple 
+# sources can "require" Duo.  Then, your code should declare an appropriate 
+# base::duo::config to set up the configuration the way you want.
+#
+# As a convenience, if your code is using the base::duo::config type or the 
+# base::duo::config::common type, then you get base::duo::package 
+# automatically!
+
+class base::duo::package (
+  $wallet_name = $::fqdn,
+  $use_gecos   = false,
+  $fail_secure = false,
+) {
+  # What we pull in depends on the OS family
+  case $::osfamily {
+    redhat: {
+      # For RHEL, bring in the duo_unix, with Duo's key
+      include base::rpm::duo
+      base::rpm::import { 'duo-rpmkey':
+        url       => 'http://yum.stanford.edu/RPM-GPG-KEY-DUO',
+        signature => 'gpg-pubkey-15d32efc-522883e4';
+      }
+      package { 'duo_unix':
+        ensure => installed,
+        require => Base::Rpm::Import[ 'duo-rpmkey' ],
+      }
+    }
+    debian: {
+      # The libpam-duo that ships with Debian doesn't support GECOS, so we need
+      # a backport.  That means we need a pin, and THAT means we need to make
+      # sure that the debian-stanford backports repository is already set up!
+      # libpam-duo-gecos is a custom-made virtual package that ensures we are
+      # using a new-enough version of libpam-duo.
+      file { '/etc/apt/preferences.d/duo':
+        ensure  => present,
+        content => template('base/duo/duo.erb'),
+        require => File['/etc/apt/sources.list.d/stanford.list'],
+      }
+      package { 'libpam-duo-gecos':
+        ensure  => present,
+        require => File['/etc/apt/preferences.d/duo'],
+      }
+    }
+    default: {
+      fail('Your OS family is not recognized for installing the Duo PAM module')
+    }
+  }
+}
diff --git a/templates/duo/duo.erb b/templates/duo/duo.erb
new file mode 100644
index 0000000..13c8075
--- /dev/null
+++ b/templates/duo/duo.erb
@@ -0,0 +1,21 @@
+# Use backported Duo packages
+# (This file is maintained by Puppet)
+#
+# We need a Duo package that includes GECOS support.  The existing Duo packages
+# in main don't have that, so we need to use the backported package we build.
+
+Package: libduo-dev
+Pin: release a=<%= lsbdistcodename %>-backports
+Pin-Priority: 995
+
+Package: libduo3
+Pin: release a=<%= lsbdistcodename %>-backports
+Pin-Priority: 995
+
+Package: libpam-duo
+Pin: release a=<%= lsbdistcodename %>-backports
+Pin-Priority: 995
+
+Package: login-duo
+Pin: release a=<%= lsbdistcodename %>-backports
+Pin-Priority: 995
-- 
GitLab