diff --git a/CHANGELOG b/CHANGELOG index ad55145e..61000161 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +* Fri Apr 12 2019 Michael Morrone - 3.15.0-0 +- Added `simplib__sshd_config` fact to check the contents of sshd_config file + * Fri Apr 05 2019 Joseph Sharkey - 3.15.0-0 - Re-enable simplib deprecation warnings by default diff --git a/lib/facter/simplib__sshd_config.rb b/lib/facter/simplib__sshd_config.rb new file mode 100644 index 00000000..c3395a8e --- /dev/null +++ b/lib/facter/simplib__sshd_config.rb @@ -0,0 +1,80 @@ +# _Description_ +# +# Return values from the /etc/ssh/sshd_conf file +# +Facter.add('simplib__sshd_config') do + confine { File.exist?('/etc/ssh/sshd_config') && File.readable?('/etc/ssh/sshd_config')} + + setcode do + + # Items that we wish to pull from the configuration + # + # Format: + # Key => Default Value + selected_settings = { + 'AuthorizedKeysFile' => '.ssh/authorized_keys' + } + + sshd = Facter::Util::Resolution.which('sshd') + if sshd + full_version = Facter::Core::Execution.execute("#{sshd} --help 2>&1", :on_fail => :failed) + + unless full_version == :failed + sshd_config ||= {} + + full_version = full_version.lines.grep(/^\s*OpenSSH_\d/).first + + if full_version + sshd_config['version'] = full_version.split(/,|\s/).first.split('_').last + sshd_config['full_version'] = full_version + end + end + end + + sshd_disk_config = File.read('/etc/ssh/sshd_config') + + match_section = nil + sshd_disk_config.lines do |line| + line.strip! + + next if line.empty? + next if line[0].chr == '#' + + if config_parts = line.match(/^(?:(?.+?))\s+(?.+)\s*$/) + if config_parts[:key] == 'Match' + match_section = line + next + end + + next unless selected_settings.keys.include?(config_parts[:key]) + + if match_section + sshd_config ||= {} + sshd_config[match_section] ||= {} + + if sshd_config[match_section][config_parts[:key]] + sshd_config[match_section][config_parts[:key]] = Array(sshd_config[match_section][config_parts[:key]]) + sshd_config[match_section][config_parts[:key]] << config_parts[:value] + else + sshd_config[match_section][config_parts[:key]] = config_parts[:value] + end + else + sshd_config ||= {} + + if sshd_config[config_parts[:key]] + sshd_config[config_parts[:key]] = Array(sshd_config[config_parts[:key]]) + sshd_config[config_parts[:key]] << config_parts[:value] + else + sshd_config[config_parts[:key]] = config_parts[:value] + end + end + end + end + + if sshd_config + # Add in any defaults that we missed + # This should *not* be a deep_merge! + selected_settings.merge(sshd_config) + end + end +end diff --git a/spec/unit/facter/simplib__sshd_config_spec.rb b/spec/unit/facter/simplib__sshd_config_spec.rb new file mode 100644 index 00000000..dbb8aef9 --- /dev/null +++ b/spec/unit/facter/simplib__sshd_config_spec.rb @@ -0,0 +1,245 @@ +require 'spec_helper' + +describe "simplib__sshd_config" do + + before :each do + Facter.clear + + Facter::Util::Resolution.expects(:which).with('sshd').returns('/usr/bin/sshd') + Facter::Core::Execution.expects(:execute).with('/usr/bin/sshd --help 2>&1', :on_fail => :failed).returns(openssh_version['full_version']) + + File.expects(:exist?).with('/etc/ssh/sshd_config').returns(true) + File.expects(:readable?).with('/etc/ssh/sshd_config').returns(true) + File.expects(:read).with('/etc/ssh/sshd_config').returns(sshd_config_content) + + # This resets the stubbing code in Mocha to ensure that the code does not + # try to catch any other calls to the stubbed methods above. + # + # This is not documented well and is almost always what you want in + # Puppet testing + + File.stubs(:exist?).with(Not(equals('/etc/ssh/sshd_config'))) + File.stubs(:readable?).with(Not(equals('/etc/ssh/sshd_config'))) + File.stubs(:read).with(Not(equals('/etc/ssh/sshd_config'))) + end + + let(:openssh_version) {{ + 'full_version' => 'OpenSSH_7.9p1, OpenSSL 1.1.1a FIPS 20 Nov 2018', + 'version' => '7.9p1' + }} + + context 'with a simp /etc/ssh/sshd_config' do + let(:sshd_config_content) { <<-EOM +#Brief chunk of file +Port 22 +ListenAddress 0.0.0.0 +#ListenAddress :: + +#PubkeyAuthentication yes + +# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2 +# but this is overridden so installations will only check .ssh/authorized_keys +AuthorizedKeysFile /etc/ssh/local_keys/%u + +#AuthorizedPrincipalsFile none + +#AuthorizedKeysCommand none +AuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys +#AuthorizedKeysCommandUser nobody +AuthorizedKeysCommandUser nobody + +# Even inline comments! + # And indented comments + EOM + } + it { + expect(Facter.fact('simplib__sshd_config').value).to eq(openssh_version.merge({"AuthorizedKeysFile"=>"/etc/ssh/local_keys/%u"})) + } + end + + context 'with a default /etc/ssh/sshd_config' do + let(:sshd_config_content) { <<-EOM +#Brief chunk of file +#Port 22 +#AddressFamily any +#ListenAddress 0.0.0.0 +#ListenAddress :: + +#PubkeyAuthentication yes + +# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2 +# but this is overridden so installations will only check .ssh/authorized_keys +AuthorizedKeysFile .ssh/authorized_keys + +#AuthorizedPrincipalsFile none + +#AuthorizedKeysCommand none +#AuthorizedKeysCommandUser nobody + +# Even inline comments! + # And indented comments + EOM + } + + it { + expect(Facter.fact(:simplib__sshd_config).value).to eq(openssh_version.merge({ 'AuthorizedKeysFile' => '.ssh/authorized_keys' })) + } + + context 'when the SSH daemon does not return a version string' do + let(:openssh_version) {{ + 'full_version' => :failed, + 'version' => nil + }} + + it { + expect(Facter.fact(:simplib__sshd_config).value).to eq({ 'AuthorizedKeysFile' => '.ssh/authorized_keys' }) + } + end + + context 'when the SSH daemon does not return a valid version string' do + let(:openssh_version) {{ + 'full_version' => 'OpenSSH_is amazing', + 'version' => nil + }} + + it { + expect(Facter.fact(:simplib__sshd_config).value).to eq({ 'AuthorizedKeysFile' => '.ssh/authorized_keys' }) + } + end + end + + context 'with a commented values /etc/ssh/sshd_config' do + let(:sshd_config_content) { <<-EOM +#Brief chunk of file +#Port 22 +#AddressFamily any +#ListenAddress 0.0.0.0 +#ListenAddress :: + +#PubkeyAuthentication yes + +# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2 +# but this is overridden so installations will only check .ssh/authorized_keys +#AuthorizedKeysFile /etc/ssh/local_keys/%u + +#AuthorizedPrincipalsFile none + +#AuthorizedKeysCommand none +#AuthorizedKeysCommandUser nobody + EOM + } + + it { + expect(Facter.fact(:simplib__sshd_config).value).to eq(openssh_version.merge({ 'AuthorizedKeysFile' => '.ssh/authorized_keys' })) + } + end + + context 'with empty /etc/ssh/sshd_config' do + let(:sshd_config_content) {''} + + it { + expect(Facter.fact(:simplib__sshd_config).value).to eq(openssh_version.merge({ 'AuthorizedKeysFile' => '.ssh/authorized_keys' })) + } + end + + context 'with multiple matching entries' do + let(:sshd_config_content) { <<-EOM +#Brief chunk of file +#Port 22 +#AddressFamily any +#ListenAddress 0.0.0.0 +#ListenAddress :: + +#PubkeyAuthentication yes + +# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2 +# but this is overridden so installations will only check .ssh/authorized_keys +AuthorizedKeysFile /etc/ssh/local_keys/%u +AuthorizedKeysFile /foo/bar/baz + +#AuthorizedPrincipalsFile none + +#AuthorizedKeysCommand none +#AuthorizedKeysCommandUser nobody + EOM + } + + it { + expect(Facter.fact(:simplib__sshd_config).value).to eq(openssh_version.merge( + { 'AuthorizedKeysFile' => ['/etc/ssh/local_keys/%u', '/foo/bar/baz'] } + )) + } + end + + context 'with multiple entries in a Match block' do + let(:sshd_config_content) { <<-EOM +#Brief chunk of file +#Port 22 +#AddressFamily any +#ListenAddress 0.0.0.0 +#ListenAddress :: + +#PubkeyAuthentication yes + +# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2 +# but this is overridden so installations will only check .ssh/authorized_keys +Match user foo + AuthorizedKeysFile /etc/ssh/local_keys/%u + AuthorizedKeysFile /foo/bar/baz + +#AuthorizedPrincipalsFile none + +#AuthorizedKeysCommand none +#AuthorizedKeysCommandUser nobody + EOM + } + + it { + expect(Facter.fact(:simplib__sshd_config).value).to eq(openssh_version.merge( + { + 'AuthorizedKeysFile' => '.ssh/authorized_keys', + 'Match user foo' => { + 'AuthorizedKeysFile' => [ '/etc/ssh/local_keys/%u', '/foo/bar/baz'] + } + } + )) + } + end + + context 'with global and Match block entries' do + let(:sshd_config_content) { <<-EOM +#Brief chunk of file +#Port 22 +#AddressFamily any +#ListenAddress 0.0.0.0 +#ListenAddress :: + +#PubkeyAuthentication yes + +AuthorizedKeysFile /global/time + +# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2 +# but this is overridden so installations will only check .ssh/authorized_keys +Match user foo + AuthorizedKeysFile /etc/ssh/local_keys/%u + AuthorizedKeysFile /foo/bar/baz + +#AuthorizedPrincipalsFile none + +#AuthorizedKeysCommand none +#AuthorizedKeysCommandUser nobody + EOM + } + + it { + expect(Facter.fact(:simplib__sshd_config).value).to eq(openssh_version.merge( + { + 'AuthorizedKeysFile' => '/global/time', + 'Match user foo' => { + 'AuthorizedKeysFile' => [ '/etc/ssh/local_keys/%u', '/foo/bar/baz'] + } + } + )) + } + end +end