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