diff --git a/REFERENCE.md b/REFERENCE.md index d3fa85cf..a5c9c3f1 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -19,7 +19,7 @@ * `ssh::client::install`: Install ssh client package * `ssh::server::config`: Managed ssh server configuration * `ssh::server::install`: Install ssh server package -* `ssh::server::service`: This class managed ssh server service +* `ssh::server::service`: This class manages the ssh server service ### Defined types @@ -613,6 +613,8 @@ The following parameters are available in the `ssh::server` class: * [`config_group`](#-ssh--server--config_group) * [`default_options`](#-ssh--server--default_options) * [`ensure`](#-ssh--server--ensure) +* [`service_ensure`](#-ssh--server--service_ensure) +* [`service_enable`](#-ssh--server--service_enable) * [`include_dir`](#-ssh--server--include_dir) * [`include_dir_mode`](#-ssh--server--include_dir_mode) * [`include_dir_purge`](#-ssh--server--include_dir_purge) @@ -731,6 +733,22 @@ Ensurable param to ssh server Default value: `present` +##### `service_ensure` + +Data type: `Stdlib::Ensure::Service` + +Whether the service should be running or stopped, defaults to true when ensure is set to present, otherwise false + +Default value: `$ensure ? { 'present' => 'running', 'absent' => 'stopped'` + +##### `service_enable` + +Data type: `Boolean` + +Whether the service should be started at boot. Will be added automatically if ensure is running/removed if ensure is stopped + +Default value: `($service_ensure == 'running'` + ##### `include_dir` Data type: `Optional[Stdlib::Absolutepath]` diff --git a/manifests/server.pp b/manifests/server.pp index bc5009dc..9d68cab9 100644 --- a/manifests/server.pp +++ b/manifests/server.pp @@ -44,6 +44,12 @@ # @param ensure # Ensurable param to ssh server # +# @param service_ensure +# Whether the service should be running or stopped, defaults to true when ensure is set to present, otherwise false +# +# @param service_enable +# Whether the service should be started at boot. Will be added automatically if ensure is running/removed if ensure is stopped +# # @param include_dir # Path to sshd include directory. # @@ -127,6 +133,8 @@ Variant[Integer, String[1]] $config_group, Hash $default_options, String $ensure = present, + Stdlib::Ensure::Service $service_ensure = $ensure ? { 'present' => 'running', 'absent' => 'stopped' }, + Boolean $service_enable = ($service_ensure == 'running'), Optional[Stdlib::Absolutepath] $include_dir = undef, Stdlib::Filemode $include_dir_mode = '0700', Boolean $include_dir_purge = true, diff --git a/manifests/server/service.pp b/manifests/server/service.pp index 990ec3cd..3db868e4 100644 --- a/manifests/server/service.pp +++ b/manifests/server/service.pp @@ -1,25 +1,16 @@ # @summary -# This class managed ssh server service +# This class manages the ssh server service # # @api private # -# @param ensure -# Ensurable service param -# -# @param enable -# Define if service is enable -# -class ssh::server::service ( - Stdlib::Ensure::Service $ensure = 'running', - Boolean $enable = true, -) { +class ssh::server::service { assert_private() service { $ssh::server::service_name: - ensure => $ssh::server::service::ensure, + ensure => $ssh::server::service_ensure, hasstatus => true, hasrestart => true, - enable => $ssh::server::service::enable, + enable => $ssh::server::service_enable, require => Class['ssh::server::config'], } } diff --git a/spec/classes/client_spec.rb b/spec/classes/client_spec.rb index 4254df24..eefe7181 100644 --- a/spec/classes/client_spec.rb +++ b/spec/classes/client_spec.rb @@ -140,6 +140,89 @@ it { is_expected.to compile.with_all_deps } it { is_expected.not_to contain_ssh__client__config_file('custom') } end + + context 'with use_augeas enabled' do + let :pre_condition do + 'define ssh_config ($ensure = present, $key = undef, $value = undef, $target = undef, $host = undef) {}' + end + + let :params do + { + use_augeas: true, + options: { + 'ForwardAgent' => 'no', + 'StrictHostKeyChecking' => 'ask', + }, + options_absent: ['GSSAPIAuthentication'], + } + end + + it { is_expected.to compile.with_all_deps } + it { is_expected.not_to contain_concat('/etc/ssh/ssh_config') } + + it { + is_expected.to contain_ssh_config('ForwardAgent').with( + ensure: 'present', + key: 'ForwardAgent', + value: 'no', + target: '/etc/ssh/ssh_config', + ) + } + + it { + is_expected.to contain_ssh_config('StrictHostKeyChecking').with( + ensure: 'present', + key: 'StrictHostKeyChecking', + value: 'ask', + ) + } + + it { + is_expected.to contain_ssh_config('GSSAPIAuthentication').with( + ensure: 'absent', + key: 'GSSAPIAuthentication', + ) + } + end + + context 'with use_augeas and host block options' do + let :pre_condition do + 'define ssh_config ($ensure = present, $key = undef, $value = undef, $target = undef, $host = undef) {}' + end + + let :params do + { + use_augeas: true, + options: { + 'Host *.example.com' => { + 'ForwardAgent' => 'yes', + 'BatchMode' => 'yes', + }, + }, + options_absent: [], + } + end + + it { is_expected.to compile.with_all_deps } + + it { + is_expected.to contain_ssh_config('ForwardAgent *.example.com').with( + ensure: 'present', + host: '*.example.com', + key: 'ForwardAgent', + value: 'yes', + ) + } + + it { + is_expected.to contain_ssh_config('BatchMode *.example.com').with( + ensure: 'present', + host: '*.example.com', + key: 'BatchMode', + value: 'yes', + ) + } + end end end end diff --git a/spec/classes/server_spec.rb b/spec/classes/server_spec.rb index 2bf92d71..e0497003 100644 --- a/spec/classes/server_spec.rb +++ b/spec/classes/server_spec.rb @@ -193,6 +193,234 @@ expect(exported_resources).not_to contain_sshkey('foo.example.com_ed25519') end end + + context 'with use_augeas enabled' do + let :pre_condition do + <<~PP + define sshd_config ($ensure = present, $key = undef, $value = undef, $target = undef, $condition = undef) {} + define sshd_config_subsystem ($command = undef) {} + PP + end + + let :params do + { + use_augeas: true, + options: { + 'X11Forwarding' => 'no', + 'PermitRootLogin' => 'no', + }, + options_absent: ['GSSAPIAuthentication'], + } + end + + it { is_expected.to compile.with_all_deps } + it { is_expected.not_to contain_concat('/etc/ssh/sshd_config') } + + it { + is_expected.to contain_sshd_config('X11Forwarding').with( + ensure: 'present', + key: 'X11Forwarding', + value: 'no', + target: '/etc/ssh/sshd_config', + ) + } + + it { + is_expected.to contain_sshd_config('PermitRootLogin').with( + ensure: 'present', + key: 'PermitRootLogin', + value: 'no', + ) + } + + it { + is_expected.to contain_sshd_config('GSSAPIAuthentication').with( + ensure: 'absent', + key: 'GSSAPIAuthentication', + ) + } + end + + context 'with use_augeas and match block options' do + let :pre_condition do + <<~PP + define sshd_config ($ensure = present, $key = undef, $value = undef, $target = undef, $condition = undef) {} + define sshd_config_subsystem ($command = undef) {} + PP + end + + let :params do + { + use_augeas: true, + options: { + 'Match User www-data' => { + 'ChrootDirectory' => '%h', + 'ForceCommand' => 'internal-sftp', + }, + }, + options_absent: [], + } + end + + it { is_expected.to compile.with_all_deps } + + it { + is_expected.to contain_sshd_config('ChrootDirectory User www-data').with( + ensure: 'present', + condition: 'User www-data', + key: 'ChrootDirectory', + value: '%h', + ) + } + + it { + is_expected.to contain_sshd_config('ForceCommand User www-data').with( + ensure: 'present', + condition: 'User www-data', + key: 'ForceCommand', + value: 'internal-sftp', + ) + } + end + + context 'with use_issue_net enabled' do + let :params do + { + use_issue_net: true, + } + end + + it { is_expected.to compile.with_all_deps } + + it { + is_expected.to contain_file('/etc/issue.net').with( + ensure: 'file', + owner: 0, + group: 0, + ) + } + + it { is_expected.to contain_file('/etc/issue.net').that_notifies("Service[#{svc_name}]") } + + it { + is_expected.to contain_concat__fragment('banner file').with( + target: '/etc/ssh/sshd_config', + content: "Banner /etc/issue.net\n", + order: '01', + ) + } + end + + context 'with include_dir set' do + let :params do + { + include_dir: '/etc/ssh/sshd_config.d', + } + end + + it { is_expected.to compile.with_all_deps } + + it { + is_expected.to contain_file('/etc/ssh/sshd_config.d').with( + ensure: 'directory', + owner: 0, + group: 0, + mode: '0700', + purge: true, + recurse: true, + ) + } + end + + context 'with include_dir and include_dir_purge false' do + let :params do + { + include_dir: '/etc/ssh/sshd_config.d', + include_dir_purge: false, + } + end + + it { is_expected.to compile.with_all_deps } + + it { + is_expected.to contain_file('/etc/ssh/sshd_config.d').with( + ensure: 'directory', + purge: false, + recurse: false, + ) + } + end + + context 'with include_dir and custom mode' do + let :params do + { + include_dir: '/etc/ssh/sshd_config.d', + include_dir_mode: '0755', + } + end + + it { + is_expected.to contain_file('/etc/ssh/sshd_config.d').with( + mode: '0755', + ) + } + end + + context 'with config_files' do + let :params do + { + include_dir: '/etc/ssh/sshd_config.d', + config_files: { + 'hardening' => { + 'options' => { + 'PermitRootLogin' => 'no', + }, + }, + 'logging' => { + 'options' => { + 'LogLevel' => 'VERBOSE', + }, + }, + }, + } + end + + it { is_expected.to compile.with_all_deps } + it { is_expected.to contain_ssh__server__config_file('hardening') } + it { is_expected.to contain_ssh__server__config_file('logging') } + + it { + is_expected.to contain_concat('/etc/ssh/sshd_config.d/hardening.conf').with( + ensure: 'present', + owner: 0, + group: 0, + ) + } + + it { + is_expected.to contain_concat('/etc/ssh/sshd_config.d/logging.conf').with( + ensure: 'present', + owner: 0, + group: 0, + ) + } + end + + # Skip OSes where hiera sets include_dir by default (e.g. RedHat 9) + context 'without include_dir but with config_files', unless: os_facts.dig(:os, 'family') == 'RedHat' && os_facts.dig(:os, 'release', 'major') == '9' do + let :params do + { + config_files: { + 'hardening' => { + 'options' => {}, + }, + }, + } + end + + it { is_expected.to compile.with_all_deps } + it { is_expected.not_to contain_ssh__server__config_file('hardening') } + end end end end diff --git a/spec/defines/client/config_file_spec.rb b/spec/defines/client/config_file_spec.rb new file mode 100644 index 00000000..b7f24b4b --- /dev/null +++ b/spec/defines/client/config_file_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'ssh::client::config_file' do + on_supported_os.each do |os, os_facts| + context "on #{os}" do + let(:facts) { os_facts } + let(:title) { 'work' } + + context 'with include_dir set' do + let :pre_condition do + <<~PP + class { 'ssh::client': + include_dir => '/etc/ssh/ssh_config.d', + storeconfigs_enabled => false, + } + PP + end + + context 'with basic options' do + let(:params) do + { + options: { + 'Host *.work.internal' => { + 'User' => 'deploy', + 'IdentityFile' => '~/.ssh/work_key', + }, + }, + } + end + + it { is_expected.to compile.with_all_deps } + + it { + is_expected.to contain_concat('/etc/ssh/ssh_config.d/work.conf').with( + ensure: 'present', + owner: 0, + group: 0, + mode: '0644', + ) + } + + it { + is_expected.to contain_concat__fragment('ssh_config_file work').with( + target: '/etc/ssh/ssh_config.d/work.conf', + order: '00', + ) + } + end + + context 'with custom path' do + let(:params) do + { + path: '/etc/ssh/ssh_config.d/custom.conf', + options: {}, + } + end + + it { is_expected.to compile.with_all_deps } + it { is_expected.to contain_concat('/etc/ssh/ssh_config.d/custom.conf') } + end + + context 'with custom mode' do + let(:params) do + { + mode: '0600', + options: {}, + } + end + + it { + is_expected.to contain_concat('/etc/ssh/ssh_config.d/work.conf').with( + mode: '0600', + ) + } + end + + context 'with include parameter' do + let(:params) do + { + include: '/etc/crypto-policies/back-ends/openssh.config', + options: {}, + } + end + + it { is_expected.to compile.with_all_deps } + + it { + is_expected.to contain_concat__fragment('ssh_config_file work').with_content( + %r{Include /etc/crypto-policies/back-ends/openssh\.config}, + ) + } + end + + context 'with empty options' do + let(:params) do + { + options: {}, + } + end + + it { is_expected.to compile.with_all_deps } + it { is_expected.to contain_concat__fragment('ssh_config_file work') } + end + end + + context 'without include_dir set' do + let :pre_condition do + <<~PP + class { 'ssh::client': + storeconfigs_enabled => false, + } + PP + end + + let(:params) do + { + options: {}, + } + end + + it { is_expected.to compile.and_raise_error(%r{ssh::client::config_file\(\) define not supported if ssh::client::include_dir not set}) } + end + end + end +end diff --git a/spec/defines/server/config_file_spec.rb b/spec/defines/server/config_file_spec.rb new file mode 100644 index 00000000..d861577d --- /dev/null +++ b/spec/defines/server/config_file_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'ssh::server::config_file' do + on_supported_os.each do |os, os_facts| + context "on #{os}" do + let(:facts) { os_facts } + let(:title) { 'hardening' } + + context 'with include_dir set' do + let :pre_condition do + <<~PP + class { 'ssh::server': + include_dir => '/etc/ssh/sshd_config.d', + storeconfigs_enabled => false, + } + PP + end + + context 'with basic options' do + let(:params) do + { + options: { + 'PermitRootLogin' => 'no', + 'PasswordAuthentication' => 'no', + }, + } + end + + it { is_expected.to compile.with_all_deps } + + it { + is_expected.to contain_concat('/etc/ssh/sshd_config.d/hardening.conf').with( + ensure: 'present', + owner: 0, + group: 0, + mode: '0600', + ) + } + + it { + svc = catalogue.resource('Class', 'ssh::server')[:service_name] + is_expected.to contain_concat('/etc/ssh/sshd_config.d/hardening.conf').that_notifies("Service[#{svc}]") + } + + it { + is_expected.to contain_concat__fragment('sshd_config_file hardening').with( + target: '/etc/ssh/sshd_config.d/hardening.conf', + order: '00', + ) + } + end + + context 'with custom path' do + let(:params) do + { + path: '/etc/ssh/sshd_config.d/custom.conf', + options: { + 'LogLevel' => 'VERBOSE', + }, + } + end + + it { is_expected.to compile.with_all_deps } + it { is_expected.to contain_concat('/etc/ssh/sshd_config.d/custom.conf') } + end + + context 'with custom mode' do + let(:params) do + { + mode: '0644', + options: {}, + } + end + + it { + is_expected.to contain_concat('/etc/ssh/sshd_config.d/hardening.conf').with( + mode: '0644', + ) + } + end + + context 'with include parameter' do + let(:params) do + { + include: '/etc/crypto-policies/back-ends/opensshserver.config', + options: { + 'PermitRootLogin' => 'no', + }, + } + end + + it { is_expected.to compile.with_all_deps } + + it { + is_expected.to contain_concat__fragment('sshd_config_file hardening').with_content( + %r{Include /etc/crypto-policies/back-ends/opensshserver\.config}, + ) + } + end + + context 'with validate_sshd_file enabled' do + let :pre_condition do + <<~PP + class { 'ssh::server': + include_dir => '/etc/ssh/sshd_config.d', + storeconfigs_enabled => false, + validate_sshd_file => true, + } + PP + end + + let(:params) do + { + options: {}, + } + end + + it { + binary = catalogue.resource('Class', 'ssh::server')[:sshd_binary] + is_expected.to contain_concat('/etc/ssh/sshd_config.d/hardening.conf').with( + validate_cmd: "#{binary} -tf %", + ) + } + end + end + + # Skip OSes where hiera sets include_dir by default (e.g. RedHat 9) + context 'without include_dir set', unless: os_facts.dig(:os, 'family') == 'RedHat' && os_facts.dig(:os, 'release', 'major') == '9' do + let :pre_condition do + <<~PP + class { 'ssh::server': + storeconfigs_enabled => false, + } + PP + end + + let(:params) do + { + options: {}, + } + end + + it { is_expected.to compile.and_raise_error(%r{ssh::server::config_file\(\) define not supported if ssh::server::include_dir not set}) } + end + end + end +end diff --git a/spec/defines/server/options_spec.rb b/spec/defines/server/options_spec.rb new file mode 100644 index 00000000..09ea64e3 --- /dev/null +++ b/spec/defines/server/options_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'ssh::server::options' do + on_supported_os.each do |os, os_facts| + context "on #{os}" do + let(:facts) { os_facts } + let(:title) { 'test_options' } + let :pre_condition do + 'include ssh' + end + + context 'with simple key-value options' do + let(:params) do + { + options: { + 'PermitRootLogin' => 'no', + 'MaxAuthTries' => '3', + }, + } + end + + it { is_expected.to compile.with_all_deps } + + it { + is_expected.to contain_concat__fragment('options test_options').with( + target: '/etc/ssh/sshd_config', + order: '150', + ) + } + + it { is_expected.to contain_concat__fragment('options test_options').with_content(%r{PermitRootLogin no}) } + it { is_expected.to contain_concat__fragment('options test_options').with_content(%r{MaxAuthTries 3}) } + end + + context 'with boolean values' do + let(:params) do + { + options: { + 'X11Forwarding' => true, + 'PasswordAuthentication' => false, + }, + } + end + + it { is_expected.to compile.with_all_deps } + it { is_expected.to contain_concat__fragment('options test_options').with_content(%r{X11Forwarding yes}) } + it { is_expected.to contain_concat__fragment('options test_options').with_content(%r{PasswordAuthentication no}) } + end + + context 'with array values' do + let(:params) do + { + options: { + 'AcceptEnv' => %w[LANG LC_CTYPE LC_ALL], + }, + } + end + + it { is_expected.to compile.with_all_deps } + it { is_expected.to contain_concat__fragment('options test_options').with_content(%r{AcceptEnv LANG}) } + it { is_expected.to contain_concat__fragment('options test_options').with_content(%r{AcceptEnv LC_CTYPE}) } + it { is_expected.to contain_concat__fragment('options test_options').with_content(%r{AcceptEnv LC_ALL}) } + end + + context 'with hash values (subsection)' do + let(:params) do + { + options: { + 'Match User deploy' => { + 'ChrootDirectory' => '/home/deploy', + 'ForceCommand' => 'internal-sftp', + }, + }, + } + end + + it { is_expected.to compile.with_all_deps } + it { is_expected.to contain_concat__fragment('options test_options').with_content(%r{Match User deploy}) } + it { is_expected.to contain_concat__fragment('options test_options').with_content(%r{ChrootDirectory /home/deploy}) } + it { is_expected.to contain_concat__fragment('options test_options').with_content(%r{ForceCommand internal-sftp}) } + end + + context 'with custom order' do + let(:params) do + { + options: { + 'LogLevel' => 'VERBOSE', + }, + order: 10, + } + end + + it { + is_expected.to contain_concat__fragment('options test_options').with( + order: '110', + ) + } + end + + context 'with empty options' do + let(:params) do + { + options: {}, + } + end + + it { is_expected.to compile.with_all_deps } + it { is_expected.to contain_concat__fragment('options test_options') } + end + end + end +end