-
Notifications
You must be signed in to change notification settings - Fork 70
/
Copy pathdevsec_ssh.rb
240 lines (217 loc) · 8.59 KB
/
devsec_ssh.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
#
# Cookbook:: ssh-hardening
# Library:: devsec_ssh
#
# Copyright:: 2012, Dominik Richter
# Copyright:: 2014, Christoph Hartmann
# Copyright:: 2014, Deutsche Telekom AG
# Copyright:: 2016, Artem Sidorenko
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
module DevSec
# Class methods of this class can be called in order to
# return different cryptographic configuration data. The calls
# can be done based on the CRYPTO constant below. There is autodetection
# of required ssh version. If ssh package can not be found, preconfigured OS
# matrix is used.
#
# Syntax:
#
# get_[client|server]_[kexs|macs|ciphers] (enable-weak-ciphers)
#
# Example:
#
# DevSec::Ssh.get_server_macs(false)
# => 'hmac-ripemd160 hmac-sha1' # for openssh server version 5.3
#
class Ssh # rubocop:disable Metrics/ClassLength
# Fallback ssh version for autodetection
FALLBACK_SSH_VERSION ||= 5.9
# Support types of ssh
SSH_TYPES ||= %i(client server).freeze
# Crypto configuration for different ssh parameters
CRYPTO ||= {
kexs: {
5.3 => [],
5.9 => %w(diffie-hellman-group-exchange-sha256),
:weak => %w(diffie-hellman-group14-sha1 diffie-hellman-group-exchange-sha1 diffie-hellman-group1-sha1),
},
macs: {
5.3 => %w(hmac-ripemd160 hmac-sha1),
5.9 => %w(hmac-sha2-512 hmac-sha2-256 hmac-ripemd160),
:weak => %w(hmac-sha1),
},
ciphers: {
5.3 => %w(aes256-ctr aes192-ctr aes128-ctr),
aes256-ctr aes192-ctr aes128-ctr),
:weak => %w(aes256-cbc aes192-cbc aes128-cbc),
},
}.freeze
# Privilege separation values
PRIVILEGE_SEPARATION ||= {
5.3 => 'yes',
5.9 => 'sandbox',
}.freeze
# Hostkey algorithms
# In the current implementation they are server specific so we need own data hash for it
HOSTKEY_ALGORITHMS ||= {
5.3 => %w(rsa),
6.0 => %w(rsa ecdsa),
6.6 => %w(rsa ecdsa ed25519),
}.freeze
class << self
def get_server_privilege_separarion # rubocop:disable Style/AccessorMethodName
Chef::Log.debug('Called get_server_privilege_separarion')
found_ssh_version = find_ssh_version(get_ssh_server_version, PRIVILEGE_SEPARATION.keys)
ret = PRIVILEGE_SEPARATION[found_ssh_version]
Chef::Log.debug("Using configuration for ssh version #{found_ssh_version}, value: #{ret}")
ret
end
def get_server_algorithms # rubocop:disable Style/AccessorMethodName
Chef::Log.debug('Called get_server_algorithms')
found_ssh_version = find_ssh_version(get_ssh_server_version, HOSTKEY_ALGORITHMS.keys)
ret = HOSTKEY_ALGORITHMS[found_ssh_version]
Chef::Log.debug("Using configuration for ssh version #{found_ssh_version}, value: #{ret}")
ret
end
def get_client_macs(enable_weak = false)
get_crypto_data(:macs, :client, enable_weak)
end
def get_server_macs(enable_weak = false)
get_crypto_data(:macs, :server, enable_weak)
end
def get_client_ciphers(enable_weak = false)
get_crypto_data(:ciphers, :client, enable_weak)
end
def get_server_ciphers(enable_weak = false)
get_crypto_data(:ciphers, :server, enable_weak)
end
def get_client_kexs(enable_weak = false)
get_crypto_data(:kexs, :client, enable_weak)
end
def get_server_kexs(enable_weak = false)
get_crypto_data(:kexs, :server, enable_weak)
end
{ client: 'sshclient',
server: 'sshserver' }.each do |k, v|
define_method("get_ssh_#{k}_version") do
get_ssh_version(node['ssh-hardening'][v]['package'])
end
end
# Verify values of permit_tunnel
def validate_permit_tunnel(value)
case value
when true
'yes'
when false
'no'
when 'yes', 'no', 'point-to-point', 'ethernet'
value
else
raise "Incorrect value for attribute node['ssh-hardening']['ssh']['server']['permit_tunnel']: must be boolean or a string as defined in the sshd_config man pages, you passed \"#{value}\""
end
end
private
# :nocov:
def node
Chef.node
end
# :nocov:
# Return the crypto data for given crypto type and ssh type
# crypto_type is a subkey of CRYPTO constant, e.g. :kex, :macs, :ciphers
# ssh_type is :client or :server
def get_crypto_data(crypto_type, ssh_type, enable_weak)
Chef::Log.debug("Called get_crypto_data for crypto_type #{crypto_type} and ssh_type #{ssh_type}")
ssh_version = send("get_ssh_#{ssh_type}_version")
Chef::Log.debug("Detected ssh version #{ssh_version}")
found_ssh_version = find_ssh_version(ssh_version, CRYPTO[crypto_type].keys)
Chef::Log.debug("Using configuration for ssh version #{found_ssh_version}")
crypto = []
crypto += CRYPTO[crypto_type][found_ssh_version]
# Sometimes we do not have a value, e.g. this feature is not supported
# on the particilar ssh version. Return nil in such cases
if crypto.empty?
Chef::Log.debug("No value present for ssh version #{found_ssh_version}. Returning nil.")
return
end
if enable_weak
weak = CRYPTO[crypto_type][:weak]
Chef::Log.info("Enabling weak #{crypto_type}: #{weak}")
crypto += weak
end
Chef::Log.debug("get_crypto_data result for crypto_type #{crypto_type} and ssh_type #{ssh_type}: #{crypto}")
crypto.uniq.join(',')
end
# Find the ssh version, matching to the next small
# version in versions array
def find_ssh_version(version, versions)
found_ssh_version = nil
versions.map do |v|
next unless v.is_a?(Float) # skip everything what does not look like a version
found_ssh_version = v if version >= v
end
raise "Unsupported ssh version #{version}" unless found_ssh_version
found_ssh_version
end
def get_ssh_version(package)
version = node['packages'][package]['version']
# on debian we get the epoch in front of version number: 1:7.2p2-4ubuntu2.1
version = version.split(':')[1] if platform_family?('debian')
Chef::Log.debug("Detected openssh version #{version} for package #{package}")
version.to_f
rescue NoMethodError
# Detection via installed package failed, lets guess the installed version
version = guess_ssh_version
Chef::Log.debug("Failed to get the openssh version from installed package. Guessed version is #{version}")
version
end
# Guess the version of ssh via OS matrix
def guess_ssh_version # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
family = node['platform_family']
platform = node['platform']
version = node['platform_version'].to_f
case family
when 'debian'
case platform
when 'ubuntu'
return 6.6 if version >= 14.04
when 'debian'
return 6.6 if version >= 8
return 6.0 if version >= 7
return 5.3 if version <= 6
end
when 'rhel'
return 6.6 if version >= 7
return 5.3 if version >= 6
when 'fedora'
return 7.3 if version >= 25
return 7.2 if version >= 24
when 'suse'
case platform
when 'opensuse'
return 6.6 if version >= 13.2
when 'opensuseleap'
return 7.2 if version >= 42.1
end
end
Chef::Log.info("Unknown platform #{node['platform']} with version #{node['platform_version']} and family #{node['platform_family']}. Assuming ssh version #{FALLBACK_SSH_VERSION}")
FALLBACK_SSH_VERSION
end
end
end
end