diff --git a/README.md b/README.md index 2a2d050..87a8fba 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,12 @@ though generally include things such as the following. The management of `/etc/security/access.conf` can be controlled by the `pam::manage_accesslogin` parameter (enabled by default). +The management of `/etc/security/faillock.conf` can be controlled by the +`pam::manage_faillock` parameter (disabled by default). + +The management of `/etc/security/pwquality.conf` and `/etc/security/pwquality.conf.d` +can be controlled by the `pam::manage_pwquality` parameter (disabled by default). + ### Setup requirements This module requires `stdlib`. When deployed by default it will require `nsswitch`. See below for more information. @@ -53,11 +59,6 @@ This module has been deployed in production along with `examples/hiera/sssd/RedHat-6.yaml` file for an example with the additional SSSD entries added via hiera. -##### pwquality - -An example of using [pam_pwquality](https://linux.die.net/man/8/pam_pwquality) can be found -in the `examples/hiera/pwquality.yaml`. - ### Beginning with pam Include the main `pam` class. @@ -202,6 +203,59 @@ pam::services: content : 'auth required pam_unix2.so' ``` +#### Manage faillock + +Management of faillock and faillock.conf is enabled via `pam::manage_faillock`. + +The following example would enable faillock, configure it, and add it to the PAM stack. + +```yaml +pam::manage_faillock: true +pam::faillock::deny: 3 +pam::pam_auth_lines: + - 'auth required pam_env.so' + - 'auth required pam_faillock.so preauth silent audit deny=5 unlock_time=900' + - 'auth sufficient pam_unix.so try_first_pass nullok' + - 'auth [default=die] pam_faillock.so authfail audit deny=5 unlock_time=900' + - 'auth required pam_deny.so' +pam::pam_account_lines: + - 'account required pam_faillock.so' + - 'account required pam_unix.so' +pam::pam_password_auth_lines: + - 'auth required pam_env.so' + - 'auth required pam_faillock.so preauth silent audit deny=5 unlock_time=900' + - 'auth sufficient pam_unix.so try_first_pass nullok' + - 'auth [default=die] pam_faillock.so authfail audit deny=5 unlock_time=900' + - 'auth required pam_deny.so' +pam::pam_password_account_lines: + - 'account required pam_faillock.so' + - 'account required pam_unix.so' +``` + +#### Manage pwquality + +Management of pwquality and pwquality.conf is enabled via `pam::manage_pwquality`. + +The following example would enable pwquality, configure it, and add it to the PAM stack. + +```yaml +pam::manage_pwquality: true +pam::pwquality::retry: 3 +pam::pwquality::maxclassrepeat: 4 +pam::pwquality::maxrepeat: 3 +pam::pwquality::minclass: 4 +pam::pwquality::difok: 8 +pam::pwquality::minlen: 15 +pam::pam_password_lines: + - 'password requisite pam_pwquality.so try_first_pass local_users_only difok=3 minlen=15 dcredit= 2 ocredit=2' + - 'password sufficient pam_unix.so try_first_pass use_authtok nullok sha512 shadow' + - 'password required pam_deny.so' +pam::pam_password_password_lines: + - 'password requisite pam_pwquality.so try_first_pass local_users_only difok=3 minlen=15 dcredit= 2 ocredit=2' + - 'password sufficient pam_unix.so try_first_pass use_authtok nullok sha512 shadow' + - 'password required pam_deny.so' +``` + ## Usage Minimal and normal usage. diff --git a/examples/hiera/faillock.yaml b/examples/hiera/faillock.yaml deleted file mode 100644 index de4f4c9..0000000 --- a/examples/hiera/faillock.yaml +++ /dev/null @@ -1,19 +0,0 @@ ---- -pam::pam_auth_lines: - - 'auth required pam_env.so' - - 'auth required pam_faillock.so preauth silent audit deny=5 unlock_time=900' - - 'auth sufficient pam_unix.so try_first_pass nullok' - - 'auth [default=die] pam_faillock.so authfail audit deny=5 unlock_time=900' - - 'auth required pam_deny.so' -pam::pam_account_lines: - - 'account required pam_faillock.so' - - 'account required pam_unix.so' -pam::pam_password_auth_lines: - - 'auth required pam_env.so' - - 'auth required pam_faillock.so preauth silent audit deny=5 unlock_time=900' - - 'auth sufficient pam_unix.so try_first_pass nullok' - - 'auth [default=die] pam_faillock.so authfail audit deny=5 unlock_time=900' - - 'auth required pam_deny.so' -pam::pam_password_account_lines: - - 'account required pam_faillock.so' - - 'account required pam_unix.so' diff --git a/examples/hiera/pwquality.yaml b/examples/hiera/pwquality.yaml deleted file mode 100644 index e55cd1e..0000000 --- a/examples/hiera/pwquality.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -pam::pam_password_lines: - - 'password requisite pam_pwquality.so try_first_pass local_users_only difok=3 minlen=15 dcredit= 2 ocredit=2' - - 'password sufficient pam_unix.so try_first_pass use_authtok nullok sha512 shadow' - - 'password required pam_deny.so' -pam::pam_password_password_lines: - - 'password requisite pam_pwquality.so try_first_pass local_users_only difok=3 minlen=15 dcredit= 2 ocredit=2' - - 'password sufficient pam_unix.so try_first_pass use_authtok nullok sha512 shadow' - - 'password required pam_deny.so' diff --git a/manifests/faillock.pp b/manifests/faillock.pp new file mode 100644 index 0000000..16a99bd --- /dev/null +++ b/manifests/faillock.pp @@ -0,0 +1,75 @@ +# @summary Manage faillock.conf +# +# @param config_file +# The faillock config path +# @param config_file_owner +# The faillock config owner +# @param config_file_group +# The faillock config group +# @param config_file_mode +# The faillock config mode +# @param config_file_template +# The faillock config template +# @param config_file_source +# The faillock config source +# @param dir +# The faillock 'dir' config option +# @param audit_enabled +# The faillock 'audit' config option +# @param silent +# The faillock 'silent' config option +# @param no_log_info +# The faillock 'no_log_info' config option +# @param local_users_only +# The faillock 'local_users_only' config option +# @param deny +# The faillock 'deny' config option +# @param fail_interval +# The faillock 'fail_interval' config option +# @param unlock_time +# The faillock 'unlock_time' config option +# @param even_deny_root +# The faillock 'even_deny_root' config option +# @param root_unlock_time +# The faillock 'root_unlock_time' config option +# @param admin_group +# The faillock 'admin_group' config option +# +class pam::faillock ( + Stdlib::Absolutepath $config_file = '/etc/security/faillock.conf', + String[1] $config_file_owner = 'root', + String[1] $config_file_group = 'root', + Stdlib::Filemode $config_file_mode = '0644', + String[1] $config_file_template = 'pam/faillock.conf.erb', + Optional[Stdlib::Filesource] $config_file_source = undef, + Stdlib::Absolutepath $dir = '/var/run/faillock', + Optional[Boolean] $audit_enabled = undef, + Optional[Boolean] $silent = undef, + Optional[Boolean] $no_log_info = undef, + Optional[Boolean] $local_users_only = undef, + Integer[0] $deny = 3, + Integer[0] $fail_interval = 900, + Integer[0] $unlock_time = 600, + Optional[Boolean] $even_deny_root = undef, + Integer[0] $root_unlock_time = $unlock_time, + Optional[String[1]] $admin_group = undef, +) { + include pam + + if $config_file_source { + $_config_file_content = undef + } else { + $_config_file_content = template($config_file_template) + } + + file { 'faillock.conf': + ensure => 'file', + path => $config_file, + owner => $config_file_owner, + group => $config_file_group, + mode => $config_file_mode, + content => $_config_file_content, + source => $config_file_source, + require => Package[$pam::package_name], + } +} diff --git a/manifests/init.pp b/manifests/init.pp index d15c29e..fc5f78b 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -29,6 +29,12 @@ # in Hiera. This is useful for specifying fragments at different levels of # the hierarchy and having them all included in the catalog. # +# @param manage_faillock +# Controls whether to manage faillock.conf +# +# @param manage_pwquality +# Controls whether to manage pwquality.conf and pwquality.conf.d +# # @param package_name # String or Array of packages providing the pam functionality. If undef, # parameter is set based on the OS version. @@ -203,6 +209,8 @@ Optional[Hash] $services = undef, Optional[Hash] $limits_fragments = undef, Boolean $limits_fragments_hiera_merge = false, + Boolean $manage_faillock = false, + Boolean $manage_pwquality = false, Array $pam_d_login_oracle_options = [], Stdlib::Absolutepath $pam_d_login_path = '/etc/pam.d/login', String $pam_d_login_owner = 'root', @@ -311,6 +319,14 @@ } } + if $manage_faillock { + include pam::faillock + } + + if $manage_pwquality { + include pam::pwquality + } + if $manage_nsswitch { include nsswitch } diff --git a/manifests/pwquality.pp b/manifests/pwquality.pp new file mode 100644 index 0000000..8d261bb --- /dev/null +++ b/manifests/pwquality.pp @@ -0,0 +1,136 @@ +# @summary Manage pwquality.conf +# +# @example +# This class is included by the pam class for platforms which use it. +# +# @param config_file +# Path to pwquality.conf. +# @param config_file_owner +# Owner for pwquality.conf +# @param config_file_group +# Group for pwquality.conf +# @param config_file_mode +# Mode for config_file. +# @param config_file_source +# String with source path to a pwquality.conf +# @param config_file_template +# Template to render pwquality.conf +# @param config_d_dir +# Path to pwquality.conf.d directory. +# @param config_d_dir_owner +# Owner for pwquality.conf.d +# @param config_d_dir_group +# Group for pwquality.conf.d +# @param config_d_dir_mode +# Mode for pwquality.conf.d +# @param purge_config_d_dir +# Boolean to purge the pwquality.conf.d directory. +# @param purge_config_d_dir_ignore +# A glob or array of file names to ignore when purging pwquality.conf.d +# +# @param difok +# The pwquality.conf 'difok' option +# @param minlen +# The pwquality.conf 'minlen' option +# @param dcredit +# The pwquality.conf 'dcredit' option +# @param ucredit +# The pwquality.conf 'ucredit' option +# @param lcredit +# The pwquality.conf 'lcredit' option +# @param ocredit +# The pwquality.conf 'ocredit' option +# @param minclass +# The pwquality.conf 'minclass' option +# @param maxrepeat +# The pwquality.conf 'maxrepeat' option +# @param maxsequence +# The pwquality.conf 'maxsequence' option +# @param maxclassrepeat +# The pwquality.conf 'maxclassrepeat' option +# @param gecoscheck +# The pwquality.conf 'gecoscheck' option +# @param dictcheck +# The pwquality.conf 'dictcheck' option +# @param usercheck +# The pwquality.conf 'usercheck' option +# @param usersubstr +# The pwquality.conf 'usersubstr' option +# @param enforcing +# The pwquality.conf 'enforcing' option +# @param badwords +# The pwquality.conf 'badwords' option +# @param dictpath +# The pwquality.conf 'dictpath' option +# @param retry +# The pwquality.conf 'retry' option +# @param enforce_for_root +# The pwquality.conf 'enforce_for_root' option +# @param local_users_only +# The pwquality.conf 'local_users_only' option +# +class pam::pwquality ( + Stdlib::Absolutepath $config_file = '/etc/security/pwquality.conf', + String[1] $config_file_owner = 'root', + String[1] $config_file_group = 'root', + Stdlib::Filemode $config_file_mode = '0644', + Optional[Stdlib::Filesource] $config_file_source = undef, + String[1] $config_file_template = 'pam/pwquality.conf.erb', + Stdlib::Absolutepath $config_d_dir = '/etc/security/pwquality.conf.d', + String[1] $config_d_dir_owner = 'root', + String[1] $config_d_dir_group = 'root', + Stdlib::Filemode $config_d_dir_mode = '0755', + Boolean $purge_config_d_dir = true, + Optional[Variant[String[1], Array[String[1]]]] $purge_config_d_dir_ignore = undef, + Integer[0] $difok = 1, + Integer[6] $minlen = 8, + Integer $dcredit = 0, + Integer $ucredit = 0, + Integer $lcredit = 0, + Integer $ocredit = 0, + Integer[0] $minclass = 0, + Integer[0] $maxrepeat = 0, + Integer[0] $maxsequence = 0, + Integer[0] $maxclassrepeat = 0, + Integer[0] $gecoscheck = 0, + Integer[0] $dictcheck = 1, + Integer[0] $usercheck = 1, + Integer[0] $usersubstr = 0, + Integer[0] $enforcing = 1, + Optional[Array[String[1]]] $badwords = undef, + Optional[Stdlib::Absolutepath] $dictpath = undef, + Integer[0] $retry = 1, + Optional[Boolean] $enforce_for_root = undef, + Optional[Boolean] $local_users_only = undef, +) { + include pam + + if $config_file_source { + $_config_file_content = undef + } else { + $_config_file_content = template($config_file_template) + } + + file { 'pwquality.conf': + ensure => 'file', + path => $config_file, + owner => $config_file_owner, + group => $config_file_group, + mode => $config_file_mode, + source => $config_file_source, + content => $_config_file_content, + require => Package[$pam::package_name], + } + + file { 'pwquality.conf.d': + ensure => 'directory', + path => $config_d_dir, + owner => $config_d_dir_owner, + group => $config_d_dir_group, + mode => $config_d_dir_mode, + purge => $purge_config_d_dir, + recurse => $purge_config_d_dir, + ignore => $purge_config_d_dir_ignore, + require => Package[$pam::package_name], + } +} diff --git a/spec/classes/faillock_spec.rb b/spec/classes/faillock_spec.rb new file mode 100644 index 0000000..53052dd --- /dev/null +++ b/spec/classes/faillock_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' +require 'spec_platforms' + +describe 'pam::faillock' do + on_supported_os.each do |os, os_facts| + # this function call mimic hiera data, it is sourced in from spec/spec_platforms.rb + package_name = package_name(os) + + context "on #{os}" do + let(:facts) { os_facts } + let(:content) do + <<-END.gsub(%r{^\s+\|}, '') + |# This file is being maintained by Puppet. + |# DO NOT EDIT + |# + |dir = /var/run/faillock + |deny = 3 + |fail_interval = 900 + |unlock_time = 600 + |root_unlock_time = 600 + END + end + + it { is_expected.to compile.with_all_deps } + it { is_expected.to contain_class('pam') } + + it do + is_expected.to contain_file('faillock.conf').with( + 'ensure' => 'file', + 'path' => '/etc/security/faillock.conf', + 'source' => nil, + 'content' => content, + 'owner' => 'root', + 'group' => 'root', + 'mode' => '0644', + ) + end + + package_name.sort.each do |pkg| + it { is_expected.to contain_file('faillock.conf').that_requires("Package[#{pkg}]") } + end + + context 'with config_file set to a valid path' do + let(:params) { { config_file: '/testing' } } + + it { is_expected.to contain_file('faillock.conf').with_path('/testing') } + end + + context 'with config_file_source set to a valid string' do + let(:params) { { config_file_source: 'puppet:///pam/unit_tests.erb' } } + + it { is_expected.to contain_file('faillock.conf').with_source('puppet:///pam/unit_tests.erb') } + it { is_expected.to contain_file('faillock.conf').with_content(nil) } + end + + context 'with config_file_mode set to a valid string' do + let(:params) { { config_file_mode: '0242' } } + + it { is_expected.to contain_file('faillock.conf').with_mode('0242') } + end + + context 'when config options are non-default' do + let(:params) do + { + dir: '/foo', + audit_enabled: true, + silent: true, + no_log_info: true, + local_users_only: true, + deny: 1, + fail_interval: 2, + unlock_time: 3, + even_deny_root: true, + root_unlock_time: 4, + admin_group: 'admins', + } + end + let(:content) do + <<-END.gsub(%r{^\s+\|}, '') + |# This file is being maintained by Puppet. + |# DO NOT EDIT + |# + |dir = /foo + |audit + |silent + |no_log_info + |local_users_only + |deny = 1 + |fail_interval = 2 + |unlock_time = 3 + |even_deny_root + |root_unlock_time = 4 + |admin_group = admins + END + end + + it { is_expected.to contain_file('faillock.conf').with_content(content) } + end + end + end +end diff --git a/spec/classes/init_spec.rb b/spec/classes/init_spec.rb index 6442896..177465c 100644 --- a/spec/classes/init_spec.rb +++ b/spec/classes/init_spec.rb @@ -268,6 +268,26 @@ end end + context 'with manage_faillock parameter default value' do + it { is_expected.not_to contain_class('pam::faillock') } + end + + context 'with manage_faillock parameter set to true' do + let(:params) { { manage_faillock: true } } + + it { is_expected.to contain_class('pam::faillock') } + end + + context 'with manage_pwquality parameter default value' do + it { is_expected.not_to contain_class('pam::pwquality') } + end + + context 'with manage_pwquality parameter set to true' do + let(:params) { { manage_pwquality: true } } + + it { is_expected.to contain_class('pam::pwquality') } + end + context 'with manage_nsswitch parameter default value' do it { is_expected.to contain_class('nsswitch') } end diff --git a/spec/classes/pwquality_spec.rb b/spec/classes/pwquality_spec.rb new file mode 100644 index 0000000..eebb9c8 --- /dev/null +++ b/spec/classes/pwquality_spec.rb @@ -0,0 +1,175 @@ +require 'spec_helper' +require 'spec_platforms' + +describe 'pam::pwquality' do + on_supported_os.each do |os, os_facts| + # this function call mimic hiera data, it is sourced in from spec/spec_platforms.rb + package_name = package_name(os) + + context "on #{os}" do + let(:facts) { os_facts } + let(:content) do + <<-END.gsub(%r{^\s+\|}, '') + |# This file is being maintained by Puppet. + |# DO NOT EDIT + |# + |difok = 1 + |minlen = 8 + |dcredit = 0 + |ucredit = 0 + |lcredit = 0 + |ocredit = 0 + |minclass = 0 + |maxrepeat = 0 + |maxsequence = 0 + |maxclassrepeat = 0 + |gecoscheck = 0 + |dictcheck = 1 + |usercheck = 1 + |usersubstr = 0 + |enforcing = 1 + |retry = 1 + END + end + + it { is_expected.to compile.with_all_deps } + it { is_expected.to contain_class('pam') } + + it do + is_expected.to contain_file('pwquality.conf').with( + 'ensure' => 'file', + 'path' => '/etc/security/pwquality.conf', + 'owner' => 'root', + 'group' => 'root', + 'mode' => '0644', + 'source' => nil, + 'content' => content, + ) + end + + it do + is_expected.to contain_file('pwquality.conf.d').with( + 'ensure' => 'directory', + 'path' => '/etc/security/pwquality.conf.d', + 'owner' => 'root', + 'group' => 'root', + 'mode' => '0755', + 'purge' => true, + 'recurse' => true, + 'ignore' => nil, + ) + end + + package_name.sort.each do |pkg| + it { is_expected.to contain_file('pwquality.conf').that_requires("Package[#{pkg}]") } + it { is_expected.to contain_file('pwquality.conf.d').that_requires("Package[#{pkg}]") } + end + + context 'with config_file set to a valid path' do + let(:params) { { config_file: '/testing' } } + + it { is_expected.to contain_file('pwquality.conf').with_path('/testing') } + end + + context 'with config_file_source set to a valid string' do + let(:params) { { config_file_source: 'puppet:///pam/unit_tests.erb' } } + + it { is_expected.to contain_file('pwquality.conf').with_source('puppet:///pam/unit_tests.erb') } + it { is_expected.to contain_file('pwquality.conf').with_content(nil) } + end + + context 'with config_file_mode set to a valid string' do + let(:params) { { config_file_mode: '0242' } } + + it { is_expected.to contain_file('pwquality.conf').with_mode('0242') } + end + + context 'with config_d_dir set to a valid string' do + let(:params) { { config_d_dir: '/testing.d' } } + + it { is_expected.to contain_file('pwquality.conf.d').with_path('/testing.d') } + end + + context 'with config_d_dir_mode set to a valid string' do + let(:params) { { config_d_dir_mode: '0242' } } + + it { is_expected.to contain_file('pwquality.conf.d').with_mode('0242') } + end + + context 'with purge_config_d_dir set to a valid boolean false' do + let(:params) { { purge_config_d_dir: false } } + + it { is_expected.to contain_file('pwquality.conf.d').with_purge(false) } + it { is_expected.to contain_file('pwquality.conf.d').with_recurse(false) } + + context 'with purge_config_d_dir_ignore set to glob pattern' do + let(:params) { { purge_config_d_dir: true, purge_config_d_dir_ignore: '{foo,bar}.conf' } } + + it { is_expected.to contain_file('pwquality.conf.d').with_ignore('{foo,bar}.conf') } + end + + context 'with purge_config_d_dir_ignore set to array' do + let(:params) { { purge_config_d_dir: true, purge_config_d_dir_ignore: ['foo.conf', 'bar.conf'] } } + + it { is_expected.to contain_file('pwquality.conf.d').with_ignore(['foo.conf', 'bar.conf']) } + end + + context 'when non-default parameters passed' do + let(:params) do + { + difok: 0, + minlen: 10, + dcredit: 1, + ucredit: 2, + lcredit: 3, + ocredit: 4, + minclass: 5, + maxrepeat: 6, + maxsequence: 7, + maxclassrepeat: 8, + gecoscheck: 9, + dictcheck: 10, + usercheck: 11, + usersubstr: 12, + enforcing: 13, + badwords: ['foo', 'bar'], + dictpath: '/etc/dict', + retry: 14, + enforce_for_root: true, + local_users_only: true, + } + end + let(:content) do + <<-END.gsub(%r{^\s+\|}, '') + |# This file is being maintained by Puppet. + |# DO NOT EDIT + |# + |difok = 0 + |minlen = 10 + |dcredit = 1 + |ucredit = 2 + |lcredit = 3 + |ocredit = 4 + |minclass = 5 + |maxrepeat = 6 + |maxsequence = 7 + |maxclassrepeat = 8 + |gecoscheck = 9 + |dictcheck = 10 + |usercheck = 11 + |usersubstr = 12 + |enforcing = 13 + |badwords = foo bar + |dictpath = /etc/dict + |retry = 14 + |enforce_for_root + |local_users_only + END + end + + it { is_expected.to contain_file('pwquality.conf').with_content(content) } + end + end + end + end +end diff --git a/templates/faillock.conf.erb b/templates/faillock.conf.erb new file mode 100644 index 0000000..222cf7e --- /dev/null +++ b/templates/faillock.conf.erb @@ -0,0 +1,26 @@ +# This file is being maintained by Puppet. +# DO NOT EDIT +# +dir = <%= @dir %> +<% if @audit_enabled -%> +audit +<% end -%> +<% if @silent -%> +silent +<% end -%> +<% if @no_log_info -%> +no_log_info +<% end -%> +<% if @local_users_only -%> +local_users_only +<% end -%> +deny = <%= @deny %> +fail_interval = <%= @fail_interval %> +unlock_time = <%= @unlock_time %> +<% if @even_deny_root -%> +even_deny_root +<% end -%> +root_unlock_time = <%= @root_unlock_time %> +<% if @admin_group -%> +admin_group = <%= @admin_group %> +<% end -%> diff --git a/templates/pwquality.conf.erb b/templates/pwquality.conf.erb new file mode 100644 index 0000000..9ed0f0f --- /dev/null +++ b/templates/pwquality.conf.erb @@ -0,0 +1,31 @@ +# This file is being maintained by Puppet. +# DO NOT EDIT +# +difok = <%= @difok %> +minlen = <%= @minlen %> +dcredit = <%= @dcredit %> +ucredit = <%= @ucredit %> +lcredit = <%= @lcredit %> +ocredit = <%= @ocredit %> +minclass = <%= @minclass %> +maxrepeat = <%= @maxrepeat %> +maxsequence = <%= @maxsequence %> +maxclassrepeat = <%= @maxclassrepeat %> +gecoscheck = <%= @gecoscheck %> +dictcheck = <%= @dictcheck %> +usercheck = <%= @usercheck %> +usersubstr = <%= @usersubstr %> +enforcing = <%= @enforcing %> +<% if @badwords -%> +badwords = <%= @badwords.join(' ') %> +<% end -%> +<% if @dictpath -%> +dictpath = <%= @dictpath %> +<% end -%> +retry = <%= @retry %> +<% if @enforce_for_root -%> +enforce_for_root +<% end -%> +<% if @local_users_only -%> +local_users_only +<% end -%>