├── files └── .keep ├── spec ├── fixtures │ └── modules │ │ └── ssh_hardening │ │ ├── files │ │ ├── lib │ │ └── manifests ├── classes │ ├── client_spec.rb │ └── server_spec.rb ├── functions │ ├── get_ssh_macs_spec.rb │ ├── get_ssh_ciphers_spec.rb │ └── get_ssh_kex_spec.rb └── spec_helper.rb ├── .puppet-lint.rc ├── Puppetfile ├── .gitignore ├── .fixtures.yml ├── Thorfile ├── .github └── workflows │ └── codespell.yml ├── renovate.json ├── Guardfile ├── Modulefile ├── .rubocop.yml ├── metadata.json ├── .travis.yml ├── Rakefile ├── Gemfile ├── lib └── puppet │ └── parser │ └── functions │ ├── use_privilege_separation.rb │ ├── get_ssh_kex.rb │ ├── get_ssh_ciphers.rb │ └── get_ssh_macs.rb ├── CHANGELOG.md ├── .kitchen.yml ├── manifests ├── init.pp ├── client.pp └── server.pp └── README.md /files/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/fixtures/modules/ssh_hardening/files: -------------------------------------------------------------------------------- 1 | ../../../../files -------------------------------------------------------------------------------- /spec/fixtures/modules/ssh_hardening/lib: -------------------------------------------------------------------------------- 1 | ../../../../lib -------------------------------------------------------------------------------- /.puppet-lint.rc: -------------------------------------------------------------------------------- 1 | --no-autoloader_layout-check 2 | --no-80chars-check -------------------------------------------------------------------------------- /spec/fixtures/modules/ssh_hardening/manifests: -------------------------------------------------------------------------------- 1 | ../../../../manifests -------------------------------------------------------------------------------- /Puppetfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | #^syntax detection 3 | 4 | forge "http://forge.puppetlabs.com" 5 | 6 | # use dependencies defined in Modulefile 7 | modulefile -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | exp.* 2 | .kitchen 3 | .kitchen.local.yml 4 | shared_test_repo/ 5 | .librarian/ 6 | .tmp/ 7 | test/integration 8 | 9 | Gemfile.lock 10 | Puppetfile.lock 11 | spec/fixtures 12 | -------------------------------------------------------------------------------- /.fixtures.yml: -------------------------------------------------------------------------------- 1 | fixtures: 2 | repositories: 3 | ssh: 4 | repo: https://github.com/saz/puppet-ssh.git 5 | ref: v2.3.6 6 | stdlib: https://github.com/puppetlabs/puppetlabs-stdlib.git 7 | -------------------------------------------------------------------------------- /Thorfile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'bundler' 4 | require 'bundler/setup' 5 | require 'kitchen_sharedtests' 6 | require 'kitchen/sharedtests_thor_tasks' 7 | 8 | Kitchen::SharedtestsThorTasks.new 9 | -------------------------------------------------------------------------------- /.github/workflows/codespell.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Codespell - Spellcheck 3 | 4 | on: # yamllint disable-line rule:truthy 5 | push: 6 | branches: [master] 7 | pull_request: 8 | branches: [master] 9 | 10 | jobs: 11 | codespell: 12 | uses: "dev-sec/.github/.github/workflows/codespell.yml@main" 13 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | ":gitSignOff" 6 | ], 7 | "dependencyDashboard": true, 8 | "dependencyDashboardAutoclose": true, 9 | "packageRules": [ 10 | { 11 | "matchUpdateTypes": ["patch", "minor"], 12 | "automerge": true 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # Guardfile 4 | 5 | guard 'rake', :task => 'lint' do 6 | watch(%r{^manifests/.*$}) 7 | watch(%r{^templates/.*$}) 8 | end 9 | 10 | guard 'rake', :task => 'spec' do 11 | watch(%r{^spec/(classes|defines)/.+_spec\.rb$}) 12 | watch('spec/spec_helper.rb') 13 | watch(%r{^lib/.*$}) 14 | watch(%r{^manifests/.*$}) 15 | watch(%r{^templates/.*$}) 16 | end 17 | -------------------------------------------------------------------------------- /Modulefile: -------------------------------------------------------------------------------- 1 | name 'hardening/ssh_hardening' 2 | version '1.0.5' 3 | source 'https://github.com/TelekomLabs/puppet-ssh-hardening' 4 | author 'Dominik Richter' 5 | license 'Apache License, Version 2.0' 6 | summary 'Installs and configures OpenSSH with hardening' 7 | description 'Installs and configures OpenSSH with hardening' 8 | project_page 'https://github.com/TelekomLabs/puppet-ssh-hardening' 9 | 10 | dependency 'saz/ssh', '>= 2.3.6' 11 | dependency 'puppetlabs/stdlib', '>= 4.2.0' 12 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AllCops: 3 | Exclude: 4 | - vendor/**/* 5 | - test/**/* 6 | - spec/fixtures/**/* 7 | - Puppetfile 8 | Documentation: 9 | Enabled: false 10 | AlignParameters: 11 | Enabled: true 12 | Encoding: 13 | Enabled: true 14 | HashSyntax: 15 | Enabled: false 16 | LineLength: 17 | Enabled: false 18 | EmptyLinesAroundBlockBody: 19 | Enabled: false 20 | MethodLength: 21 | Max: 40 22 | NumericLiterals: 23 | MinDigits: 10 24 | Metrics/CyclomaticComplexity: 25 | Max: 10 26 | Metrics/PerceivedComplexity: 27 | Max: 10 28 | Metrics/AbcSize: 29 | Max: 29 30 | Style/DotPosition: 31 | EnforcedStyle: trailing 32 | Enabled: true 33 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardening-ssh_hardening", 3 | "version": "1.0.5", 4 | "source": "https://github.com/TelekomLabs/puppet-ssh-hardening", 5 | "author": "Dominik Richter", 6 | "license": "Apache License, Version 2.0", 7 | "summary": "Installs and configures OpenSSH with hardening", 8 | "description": "Installs and configures OpenSSH with hardening", 9 | "project_page": "https://github.com/TelekomLabs/puppet-ssh-hardening", 10 | "dependencies": [ 11 | { 12 | "name": "saz/ssh", 13 | "version_requirement": ">= 2.3.6" 14 | },{ 15 | "name": "puppetlabs/stdlib", 16 | "version_requirement": ">= 4.2.0" 17 | } 18 | ], 19 | "types": [ 20 | 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.8.7 3 | - 1.9.3 4 | - 2.0.0 5 | - 2.1.0 6 | language: ruby 7 | bundler_args: --without development integration openstack 8 | env: 9 | - PUPPET_VERSION="~> 4.0.0" 10 | - PUPPET_VERSION="~> 3.7.5" 11 | - PUPPET_VERSION="~> 3.6.2" 12 | - PUPPET_VERSION="~> 2.7.0" 13 | matrix: 14 | fast_finish: true 15 | exclude: 16 | - rvm: 1.9.3 17 | env: PUPPET_VERSION="~> 2.7.0" 18 | - rvm: 2.0.0 19 | env: PUPPET_VERSION="~> 2.7.0" 20 | - rvm: 2.1.0 21 | env: PUPPET_VERSION="~> 2.7.0" 22 | - rvm: 1.8.7 23 | env: PUPPET_VERSION="~> 4.0.0" 24 | - rvm: 1.9.3 25 | env: PUPPET_VERSION="~> 4.0.0" 26 | - rvm: 2.0.0 27 | env: PUPPET_VERSION="~> 4.0.0" 28 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'puppet-lint/tasks/puppet-lint' 4 | require 'puppetlabs_spec_helper/rake_tasks' 5 | 6 | PuppetLint.configuration.send('disable_autoloader_layout') 7 | PuppetLint.configuration.send('disable_80chars') 8 | PuppetLint.configuration.fail_on_warnings = true 9 | PuppetLint.configuration.ignore_paths = ['vendor/**/*.pp'] 10 | 11 | if RUBY_VERSION > '1.9.2' 12 | require 'rubocop' 13 | require 'rubocop/rake_task' 14 | 15 | desc 'Run all linters: rubocop and puppet-lint' 16 | task :run_all_linters => [:rubocop, :lint] 17 | 18 | # Rubocop 19 | desc 'Run Rubocop lint checks' 20 | task :rubocop do 21 | RuboCop::RakeTask.new 22 | end 23 | 24 | task :default => [:run_all_linters, :spec] 25 | 26 | else 27 | desc 'Run all linters: rubocop and puppet-lint' 28 | task :run_all_linters => [:lint] 29 | 30 | task :default => [:lint, :spec] 31 | end 32 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | source 'https://rubygems.org' 4 | 5 | puppetversion = ENV['PUPPET_VERSION'] 6 | if puppetversion 7 | gem 'puppet', puppetversion, :require => false 8 | else 9 | gem 'puppet', :require => false 10 | end 11 | 12 | group :test do 13 | gem 'rake' 14 | gem 'rspec', '~> 3.13.0' 15 | gem 'rspec-puppet' 16 | # avoid NoMethodError: private method `clone' called for # 17 | gem 'puppetlabs_spec_helper', :git => 'https://github.com/ehaselwanter/puppetlabs_spec_helper' 18 | gem 'puppet-lint' 19 | gem 'rubocop', '~> 1.0' 20 | end 21 | 22 | group :development do 23 | gem 'guard-rake' 24 | end 25 | 26 | group :integration do 27 | gem 'test-kitchen' 28 | gem 'kitchen-vagrant' 29 | gem 'kitchen-puppet' 30 | gem 'librarian-puppet' 31 | gem 'kitchen-sharedtests', '~> 0.2.0' 32 | end 33 | 34 | group :openstack do 35 | gem 'kitchen-openstack' 36 | end 37 | -------------------------------------------------------------------------------- /lib/puppet/parser/functions/use_privilege_separation.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Copyright 2015, Dominik Richter 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | Puppet::Parser::Functions.newfunction(:use_privilege_separation, :type => :rvalue) do |args| 19 | os = args[0].downcase 20 | osrelease = args[1] 21 | osmajor = osrelease.sub(/\..*/, '') 22 | 23 | ps53 = 'yes' 24 | ps59 = 'sandbox' 25 | ps = ps59 26 | 27 | # redhat/centos/oracle 6.x has ssh 5.3 28 | if os == 'redhat' || os == 'centos' || os == 'oraclelinux' 29 | ps = ps53 30 | 31 | # debian 7.x and newer has ssh 5.9+ 32 | elsif os == 'debian' && osmajor.to_i <= 6 33 | ps = ps53 34 | end 35 | 36 | ps 37 | end 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.5 4 | 5 | * feature: UsePrivilegeSeparation = sandbox for ssh >= 5.9 6 | * feature: support Puppet 4 7 | 8 | ## 1.0.4 9 | 10 | * improvement: reprioritize EtM-based MACs 11 | * improvement: move SHA1 KEX algos from default to weak profile 12 | 13 | ## 1.0.3 14 | 15 | * feature: remove legacy SSHv1 code 16 | * feature: add back GCM cipher 17 | 18 | ## 1.0.2 19 | 20 | * bugfix: pass allow_{tcp,agent}_forwarding from init to classes 21 | 22 | ## 1.0.1 23 | 24 | * feature: add back puppet 3.4.3 support 25 | * feature: make other SSH configuration options configurable 26 | * improvement: added faq on locked accounts to readme 27 | 28 | ## 1.0.0 29 | 30 | * feature: add support for oracle linux in crypto 31 | * feature: add configurable agent and tcp forwarding 32 | * feature: add kex configuration for redhat and centos 33 | * feature: add mac configuration for redhat and centos 34 | * feature: add cipher configuration for redhat and centos 35 | * feature: implemented cipher detection for ubuntu 12.04 and 14.04 36 | * feature: implemented mac detection for ubuntu 12.04 and 14.04 37 | * feature: implemented kex detection for ubuntu 12.04 and 14.04 38 | * feature: make crypto configuration conditional on the OS 39 | * bugfix: dont set `undef` values in ssh client config 40 | 41 | ## 0.1.1 42 | 43 | * improvement: add kitchen tests (pre-release) 44 | * improvement: puppet-lint fixes 45 | * bugfix: adjust client ciphers to working set 46 | * bugfix: correct string concatenantions 47 | * bugfix: mac -> macs 48 | 49 | ## 0.1.0 50 | 51 | Initial release 52 | -------------------------------------------------------------------------------- /spec/classes/client_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Copyright 2014, Deutsche Telekom AG 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require 'spec_helper' 19 | 20 | describe 'ssh_hardening::client' do 21 | 22 | let(:facts) do 23 | { :osfamily => 'redhat' } 24 | end 25 | 26 | it { should contain_class('ssh::client').with_storeconfigs_enabled(false) } 27 | 28 | # default configuration 29 | expect_option('ssh::client', 'Port', [22]) 30 | # user configuration 31 | context 'with ports => [8022]' do 32 | let(:params) { { :ports => [8022] } } 33 | expect_option('ssh::client', 'Port', [8022]) 34 | end 35 | 36 | # default configuration 37 | expect_option('ssh::client', 'AddressFamily', 'inet') 38 | # user configuration 39 | context 'with ipv6_enabled => true' do 40 | let(:params) { { :ipv6_enabled => true } } 41 | expect_option('ssh::client', 'AddressFamily', 'any') 42 | end 43 | context 'with ipv6_enabled => false' do 44 | let(:params) { { :ipv6_enabled => false } } 45 | expect_option('ssh::client', 'AddressFamily', 'inet') 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /spec/functions/get_ssh_macs_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Copyright 2014, Deutsche Telekom AG 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require 'spec_helper' 19 | 20 | describe 'get_ssh_macs' do 21 | 22 | it 'should exist' do 23 | Puppet::Parser::Functions.function('get_ssh_macs').should == 'function_get_ssh_macs' 24 | end 25 | 26 | # it should get the correct macs (default) 27 | it do 28 | should run.with_params('', '', false). 29 | and_return('hmac-sha2-512,hmac-sha2-256,hmac-ripemd160') 30 | end 31 | 32 | # it should get the correct macs (default weak) 33 | it do 34 | should run.with_params('', '', true). 35 | and_return('hmac-sha2-512,hmac-sha2-256,hmac-ripemd160,hmac-sha1') 36 | end 37 | 38 | # it should get the correct macs (ubuntu 12.04, default) 39 | it do 40 | should run.with_params('ubuntu', '12.04', false). 41 | and_return('hmac-sha2-512,hmac-sha2-256,hmac-ripemd160') 42 | end 43 | 44 | # it should get the correct macs (ubuntu 12.04, weak) 45 | it do 46 | should run.with_params('ubuntu', '12.04', true). 47 | and_return('hmac-sha2-512,hmac-sha2-256,hmac-ripemd160,hmac-sha1') 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /spec/functions/get_ssh_ciphers_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Copyright 2014, Deutsche Telekom AG 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require 'spec_helper' 19 | 20 | describe 'get_ssh_ciphers' do 21 | 22 | it 'should exist' do 23 | Puppet::Parser::Functions.function('get_ssh_ciphers').should == 'function_get_ssh_ciphers' 24 | end 25 | 26 | # should get the correct ciphers (default) 27 | it do 28 | should run.with_params('', '', false). 29 | and_return('aes256-ctr,aes192-ctr,aes128-ctr') 30 | end 31 | 32 | # should get the correct ciphers (default weak) 33 | it do 34 | should run.with_params('', '', true). 35 | and_return('aes256-ctr,aes192-ctr,aes128-ctr,aes256-cbc,aes192-cbc,aes128-cbc') 36 | end 37 | 38 | # should get the correct ciphers (ubuntu 12.04, default) 39 | it do 40 | should run.with_params('ubuntu', '12.04', false). 41 | and_return('aes256-ctr,aes192-ctr,aes128-ctr') 42 | end 43 | 44 | # should get the correct ciphers (ubuntu 12.04, weak) 45 | it do 46 | should run.with_params('ubuntu', '12.04', true). 47 | and_return('aes256-ctr,aes192-ctr,aes128-ctr,aes256-cbc,aes192-cbc,aes128-cbc') 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /spec/functions/get_ssh_kex_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Copyright 2014, Deutsche Telekom AG 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require 'spec_helper' 19 | 20 | describe 'get_ssh_kex' do 21 | 22 | it 'should exist' do 23 | Puppet::Parser::Functions.function('get_ssh_kex').should == 'function_get_ssh_kex' 24 | end 25 | 26 | # should get the correct kex (default) 27 | it do 28 | should run.with_params('', '', false). 29 | and_return('diffie-hellman-group-exchange-sha256') 30 | end 31 | 32 | # should get the correct kex (default weak) 33 | it do 34 | should run.with_params('', '', true). 35 | and_return('diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group1-sha1') 36 | end 37 | 38 | # should get the correct kex (ubuntu 12.04, default) 39 | it do 40 | should run.with_params('ubuntu', '12.04', false). 41 | and_return('diffie-hellman-group-exchange-sha256') 42 | end 43 | 44 | # should get the correct kex (ubuntu 12.04, weak) 45 | it do 46 | should run.with_params('ubuntu', '12.04', true). 47 | and_return('diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group1-sha1') 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /.kitchen.yml: -------------------------------------------------------------------------------- 1 | --- 2 | driver: 3 | name: vagrant 4 | provisioner: 5 | name: puppet_apply 6 | test_repo_uri: https://github.com/TelekomLabs/tests-ssh-hardening.git 7 | platforms: 8 | - name: ubuntu-12.04 9 | driver_config: 10 | box: ubuntu/precise64 11 | box_url: https://atlas.hashicorp.com/ubuntu/boxes/precise64/versions/20150730.1.0/providers/virtualbox.box 12 | - name: ubuntu-14.04 13 | driver_config: 14 | box: ubuntu/trusty64 15 | box_url: https://atlas.hashicorp.com/ubuntu/boxes/trusty64/versions/20150609.0.10/providers/virtualbox.box 16 | - name: centos-6.4 17 | driver_config: 18 | box: opscode-centos-6.4 19 | box_url: https://opscode-vm.s3.amazonaws.com/vagrant/opscode_centos-6.4_provisionerless.box 20 | - name: centos-6.5 21 | driver_config: 22 | box: opscode-centos-6.5 23 | box_url: https://opscode-vm-bento.s3.amazonaws.com/vagrant/virtualbox/opscode_centos-6.5_chef-provisionerless.box 24 | - name: centos-7.1 25 | driver_config: 26 | box: opscode-centos-7.1 27 | box_url: https://opscode-vm-bento.s3.amazonaws.com/vagrant/virtualbox/opscode_centos-7.1_chef-provisionerless.box 28 | - name: oracle-6.4 29 | driver_config: 30 | box: oracle-6.4 31 | box_url: https://storage.us2.oraclecloud.com/v1/istoilis-istoilis/vagrant/oel64-64.box 32 | - name: oracle-6.5 33 | driver_config: 34 | box: oracle-6.5 35 | box_url: https://storage.us2.oraclecloud.com/v1/istoilis-istoilis/vagrant/oel65-64.box 36 | - name: debian-6 37 | driver_config: 38 | box: puppetlabs/debian-6.0.10-64-puppet 39 | box_url: https://atlas.hashicorp.com/puppetlabs/boxes/debian-6.0.10-64-puppet/versions/1.0.2/providers/virtualbox.box 40 | - name: debian-7 41 | driver_config: 42 | box: debian/wheezy64 43 | box_url: https://atlas.hashicorp.com/debian/boxes/wheezy64/versions/7.8.5/providers/virtualbox.box 44 | - name: debian-8 45 | driver_config: 46 | box: debian/jessie64 47 | box_url: https://atlas.hashicorp.com/debian/boxes/jessie64/versions/8.1.0/providers/virtualbox.box 48 | suites: 49 | - name: default 50 | manifest: site.pp 51 | -------------------------------------------------------------------------------- /lib/puppet/parser/functions/get_ssh_kex.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Copyright 2014, Deutsche Telekom AG 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | Puppet::Parser::Functions.newfunction(:get_ssh_kex, :type => :rvalue) do |args| 19 | os = args[0].downcase 20 | osrelease = args[1] 21 | osmajor = osrelease.sub(/\..*/, '') 22 | weak_kex = args[2] ? 'weak' : 'default' 23 | 24 | kex_59 = {} 25 | kex_59.default = 'diffie-hellman-group-exchange-sha256' 26 | kex_59['weak'] = kex_59['default'] + ',diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group1-sha1' 27 | 28 | kex_66 = {} 29 | kex_66.default = 'curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256' 30 | kex_66['weak'] = kex_66['default'] + ',diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group1-sha1' 31 | 32 | # creat the default version map (if os + version are default) 33 | default_vmap = {} 34 | default_vmap.default = kex_59 35 | 36 | # create the main map 37 | m = {} 38 | m.default = default_vmap 39 | 40 | m['ubuntu'] = {} 41 | m['ubuntu']['12'] = kex_59 42 | m['ubuntu']['14'] = kex_66 43 | m['ubuntu'].default = kex_59 44 | 45 | m['debian'] = {} 46 | m['debian']['6'] = '' 47 | m['debian']['7'] = kex_59 48 | m['debian']['8'] = kex_66 49 | m['debian'].default = kex_59 50 | 51 | m['redhat'] = {} 52 | m['redhat']['6'] = '' 53 | m['redhat'].default = kex_59 54 | 55 | m['centos'] = m['redhat'] 56 | m['oraclelinux'] = m['redhat'] 57 | 58 | m[os][osmajor][weak_kex] 59 | end 60 | -------------------------------------------------------------------------------- /lib/puppet/parser/functions/get_ssh_ciphers.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Copyright 2014, Deutsche Telekom AG 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | Puppet::Parser::Functions.newfunction(:get_ssh_ciphers, :type => :rvalue) do |args| 19 | os = args[0].downcase 20 | osrelease = args[1] 21 | osmajor = osrelease.sub(/\..*/, '') 22 | weak_ciphers = args[2] ? 'weak' : 'default' 23 | 24 | ciphers_53 = {} 25 | ciphers_53.default = 'aes256-ctr,aes192-ctr,aes128-ctr' 26 | ciphers_53['weak'] = ciphers_53['default'] + ',aes256-cbc,aes192-cbc,aes128-cbc' 27 | 28 | ciphers_66 = {} 29 | ciphers_66.default = 'chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr' 30 | ciphers_66['weak'] = ciphers_66['default'] + ',aes256-cbc,aes192-cbc,aes128-cbc' 31 | 32 | # creat the default version map (if os + version are default) 33 | default_vmap = {} 34 | default_vmap.default = ciphers_53 35 | 36 | # create the main map 37 | m = {} 38 | m.default = default_vmap 39 | 40 | m['ubuntu'] = {} 41 | m['ubuntu']['12'] = ciphers_53 42 | m['ubuntu']['14'] = ciphers_66 43 | m['ubuntu'].default = ciphers_53 44 | 45 | m['debian'] = {} 46 | m['debian']['6'] = ciphers_53 47 | m['debian']['7'] = ciphers_53 48 | m['debian']['8'] = ciphers_66 49 | m['debian'].default = ciphers_53 50 | 51 | m['redhat'] = {} 52 | m['redhat']['6'] = ciphers_53 53 | m['redhat'].default = ciphers_53 54 | 55 | m['centos'] = m['redhat'] 56 | m['oraclelinux'] = m['redhat'] 57 | 58 | m[os][osmajor][weak_ciphers] 59 | end 60 | -------------------------------------------------------------------------------- /lib/puppet/parser/functions/get_ssh_macs.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Copyright 2014, Deutsche Telekom AG 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | Puppet::Parser::Functions.newfunction(:get_ssh_macs, :type => :rvalue) do |args| 19 | os = args[0].downcase 20 | osrelease = args[1] 21 | osmajor = osrelease.sub(/\..*/, '') 22 | weak_macs = args[2] ? 'weak' : 'default' 23 | 24 | macs_53 = {} 25 | macs_53.default = 'hmac-ripemd160,hmac-sha1' 26 | 27 | macs_59 = {} 28 | macs_59.default = 'hmac-sha2-512,hmac-sha2-256,hmac-ripemd160' 29 | macs_59['weak'] = macs_59['default'] + ',hmac-sha1' 30 | 31 | macs_66 = {} 32 | macs_66.default = 'hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-ripemd160-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,hmac-ripemd160' 33 | macs_66['weak'] = macs_66['default'] + ',hmac-sha1' 34 | 35 | # creat the default version map (if os + version are default) 36 | default_vmap = {} 37 | default_vmap.default = macs_59 38 | 39 | # create the main map 40 | m = {} 41 | m.default = default_vmap 42 | 43 | m['ubuntu'] = {} 44 | m['ubuntu']['12'] = macs_59 45 | m['ubuntu']['14'] = macs_66 46 | m['ubuntu'].default = macs_59 47 | 48 | m['debian'] = {} 49 | m['debian']['6'] = macs_53 50 | m['debian']['7'] = macs_59 51 | m['debian']['8'] = macs_66 52 | m['debian'].default = macs_59 53 | 54 | m['redhat'] = {} 55 | m['redhat']['6'] = macs_53 56 | m['redhat'].default = macs_53 57 | 58 | m['centos'] = m['redhat'] 59 | m['oraclelinux'] = m['redhat'] 60 | 61 | m[os][osmajor][weak_macs] 62 | end 63 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Copyright 2014, Deutsche Telekom AG 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require 'puppetlabs_spec_helper/module_spec_helper' 19 | 20 | fixture_path = File.expand_path(File.join(__FILE__, '..', 'fixtures')) 21 | 22 | RSpec.configure do |c| 23 | c.module_path = File.join(fixture_path, 'modules') 24 | c.manifest_dir = File.join(fixture_path, 'manifests') 25 | c.environmentpath = File.expand_path(File.join(Dir.pwd, 'spec')) 26 | end 27 | 28 | # Wrap expected value to strings for older releases of Puppet 29 | # These will convert their number to strings; wrap around arrays 30 | def wrap_expected(val) 31 | return val if Puppet.version.to_f >= 4 32 | return val.map { |x| wrap_expected(x) } if val.is_a?(Array) 33 | val.to_s 34 | end 35 | 36 | # Helper function to expect a class to have a set of 37 | # options defined. These options are not first-class citizens 38 | # of puppet, but instead a key-value map. So regular rspec matchers 39 | # don't help. To stay DRY, introduce this helper. 40 | # 41 | # @param klass [String] the puppet class which must be contained 42 | # @param key [String] the key in the class' options map to test 43 | # @param val [String] the value in the class' options map to expect 44 | # @return Creates an rspec test to check this expectation. 45 | def expect_option(klass, key, val) 46 | # test each option 47 | it do 48 | should contain_class(klass).with_options( 49 | lambda do |map| 50 | # check 51 | if map[key] == wrap_expected(val) 52 | true 53 | else 54 | fail "#{klass} option #{key.inspect} doesn't match (-- expected, ++ actual):\n"\ 55 | "-- #{val.inspect}\n"\ 56 | "++ #{map[key].inspect}\n" 57 | end 58 | end 59 | ) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /manifests/init.pp: -------------------------------------------------------------------------------- 1 | # === Copyright 2 | # 3 | # Copyright 2014, Deutsche Telekom AG 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | 8 | # == Class: ssh_hardening 9 | # 10 | # The default SSH class which installs the SSH server and client 11 | # 12 | # === Parameters 13 | # 14 | # [*cbc_required*] 15 | # CBC-meachnisms are considered weaker and will not be used as ciphers by 16 | # default. Set this option to true if you really need CBC-based ciphers. 17 | # 18 | # [*weak_hmac*] 19 | # The HMAC-mechanisms are selected to be cryptographically strong. If you 20 | # require some weaker variants, set this option to true to get safe selection. 21 | # 22 | # [*weak_kex*] 23 | # The KEX-mechanisms are selected to be cryptographically strong. If you 24 | # require some weaker variants, set this option to true to get safe selection. 25 | # 26 | # [*ports*] 27 | # A list of ports that SSH expects to run on. Defaults to 22. 28 | # 29 | # [*listen_to*] 30 | # A list of addresses which SSH listens on. Best to specify only on address of 31 | # one interface. 32 | # 33 | # [*host_key_files*] 34 | # A list of host key files to use. 35 | # 36 | # [*client_alive_interval*] 37 | # Interval after which the server checks if the client is alive (in seconds). 38 | # 39 | # [*client_alive_count*] 40 | # The maximum number of failed client alive checks before the client is 41 | # forcefully disconnected. 42 | # 43 | # [*allow_root_with_key*] 44 | # Whether to allow login of root. If true, root may log in using the key files 45 | # specified in authroized_keys. Otherwise any login attempts as user root 46 | # are forbidden. 47 | # 48 | # [*ipv6_enabled*] 49 | # Set to true if you need IPv6 support in SSH. 50 | # 51 | # [*allow_tcp_forwarding*] 52 | # Set to true to allow TCP forwarding 53 | # 54 | # [*allow_agent_forwarding*] 55 | # Set to true to allow Agent forwarding 56 | # 57 | # [*server_options*] 58 | # Allow override of default server settings provided by the module. 59 | # 60 | # [*client_options*] 61 | # Allow override of default client settings provided by the module. 62 | # 63 | class ssh_hardening( 64 | $cbc_required = false, 65 | $weak_hmac = false, 66 | $weak_kex = false, 67 | $ports = [ 22 ], 68 | $listen_to = [], 69 | $host_key_files = [ 70 | '/etc/ssh/ssh_host_rsa_key', 71 | '/etc/ssh/ssh_host_dsa_key', 72 | '/etc/ssh/ssh_host_ecdsa_key' 73 | ], 74 | $client_alive_interval = 600, 75 | $client_alive_count = 3, 76 | $allow_root_with_key = false, 77 | $ipv6_enabled = false, 78 | $use_pam = false, 79 | $allow_tcp_forwarding = false, 80 | $allow_agent_forwarding = false, 81 | $max_auth_retries = 2, 82 | $server_options = {}, 83 | $client_options = {}, 84 | ) { 85 | class { 'ssh_hardening::server': 86 | cbc_required => $cbc_required, 87 | weak_hmac => $weak_hmac, 88 | weak_kex => $weak_kex, 89 | ports => $ports, 90 | listen_to => $listen_to, 91 | host_key_files => $host_key_files, 92 | client_alive_interval => $client_alive_interval, 93 | client_alive_count => $client_alive_count, 94 | allow_root_with_key => $allow_root_with_key, 95 | ipv6_enabled => $ipv6_enabled, 96 | use_pam => $use_pam, 97 | allow_tcp_forwarding => $allow_tcp_forwarding, 98 | allow_agent_forwarding => $allow_agent_forwarding, 99 | max_auth_retries => $max_auth_retries, 100 | options => $server_options, 101 | } 102 | class { 'ssh_hardening::client': 103 | ipv6_enabled => $ipv6_enabled, 104 | ports => $ports, 105 | cbc_required => $cbc_required, 106 | weak_hmac => $weak_hmac, 107 | weak_kex => $weak_kex, 108 | options => $client_options, 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /spec/classes/server_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Copyright 2014, Deutsche Telekom AG 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require 'spec_helper' 19 | 20 | describe 'ssh_hardening::server' do 21 | 22 | let(:facts) do 23 | { :osfamily => 'redhat' } 24 | end 25 | 26 | it do 27 | should contain_file('/etc/ssh').with( 28 | 'ensure' => 'directory', 29 | 'owner' => 'root', 30 | 'group' => 'root', 31 | 'mode' => '0755' 32 | ) 33 | end 34 | 35 | it { should contain_class('ssh::server').with_storeconfigs_enabled(false) } 36 | 37 | # default configuration 38 | expect_option('ssh::server', 'Port', [22]) 39 | # user configuration 40 | context 'with ports => [8022]' do 41 | let(:params) { { :ports => [8022] } } 42 | expect_option('ssh::server', 'Port', [8022]) 43 | end 44 | 45 | # user configuration 46 | context 'with listen_to => 1.2.3.4' do 47 | let(:params) { { :listen_to => '1.2.3.4' } } 48 | expect_option('ssh::server', 'ListenAddress', '1.2.3.4') 49 | end 50 | 51 | # default configuration 52 | expect_option('ssh::server', 'HostKey', []) 53 | # user configuration 54 | context 'with host_key_files => [/a/file]' do 55 | let(:params) { { :host_key_files => ['/a/file'] } } 56 | expect_option('ssh::server', 'HostKey', ['/a/file']) 57 | end 58 | 59 | # default configuration 60 | expect_option('ssh::server', 'ClientAliveInterval', 600) 61 | # user configuration 62 | context 'with client_alive_interval => 300' do 63 | let(:params) { { :client_alive_interval => 300 } } 64 | expect_option('ssh::server', 'ClientAliveInterval', 300) 65 | end 66 | 67 | # default configuration 68 | expect_option('ssh::server', 'ClientAliveCountMax', 3) 69 | # user configuration 70 | context 'with client_alive_count => 2' do 71 | let(:params) { { :client_alive_count => 2 } } 72 | expect_option('ssh::server', 'ClientAliveCountMax', 2) 73 | end 74 | 75 | # default configuration 76 | expect_option('ssh::server', 'PermitRootLogin', 'no') 77 | # user configuration 78 | context 'with allow_root_with_key => true' do 79 | let(:params) { { :allow_root_with_key => true } } 80 | expect_option('ssh::server', 'PermitRootLogin', 'without-password') 81 | end 82 | context 'with allow_root_with_key => false' do 83 | let(:params) { { :allow_root_with_key => false } } 84 | expect_option('ssh::server', 'PermitRootLogin', 'no') 85 | end 86 | 87 | # default configuration 88 | expect_option('ssh::server', 'AddressFamily', 'inet') 89 | # user configuration 90 | context 'with ipv6_enabled => true' do 91 | let(:params) { { :ipv6_enabled => true } } 92 | expect_option('ssh::server', 'AddressFamily', 'any') 93 | end 94 | context 'with ipv6_enabled => false' do 95 | let(:params) { { :ipv6_enabled => false } } 96 | expect_option('ssh::server', 'AddressFamily', 'inet') 97 | end 98 | 99 | # default configuration 100 | expect_option('ssh::server', 'UsePAM', 'no') 101 | # user configuration 102 | context 'with use_pam => true' do 103 | let(:params) { { :use_pam => true } } 104 | expect_option('ssh::server', 'UsePAM', 'yes') 105 | end 106 | context 'with use_pam => false' do 107 | let(:params) { { :use_pam => false } } 108 | expect_option('ssh::server', 'UsePAM', 'no') 109 | end 110 | 111 | # default configuration 112 | expect_option('ssh::server', 'AllowTcpForwarding', 'no') 113 | # user configuration 114 | context 'with allow_tcp_forwarding => true' do 115 | let(:params) { { :allow_tcp_forwarding => true } } 116 | expect_option('ssh::server', 'AllowTcpForwarding', 'yes') 117 | end 118 | context 'with allow_tcp_forwarding => true' do 119 | let(:params) { { :allow_tcp_forwarding => false } } 120 | expect_option('ssh::server', 'AllowTcpForwarding', 'no') 121 | end 122 | 123 | # default configuration 124 | expect_option('ssh::server', 'AllowAgentForwarding', 'no') 125 | # user configuration 126 | context 'with allow_agent_forwarding => true' do 127 | let(:params) { { :allow_agent_forwarding => true } } 128 | expect_option('ssh::server', 'AllowAgentForwarding', 'yes') 129 | end 130 | context 'with allow_agent_forwarding => false' do 131 | let(:params) { { :allow_agent_forwarding => false } } 132 | expect_option('ssh::server', 'AllowAgentForwarding', 'no') 133 | end 134 | 135 | end 136 | -------------------------------------------------------------------------------- /manifests/client.pp: -------------------------------------------------------------------------------- 1 | # === Copyright 2 | # 3 | # Copyright 2014, Deutsche Telekom AG 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | 8 | # == Class: ssh_hardening::client 9 | # 10 | # The default SSH class which installs the SSH client 11 | # 12 | # === Parameters 13 | # 14 | # [*cbc_required*] 15 | # CBC-meachnisms are considered weaker and will not be used as ciphers by 16 | # default. Set this option to true if you really need CBC-based ciphers. 17 | # 18 | # [*weak_hmac*] 19 | # The HMAC-mechanisms are selected to be cryptographically strong. If you 20 | # require some weaker variants, set this option to true to get safe selection. 21 | # 22 | # [*weak_kex*] 23 | # The KEX-mechanisms are selected to be cryptographically strong. If you 24 | # require some weaker variants, set this option to true to get safe selection. 25 | # 26 | # [*ports*] 27 | # A list of ports that SSH expects to run on. Defaults to 22. 28 | # 29 | # [*ipv6_enabled*] 30 | # Set to true if you need IPv6 support in SSH. 31 | # 32 | # [*options*] 33 | # Allow override of default settings provided by the module. 34 | # 35 | class ssh_hardening::client ( 36 | $cbc_required = false, 37 | $weak_hmac = false, 38 | $weak_kex = false, 39 | $ports = [ 22 ], 40 | $options = {}, 41 | $ipv6_enabled = false 42 | ) { 43 | if $ipv6_enabled == true { 44 | $addressfamily = 'any' 45 | } else { 46 | $addressfamily = 'inet' 47 | } 48 | 49 | $ciphers = get_ssh_ciphers($::operatingsystem, $::operatingsystemrelease, $cbc_required) 50 | $macs = get_ssh_macs($::operatingsystem, $::operatingsystemrelease, $weak_hmac) 51 | $kex = get_ssh_kex($::operatingsystem, $::operatingsystemrelease, $weak_kex) 52 | 53 | $ssh_options = { 54 | # Set the addressfamily according to IPv4 / IPv6 settings 55 | 'AddressFamily' => $addressfamily, 56 | 57 | # The port at the destination should be defined 58 | 'Port' => $ports, 59 | 60 | # Set the protocol family to 2 for security reasons. 61 | # Disables legacy support. 62 | 'Protocol' => 2, 63 | 64 | # Make sure passphrase querying is enabled 65 | 'BatchMode' => 'no', 66 | 67 | # Prevent IP spoofing by checking to host IP against the 68 | # `known_hosts` file. 69 | 'CheckHostIP' => 'yes', 70 | 71 | # Always ask before adding keys to the `known_hosts` file. 72 | # Do not set to `yes`. 73 | 'StrictHostKeyChecking' => 'ask', 74 | 75 | # **Ciphers** -- If your clients don't support CTR (eg older versions), 76 | # cbc will be added 77 | # CBC: is true if you want to connect with OpenSSL-base libraries 78 | # eg Ruby's older Net::SSH::Transport::CipherFactory requires CBC-versions 79 | # of the given openssh ciphers to work 80 | # 81 | 'Ciphers' => $ciphers, 82 | 83 | # **Hash algorithms** -- Make sure not to use SHA1 84 | # for hashing, unless it is really necessary. 85 | # Weak HMAC is sometimes required if older package 86 | # versions are used 87 | # eg Ruby's Net::SSH at around 2.2.* doesn't support 88 | # sha2 for hmac, so this will have to be set true in this case. 89 | # 90 | 'MACs' => $macs, 91 | 92 | # Alternative setting, if OpenSSH version is below v5.9 93 | #MACs hmac-ripemd160 94 | 95 | # **Key Exchange Algorithms** -- Make sure not to use SHA1 for kex, 96 | # unless it is really necessary 97 | # Weak kex is sometimes required if older package versions are used 98 | # eg ruby's Net::SSH at around 2.2.* doesn't support sha2 for kex, 99 | # so this will have to be set true in this case. 100 | # 101 | 'KexAlgorithms' => $kex, 102 | 103 | # Disable agent formwarding, since local agent could be accessed 104 | # through forwarded connection. 105 | 'ForwardAgent' => 'no', 106 | 107 | # Disable X11 forwarding, since local X11 display could be accessed 108 | # through forwarded connection. 109 | 'ForwardX11' => 'no', 110 | 111 | # Never use host-based authentication. It can be exploited. 112 | 'HostbasedAuthentication' => 'no', 113 | 'RhostsRSAAuthentication' => 'no', 114 | 115 | # Enable RSA authentication via identity files. 116 | 'RSAAuthentication' => 'yes', 117 | 118 | # Disable password-based authentication, it can allow for potentially 119 | # easier brute-force attacks. 120 | 'PasswordAuthentication' => 'no', 121 | 122 | # Only use GSSAPIAuthentication if implemented on the network. 123 | 'GSSAPIAuthentication' => 'no', 124 | 'GSSAPIDelegateCredentials' => 'no', 125 | 126 | # Disable tunneling 127 | 'Tunnel' => 'no', 128 | 129 | # Disable local command execution. 130 | 'PermitLocalCommand' => 'no', 131 | 132 | # Misc. configuration 133 | # =================== 134 | 135 | # Enable compression. More pressure on the CPU, less on the network. 136 | 'Compression' => 'yes', 137 | 138 | #EscapeChar ~ 139 | #VisualHostKey yes 140 | } 141 | 142 | $merged_options = merge($ssh_options, $options) 143 | 144 | class { 'ssh::client': 145 | storeconfigs_enabled => false, 146 | options => delete_undef_values($merged_options), 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Puppet SSH hardening 2 | 3 | [![Puppet Forge](https://img.shields.io/puppetforge/dt/hardening/ssh_hardening.svg)][1] 4 | [![Build Status](http://img.shields.io/travis/hardening-io/puppet-ssh-hardening.svg)][2] 5 | [![Gitter Chat](https://badges.gitter.im/Join%20Chat.svg)][3] 6 | 7 | ## Description 8 | 9 | This Puppet module provides secure ssh-client and ssh-server configurations. 10 | 11 | ## Requirements 12 | 13 | * Puppet 14 | * Puppet modules: `saz/ssh` (>= 2.3.6), `puppetlabs/stdlib` (>= 4.2.0) 15 | 16 | 17 | ## Parameters 18 | 19 | * `ipv6_enabled = false` - true if IPv6 is needed 20 | * `cbc_required = false` - true if CBC for ciphers is required. This is usually only necessary, if older M2M mechanism need to communicate with SSH, that don't have any of the configured secure ciphers enabled. CBC is a weak alternative. Anything weaker should be avoided and is thus not available. 21 | * `weak_hmac = false` - true if weaker HMAC mechanisms are required. This is usually only necessary, if older M2M mechanism need to communicate with SSH, that don't have any of the configured secure HMACs enabled. 22 | * `weak_kex = false` - true if weaker Key-Exchange (KEX) mechanisms are required. This is usually only necessary, if older M2M mechanism need to communicate with SSH, that don't have any of the configured secure KEXs enabled. 23 | * `allow_root_with_key = false` - `false` to disable root login altogether. Set to `true` to allow root to login via key-based mechanism. 24 | * `ports = [ 22 ]` - ports to which ssh-server should listen to and ssh-client should connect to 25 | * `listen_to = [ "0.0.0.0" ]` - one or more ip addresses, to which ssh-server should listen to. Default is empty, but should be configured for security reasons! 26 | * `remote_hosts` - one or more hosts, to which ssh-client can connect to. Default is empty, but should be configured for security reasons! 27 | * `allow_tcp_forwarding = false` - set to true to allow TCP forwarding 28 | * `allow_agent_forwarding = false` - set to true to allow Agent forwarding 29 | * `use_pam = false` to disable pam authentication 30 | * `client_options = {}` - set values in the hash to override the module's settings 31 | * `server_options = {}` - set values in the hash to override the module's settings 32 | 33 | ## Usage 34 | 35 | After adding this module, you can use the class: 36 | 37 | class { 'ssh_hardening': } 38 | 39 | This will install ssh-server and ssh-client. You can alternatively choose only one via: 40 | 41 | class { 'ssh_hardening::server': } 42 | class { 'ssh_hardening::client': } 43 | 44 | You should configure core attributes: 45 | 46 | class { 'ssh_hardening::server': 47 | "listen_to" : ["10.2.3.4"] 48 | } 49 | 50 | **The default value for `listen_to` is `0.0.0.0`. It is highly recommended to change the value.** 51 | 52 | ### Overwriting default options 53 | Default options will be merged with options passed in by the `client_options` and `server_options` parameters. 54 | If an option is set both as default and via options parameter, the latter will win. 55 | 56 | The following example will enable X11Forwarding, which is disabled by default: 57 | 58 | ```puppet 59 | class { 'ssh_hardening': 60 | server_options => { 61 | 'X11Forwarding' => 'yes', 62 | }, 63 | } 64 | ``` 65 | 66 | ## Local Testing 67 | 68 | For local testing you can use vagrant and Virtualbox of VMWare to run tests locally. You will have to install Virtualbox and Vagrant on your system. See [Vagrant Downloads](http://downloads.vagrantup.com/) for a vagrant package suitable for your system. For all our tests we use `test-kitchen`. If you are not familiar with `test-kitchen` please have a look at [their guide](http://kitchen.ci/docs/getting-started). 69 | 70 | Next install test-kitchen: 71 | 72 | ```bash 73 | # Install dependencies 74 | gem install bundler 75 | bundle install 76 | 77 | # Fetch tests 78 | bundle exec thor kitchen:fetch-remote-tests 79 | 80 | # Do lint checks 81 | bundle exec rake lint 82 | 83 | # Do spec checks 84 | bundle exec rake spec 85 | 86 | # fast test on one machine 87 | bundle exec kitchen test default-ubuntu-1204 88 | 89 | # test on Debian-based machines 90 | bundle exec kitchen test 91 | 92 | # for development 93 | bundle exec kitchen create default-ubuntu-1204 94 | bundle exec kitchen converge default-ubuntu-1204 95 | ``` 96 | 97 | For more information see [test-kitchen](http://kitchen.ci/docs/getting-started) 98 | 99 | ## FAQ / Pitfalls 100 | 101 | **I can't log into my account. I have registered the client key, but it still doesn't let me it.** 102 | 103 | If you have exhausted all typical issues (firewall, network, key missing, wrong key, account disabled etc.), it may be that your account is locked. The quickest way to find out is to look at the password hash for your user: 104 | 105 | sudo grep myuser /etc/shadow 106 | 107 | If the hash includes an `!`, your account is locked: 108 | 109 | myuser:!:16280:7:60:7::: 110 | 111 | The proper way to solve this is to unlock the account (`passwd -u myuser`). If the user doesn't have a password, you should can unlock it via: 112 | 113 | usermod -p "*" myuser 114 | 115 | Alternatively, if you intend to use PAM, you enabled it via `use_pam = true`. PAM will allow locked users to get in with keys. 116 | 117 | 118 | **Why doesn't my application connect via SSH anymore?** 119 | 120 | Always look into log files first and if possible look at the negotiation between client and server that is completed when connecting. 121 | 122 | We have seen some issues in applications (based on python and ruby) that are due to their use of an outdated crypto set. This collides with this hardening module, which reduced the list of ciphers, message authentication codes (MACs) and key exchange (KEX) algorithms to a more secure selection. 123 | 124 | If you find this isn't enough, feel free to activate `cbc_required` for ciphers, `weak_hmac` for MACs, and `weak_kex` for KEX. 125 | 126 | ## Contributors + Kudos 127 | 128 | * Dominik Richter [arlimus](https://github.com/arlimus) 129 | * Edmund Haselwanter [ehaselwanter](https://github.com/ehaselwanter) 130 | * Christoph Hartmann [chris-rock](https://github.com/chris-rock) 131 | * Patrick Meier [atomic111](https://github.com/atomic111) 132 | * Matthew Haughton [3flex](https://github.com/3flex) 133 | * Bernhard Schmidt [bernhardschmidt](https://github.com/bernhardschmidt) 134 | * Kurt Huwig [kurthuwig](https://github.com/kurthuwig) 135 | * Artem Sidorenko [artem-sidorenko](https://github.com/artem-sidorenko) 136 | * Guillaume Destuynder [gdestuynder](https://github.com/gdestuynder) 137 | * Bernhard Weisshuhn [bkw](https://github.com/bkw) 138 | * [stribika](https://github.com/stribika) 139 | 140 | ## License and Author 141 | 142 | * Author:: Dominik Richter 143 | * Author:: Deutsche Telekom AG 144 | 145 | Licensed under the Apache License, Version 2.0 (the "License"); 146 | you may not use this file except in compliance with the License. 147 | You may obtain a copy of the License at 148 | 149 | http://www.apache.org/licenses/LICENSE-2.0 150 | 151 | Unless required by applicable law or agreed to in writing, software 152 | distributed under the License is distributed on an "AS IS" BASIS, 153 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 154 | See the License for the specific language governing permissions and 155 | limitations under the License. 156 | 157 | [1]: https://forge.puppetlabs.com/hardening/ssh_hardening 158 | [2]: http://travis-ci.org/hardening-io/puppet-ssh-hardening 159 | [3]: https://gitter.im/hardening-io/general 160 | -------------------------------------------------------------------------------- /manifests/server.pp: -------------------------------------------------------------------------------- 1 | # === Copyright 2 | # 3 | # Copyright 2014, Deutsche Telekom AG 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | 8 | # == Class: ssh_hardening::server 9 | # 10 | # The default SSH class which installs the SSH server 11 | # 12 | # === Parameters 13 | # 14 | # [*cbc_required*] 15 | # CBC-meachnisms are considered weaker and will not be used as ciphers by 16 | # default. Set this option to true if you really need CBC-based ciphers. 17 | # 18 | # [*weak_hmac*] 19 | # The HMAC-mechanisms are selected to be cryptographically strong. If you 20 | # require some weaker variants, set this option to true to get safe selection. 21 | # 22 | # [*weak_kex*] 23 | # The KEX-mechanisms are selected to be cryptographically strong. If you 24 | # require some weaker variants, set this option to true to get safe selection. 25 | # 26 | # [*ports*] 27 | # A list of ports that SSH expects to run on. Defaults to 22. 28 | # 29 | # [*listen_to*] 30 | # A list of addresses which SSH listens on. Best to specify only on address of 31 | # one interface. 32 | # 33 | # [*host_key_files*] 34 | # A list of host key files to use. 35 | # 36 | # [*client_alive_interval*] 37 | # Interval after which the server checks if the client is alive (in seconds). 38 | # 39 | # [*client_alive_count*] 40 | # The maximum number of failed client alive checks before the client is 41 | # forcefully disconnected. 42 | # 43 | # [*allow_root_with_key*] 44 | # Whether to allow login of root. If true, root may log in using the key files 45 | # specified in authroized_keys. Otherwise any login attempts as user root 46 | # are forbidden. 47 | # 48 | # [*ipv6_enabled*] 49 | # Set to true if you need IPv6 support in SSH. 50 | # 51 | # [*options*] 52 | # Allow override of default settings provided by the module. 53 | # 54 | class ssh_hardening::server ( 55 | $cbc_required = false, 56 | $weak_hmac = false, 57 | $weak_kex = false, 58 | $ports = [ 22 ], 59 | $listen_to = [], 60 | $host_key_files = [], 61 | $client_alive_interval = 600, 62 | $client_alive_count = 3, 63 | $allow_root_with_key = false, 64 | $ipv6_enabled = false, 65 | $use_pam = false, 66 | $allow_tcp_forwarding = false, 67 | $allow_agent_forwarding = false, 68 | $max_auth_retries = 2, 69 | $options = {}, 70 | ) { 71 | 72 | $addressfamily = $ipv6_enabled ? { 73 | true => 'any', 74 | false => 'inet', 75 | } 76 | 77 | $ciphers = get_ssh_ciphers($::operatingsystem, $::operatingsystemrelease, $cbc_required) 78 | $macs = get_ssh_macs($::operatingsystem, $::operatingsystemrelease, $weak_hmac) 79 | $kex = get_ssh_kex($::operatingsystem, $::operatingsystemrelease, $weak_kex) 80 | $priv_sep = use_privilege_separation($::operatingsystem, $::operatingsystemrelease) 81 | 82 | $permit_root_login = $allow_root_with_key ? { 83 | true => 'without-password', 84 | false => 'no', 85 | } 86 | 87 | $use_pam_option = $use_pam ? { 88 | true => 'yes', 89 | false => 'no', 90 | } 91 | 92 | $tcp_forwarding = $allow_tcp_forwarding ? { 93 | true => 'yes', 94 | false => 'no' 95 | } 96 | 97 | $agent_forwarding = $allow_agent_forwarding ? { 98 | true => 'yes', 99 | false => 'no' 100 | } 101 | 102 | $default_hardened_options = { 103 | # Basic configuration 104 | # =================== 105 | 106 | # Either disable or only allow root login via certificates. 107 | 'PermitRootLogin' => $permit_root_login, 108 | 109 | # Define which port sshd should listen to. Default to `22`. 110 | 'Port' => $ports, 111 | 112 | # Address family should always be limited to the active 113 | # network configuration. 114 | 'AddressFamily' => $addressfamily, 115 | 116 | # Define which addresses sshd should listen to. 117 | # Default to `0.0.0.0`, ie make sure you put your desired address 118 | # in here, since otherwise sshd will listen to everyone. 119 | 'ListenAddress' => $listen_to, 120 | 121 | # Define the HostKey 122 | 'HostKey' => $host_key_files, 123 | 124 | # Security configuration 125 | # ====================== 126 | 127 | # Set the protocol family to 2 for security reasons. 128 | # Disables legacy support. 129 | 'Protocol' => 2, 130 | 131 | # Make sure sshd checks file modes and ownership before accepting logins. 132 | # This prevents accidental misconfiguration. 133 | 'StrictModes' => 'yes', 134 | 135 | # Logging, obsoletes QuietMode and FascistLogging 136 | 'SyslogFacility' => 'AUTH', 137 | 'LogLevel' => 'VERBOSE', 138 | 139 | # Cryptography 140 | # ------------ 141 | 142 | # **Ciphers** -- If your clients don't support CTR (eg older versions), 143 | # cbc will be added 144 | # CBC: is true if you want to connect with OpenSSL-base libraries 145 | # eg Ruby's older Net::SSH::Transport::CipherFactory requires CBC-versions 146 | # of the given openssh ciphers to work 147 | # 148 | 'Ciphers' => $ciphers, 149 | 150 | # **Hash algorithms** -- Make sure not to use SHA1 for hashing, 151 | # unless it is really necessary. 152 | # Weak HMAC is sometimes required if older package versions are used 153 | # eg Ruby's Net::SSH at around 2.2.* doesn't support sha2 for hmac, 154 | # so this will have to be set true in this case. 155 | # 156 | 'MACs' => $macs, 157 | 158 | # Alternative setting, if OpenSSH version is below v5.9 159 | #MACs hmac-ripemd160 160 | 161 | # **Key Exchange Algorithms** -- Make sure not to use SHA1 for kex, 162 | # unless it is really necessary 163 | # Weak kex is sometimes required if older package versions are used 164 | # eg ruby's Net::SSH at around 2.2.* doesn't support sha2 for kex, 165 | # so this will have to be set true in this case. 166 | # 167 | 'KexAlgorithms' => $kex, 168 | 169 | # Authentication 170 | # -------------- 171 | 172 | # Secure Login directives. 173 | 'UseLogin' => 'no', 174 | 'UsePrivilegeSeparation' => $priv_sep, 175 | 'PermitUserEnvironment' => 'no', 176 | 'LoginGraceTime' => '30s', 177 | 'MaxAuthTries' => $max_auth_retries, 178 | 'MaxSessions' => 10, 179 | 'MaxStartups' => '10:30:100', 180 | 181 | # Enable public key authentication 182 | 'PubkeyAuthentication' => 'yes', 183 | 184 | # Never use host-based authentication. It can be exploited. 185 | 'IgnoreRhosts' => 'yes', 186 | 'IgnoreUserKnownHosts' => 'yes', 187 | 'HostbasedAuthentication' => 'no', 188 | 189 | # Disable password-based authentication, it can allow for 190 | # potentially easier brute-force attacks. 191 | 'UsePAM' => $use_pam_option, 192 | 'PasswordAuthentication' => 'no', 193 | 'PermitEmptyPasswords' => 'no', 194 | 'ChallengeResponseAuthentication' => 'no', 195 | 196 | # Only enable Kerberos authentication if it is configured. 197 | 'KerberosAuthentication' => 'no', 198 | 'KerberosOrLocalPasswd' => 'no', 199 | 'KerberosTicketCleanup' => 'yes', 200 | #KerberosGetAFSToken no 201 | 202 | # Only enable GSSAPI authentication if it is configured. 203 | 'GSSAPIAuthentication' => 'no', 204 | 'GSSAPICleanupCredentials' => 'yes', 205 | 206 | # In case you don't use PAM (`UsePAM no`), you can alternatively 207 | # restrict users and groups here. For key-based authentication 208 | # this is not necessary, since all keys must be explicitly enabled. 209 | #DenyUsers * 210 | #AllowUsers user1 211 | #DenyGroups * 212 | #AllowGroups group1 213 | 214 | 215 | # Network 216 | # ------- 217 | 218 | # Disable TCP keep alive since it is spoofable. Use ClientAlive 219 | # messages instead, they use the encrypted channel 220 | 'TCPKeepAlive' => 'no', 221 | 222 | # Manage `ClientAlive..` signals via interval and maximum count. 223 | # This will periodically check up to a `..CountMax` number of times 224 | # within `..Interval` timeframe, and abort the connection once these fail. 225 | 'ClientAliveInterval' => $client_alive_interval, 226 | 'ClientAliveCountMax' => $client_alive_count, 227 | 228 | # Disable tunneling 229 | 'PermitTunnel' => 'no', 230 | 231 | # Disable forwarding tcp connections. 232 | # no real advantage without denied shell access 233 | 'AllowTcpForwarding' => $tcp_forwarding, 234 | 235 | # Disable agent formwarding, since local agent could be accessed through 236 | # forwarded connection. No real advantage without denied shell access 237 | 'AllowAgentForwarding' => $agent_forwarding, 238 | 239 | # Do not allow remote port forwardings to bind to non-loopback addresses. 240 | 'GatewayPorts' => 'no', 241 | 242 | # Disable X11 forwarding, since local X11 display could be 243 | # accessed through forwarded connection. 244 | 'X11Forwarding' => 'no', 245 | 'X11UseLocalhost' => 'yes', 246 | 247 | # Misc. configuration 248 | # =================== 249 | 250 | 'PrintMotd' => 'no', 251 | 'PrintLastLog' => 'no', 252 | #Banner /etc/ssh/banner.txt 253 | #UseDNS yes 254 | #PidFile /var/run/sshd.pid 255 | #MaxStartups 10 256 | #ChrootDirectory none 257 | #ChrootDirectory /home/%u 258 | 259 | # Configuration, in case SFTP is used 260 | ## override default of no subsystems 261 | ## Subsystem sftp /opt/app/openssh5/libexec/sftp-server 262 | #Subsystem sftp internal-sftp -l VERBOSE 263 | # 264 | ## These lines must appear at the *end* of sshd_config 265 | #Match Group sftponly 266 | #ForceCommand internal-sftp -l VERBOSE 267 | #ChrootDirectory /sftpchroot/home/%u 268 | #AllowTcpForwarding no 269 | #AllowAgentForwarding no 270 | #PasswordAuthentication no 271 | #PermitRootLogin no 272 | #X11Forwarding no 273 | } 274 | 275 | $merged_options = merge($default_hardened_options, $options) 276 | 277 | class { 'ssh::server': 278 | storeconfigs_enabled => false, 279 | options => $merged_options, 280 | } 281 | 282 | file {'/etc/ssh': 283 | ensure => 'directory', 284 | mode => '0755', 285 | owner => 'root', 286 | group => 'root' 287 | } 288 | 289 | } 290 | --------------------------------------------------------------------------------