Skip to content

Commit 29302d7

Browse files
authored
Merge pull request #163 from artem-sidorenko/dh-moduli
Avoid small primes for DH and allow rebuild of DH primes
2 parents 94c08e7 + 74d0c6f commit 29302d7

File tree

5 files changed

+103
-11
lines changed

5 files changed

+103
-11
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ override['ssh-hardening']['ssh']['server']['listen_to'] = node['ipaddress']
4646
* `['ssh-hardening']['ssh']['client']['remote_hosts']` - `[]` - one or more hosts, to which ssh-client can connect to.
4747
* `['ssh-hardening']['ssh']['client']['password_authentication']` - `false`. Set to `true` if password authentication should be enabled.
4848
* `['ssh-hardening']['ssh']['client']['roaming']` - `false`. Set to `true` if experimental client roaming should be enabled. This is known to cause potential issues with secrets being disclosed to malicious servers and defaults to being disabled.
49+
* `['ssh-hardening']['ssh']['server']['dh_min_prime_size']` - `2048` - Minimal acceptable prime length in bits in `/etc/ssh/moduli`. Primes below this number will get removed. (See [this](https://entropux.net/article/openssh-moduli/) for more information and background)
50+
* `['ssh-hardening']['ssh']['server']['dh_build_primes']` - `false` - If own primes should be built. This rebuild happens only once and takes a lot of time (~ 1.5 - 2h on the modern hardware for 4096 length).
51+
* `['ssh-hardening']['ssh']['server']['dh_build_primes_size']` - `4096` - Prime length which should be generated. This option is only valid if `dh_build_primes` is enabled.
4952
* `['ssh-hardening']['ssh']['server']['listen_to']` `#override attribute#` - one or more ip addresses, to which ssh-server should listen to. Default is to listen on all interfaces. It should be configured for security reasons!
5053
* `['ssh-hardening']['ssh']['server']['allow_root_with_key']` - `false` to disable root login altogether. Set to `true` to allow root to login via key-based mechanism
5154
* `['ssh-hardening']['ssh']['server']['allow_tcp_forwarding']` - `false`. Set to `true` to allow TCP Forwarding

attributes/default.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@
7070
default['ssh-hardening']['ssh']['server']['cbc_required'] = false
7171
default['ssh-hardening']['ssh']['server']['weak_hmac'] = false
7272
default['ssh-hardening']['ssh']['server']['weak_kex'] = false
73+
default['ssh-hardening']['ssh']['server']['dh_min_prime_size'] = 2048
74+
default['ssh-hardening']['ssh']['server']['dh_build_primes'] = false
75+
default['ssh-hardening']['ssh']['server']['dh_build_primes_size'] = 4096
7376
default['ssh-hardening']['ssh']['server']['host_key_files'] = ['/etc/ssh/ssh_host_rsa_key', '/etc/ssh/ssh_host_ecdsa_key']
7477
default['ssh-hardening']['ssh']['server']['client_alive_interval'] = 600 # 10min
7578
default['ssh-hardening']['ssh']['server']['client_alive_count'] = 3 # ~> 3 x interval

recipes/server.rb

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,24 @@
3030
['0.0.0.0']
3131
end
3232

33+
# some internal definitions
34+
cache_dir = ::File.join(Chef::Config[:file_cache_path], cookbook_name.to_s)
35+
dh_moduli_file = '/etc/ssh/moduli'
36+
37+
# create a cache dir for this cookbook
38+
# we use it for storing of lock files or selinux files
39+
directory cache_dir
40+
3341
# installs package name
3442
package 'openssh-server' do
3543
package_name node['ssh-hardening']['sshserver']['package']
3644
end
3745

3846
# Handle addional SELinux policy on RHEL/Fedora for different UsePAM options
3947
if %w(fedora rhel).include?(node['platform_family'])
40-
policy_dir = ::File.join(Chef::Config[:file_cache_path], cookbook_name.to_s)
41-
policy_file = ::File.join(policy_dir, 'ssh_password.te')
42-
module_file = ::File.join(policy_dir, 'ssh_password.mod')
43-
package_file = ::File.join(policy_dir, 'ssh_password.pp')
48+
policy_file = ::File.join(cache_dir, 'ssh_password.te')
49+
module_file = ::File.join(cache_dir, 'ssh_password.mod')
50+
package_file = ::File.join(cache_dir, 'ssh_password.pp')
4451

4552
package 'policycoreutils-python'
4653
# on fedora we need an addtional package for semodule_package
@@ -56,8 +63,6 @@
5663
else
5764
# UsePAM no: enable and install the additional SELinux policy
5865

59-
directory policy_dir
60-
6166
cookbook_file policy_file do
6267
source 'ssh_password.te'
6368
end
@@ -73,6 +78,53 @@
7378
end
7479
end
7580

81+
# handle Diffie-Hellman moduli
82+
# build own moduli file if required
83+
own_primes_lock_file = ::File.join(cache_dir, 'moduli.lock')
84+
bash 'build own primes for DH' do
85+
code <<-EOS
86+
set -e
87+
tempdir=$(mktemp -d)
88+
ssh-keygen -G $tempdir/moduli.all -b #{node['ssh-hardening']['ssh']['server']['dh_build_primes_size']}
89+
ssh-keygen -T $tempdir/moduli.safe -f $tempdir/moduli.all
90+
cp $tempdir/moduli.safe #{dh_moduli_file}
91+
rm -rf $tempdir
92+
touch #{own_primes_lock_file}
93+
EOS
94+
only_if { node['ssh-hardening']['ssh']['server']['dh_build_primes'] }
95+
not_if { ::File.exist?(own_primes_lock_file) }
96+
notifies :restart, 'service[sshd]'
97+
end
98+
99+
# remove all small primes
100+
# https://stribika.github.io/2015/01/04/secure-secure-shell.html
101+
dh_min_prime_size = node['ssh-hardening']['ssh']['server']['dh_min_prime_size'].to_i - 1 # 4096 is 4095 in the moduli file
102+
ruby_block 'remove small primes from DH moduli' do # ~FC014
103+
block do
104+
tmp_file = "#{dh_moduli_file}.tmp"
105+
::File.open(tmp_file, 'w') do |new_file|
106+
::File.readlines(dh_moduli_file).each do |line|
107+
unless line_match = line.match(/^(\d+ ){4}(\d+) \d+ \h+$/) # rubocop:disable Lint/AssignmentInCondition
108+
# some line without expected data structure, e.g. comment line
109+
# write it and go to the next data
110+
new_file.write(line)
111+
next
112+
end
113+
114+
# lets compare the bits and do not write the lines with small bit size
115+
bits = line_match[2]
116+
new_file.write(line) unless bits.to_i < dh_min_prime_size
117+
end
118+
end
119+
120+
# we use cp&rm to preserve the permissions of existing file
121+
FileUtils.cp(tmp_file, dh_moduli_file)
122+
FileUtils.rm(tmp_file)
123+
end
124+
not_if "test $(awk '$5 < #{dh_min_prime_size} && $5 ~ /^[0-9]+$/ { print $5 }' #{dh_moduli_file} | uniq | wc -c) -eq 0"
125+
notifies :restart, 'service[sshd]'
126+
end
127+
76128
# defines the sshd service
77129
service 'sshd' do
78130
# use upstart for ubuntu, otherwise chef uses init

spec/recipes/default_spec.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
ChefSpec::ServerRunner.new.converge(described_recipe)
2424
end
2525

26+
before do
27+
stub_command("test $(awk '$5 < 2047 && $5 ~ /^[0-9]+$/ { print $5 }' /etc/ssh/moduli | uniq | wc -c) -eq 0").and_return(true)
28+
end
29+
2630
# check that the recipes are executed
2731
it 'includes server recipe' do
2832
expect(chef_run).to include_recipe('ssh-hardening::server')

spec/recipes/server_spec.rb

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,21 @@
2121
describe 'ssh-hardening::server' do
2222
let(:helper_lib) { DevSec::Ssh }
2323
let(:ssh_config_file) { '/etc/ssh/sshd_config' }
24+
let(:dh_primes_ok) { true }
2425

2526
# converge
2627
cached(:chef_run) do
2728
ChefSpec::ServerRunner.new.converge(described_recipe)
2829
end
2930

31+
before do
32+
stub_command("test $(awk '$5 < 2047 && $5 ~ /^[0-9]+$/ { print $5 }' /etc/ssh/moduli | uniq | wc -c) -eq 0").and_return(dh_primes_ok)
33+
end
34+
35+
it 'should create cache directory' do
36+
expect(chef_run).to create_directory('/tmp/ssh-hardening-file-cache/ssh-hardening')
37+
end
38+
3039
it 'installs openssh-server' do
3140
expect(chef_run).to install_package('openssh-server')
3241
end
@@ -229,7 +238,6 @@
229238
let(:version) { '16.04' }
230239

231240
it 'does not invoke any SELinux resources' do
232-
expect(chef_run).not_to create_directory('/tmp/ssh-hardening-file-cache/ssh-hardening')
233241
expect(chef_run).not_to render_file('/tmp/ssh-hardening-file-cache/ssh-hardening/ssh_password.te')
234242
expect(chef_run).not_to run_execute('remove selinux policy')
235243
expect(chef_run).not_to run_bash('build selinux package and install it')
@@ -300,10 +308,6 @@
300308
expect(chef_run).to render_file('/etc/ssh/sshd_config').with_content('UsePAM no')
301309
end
302310

303-
it 'should create cache directory for policy files' do
304-
expect(chef_run).to create_directory('/tmp/ssh-hardening-file-cache/ssh-hardening')
305-
end
306-
307311
it 'should create selinux source policy file' do
308312
expect(chef_run).to render_file('/tmp/ssh-hardening-file-cache/ssh-hardening/ssh_password.te')
309313
end
@@ -327,6 +331,32 @@
327331
end
328332
end
329333

334+
it 'should not build own DH primes per default' do
335+
expect(chef_run).not_to run_bash('build own primes for DH')
336+
end
337+
338+
describe 'DH primes handling' do
339+
let(:chef_run) do
340+
ChefSpec::ServerRunner.new.converge(described_recipe)
341+
end
342+
343+
context 'when there are no small primes' do
344+
let(:dh_primes_ok) { true }
345+
346+
it 'should not remove small primes from DH moduli' do
347+
expect(chef_run).not_to run_ruby_block('remove small primes from DH moduli')
348+
end
349+
end
350+
351+
context 'when there are small primes present' do
352+
let(:dh_primes_ok) { false }
353+
354+
it 'should invoke small primes from DH module' do
355+
expect(chef_run).to run_ruby_block('remove small primes from DH moduli')
356+
end
357+
end
358+
end
359+
330360
describe 'debian banner' do
331361
cached(:chef_run) do
332362
ChefSpec::ServerRunner.new(platform: 'ubuntu', version: '16.04').converge(described_recipe)

0 commit comments

Comments
 (0)