Skip to content

Commit 998b774

Browse files
JakujeRezney
authored andcommitted
OpenSSHConfigScanner: Include directive is supported since RHEL 8.6
This issue could cause false positive reports when the user has the configuration options such as "Subsystem sftp" defined in included file only. Resolves: RHEL-33902 Signed-off-by: Jakub Jelen <[email protected]> Co-Authored-By: Michal Hecko <[email protected]> do not use filesystem during tests
1 parent caff3ac commit 998b774

File tree

2 files changed

+229
-12
lines changed

2 files changed

+229
-12
lines changed

repos/system_upgrade/common/actors/opensshconfigscanner/libraries/readopensshconfig.py

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import errno
2+
import glob
3+
import os
4+
import shlex
25

6+
from leapp.exceptions import StopActorExecutionError
37
from leapp.libraries.common.rpms import check_file_modification
48
from leapp.libraries.stdlib import api
59
from leapp.models import OpenSshConfig, OpenSshPermitRootLogin
@@ -12,14 +16,31 @@ def line_empty(line):
1216
return len(line) == 0 or line.startswith('\n') or line.startswith('#')
1317

1418

15-
def parse_config(config):
16-
"""Parse OpenSSH server configuration or the output of sshd test option."""
19+
def parse_config(config, base_config=None, current_cfg_depth=0):
20+
"""
21+
Parse OpenSSH server configuration or the output of sshd test option.
1722
18-
# RHEL7 defaults
19-
ret = OpenSshConfig(
20-
permit_root_login=[],
21-
deprecated_directives=[]
22-
)
23+
:param Optional[OpenSshConfig] base_config: Base configuration that is extended with configuration options from
24+
current file.
25+
26+
:param int current_cfg_depth: Internal counter for how many includes were already followed.
27+
"""
28+
29+
if current_cfg_depth > 16:
30+
# This should really never happen as it would mean the SSH server won't
31+
# start anyway on the old system.
32+
error = 'Too many recursive includes while parsing sshd_config'
33+
api.current_logger().error(error)
34+
return None
35+
36+
ret = base_config
37+
if ret is None:
38+
# RHEL7 defaults
39+
ret = OpenSshConfig(
40+
permit_root_login=[],
41+
deprecated_directives=[]
42+
)
43+
# TODO(Jakuje): Do we need different defaults for RHEL8?
2344

2445
in_match = None
2546
for line in config:
@@ -68,8 +89,26 @@ def parse_config(config):
6889
# here we need to record all remaining items as command and arguments
6990
ret.subsystem_sftp = ' '.join(el[2:])
7091

92+
elif el[0].lower() == 'include':
93+
# recursively parse the given file or files referenced by this option
94+
# the option can have several space-separated filenames with glob wildcards
95+
for pattern in shlex.split(' '.join(el[1:])):
96+
if pattern[0] != '/' and pattern[0] != '~':
97+
pattern = os.path.join('/etc/ssh/', pattern)
98+
files_matching_include_pattern = glob.glob(pattern)
99+
# OpenSSH sorts the files lexicographically
100+
files_matching_include_pattern.sort()
101+
for included_config_file in files_matching_include_pattern:
102+
output = read_sshd_config(included_config_file)
103+
if parse_config(output, base_config=ret, current_cfg_depth=current_cfg_depth + 1) is None:
104+
raise StopActorExecutionError(
105+
'Failed to parse sshd configuration file',
106+
details={'details': 'Too many recursive includes while parsing {}.'
107+
.format(included_config_file)}
108+
)
109+
71110
elif el[0].lower() in DEPRECATED_DIRECTIVES:
72-
# Filter out duplicit occurrences of the same deprecated directive
111+
# Filter out duplicate occurrences of the same deprecated directive
73112
if el[0].lower() not in ret.deprecated_directives:
74113
# Use the directive in the form as found in config for user convenience
75114
ret.deprecated_directives.append(el[0])
@@ -82,10 +121,10 @@ def produce_config(producer, config):
82121
producer(config)
83122

84123

85-
def read_sshd_config():
124+
def read_sshd_config(config):
86125
"""Read the actual sshd configuration file."""
87126
try:
88-
with open(CONFIG, 'r') as fd:
127+
with open(config, 'r') as fd:
89128
return fd.readlines()
90129
except IOError as err:
91130
if err.errno != errno.ENOENT:
@@ -98,7 +137,7 @@ def scan_sshd(producer):
98137
"""Parse sshd_config configuration file to create OpenSshConfig message."""
99138

100139
# direct access to configuration file
101-
output = read_sshd_config()
140+
output = read_sshd_config(CONFIG)
102141
config = parse_config(output)
103142

104143
# find out whether the file was modified from the one shipped in rpm

repos/system_upgrade/common/actors/opensshconfigscanner/tests/test_readopensshconfig_opensshconfigscanner.py

Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
import glob
2+
import os
3+
import shutil
4+
import tempfile
5+
6+
import pytest
7+
8+
from leapp.exceptions import StopActorExecutionError
9+
from leapp.libraries.actor import readopensshconfig
110
from leapp.libraries.actor.readopensshconfig import line_empty, parse_config, produce_config
211
from leapp.models import OpenSshConfig, OpenSshPermitRootLogin
312

@@ -143,12 +152,181 @@ def test_parse_config_deprecated():
143152
def test_parse_config_empty():
144153
output = parse_config([])
145154
assert isinstance(output, OpenSshConfig)
146-
assert isinstance(output, OpenSshConfig)
147155
assert not output.permit_root_login
148156
assert output.use_privilege_separation is None
149157
assert output.protocol is None
150158

151159

160+
def test_parse_config_include(monkeypatch):
161+
""" This already require some files to touch """
162+
163+
config_contents = {
164+
'/etc/ssh/sshd_config': [
165+
"Include /path/*.conf"
166+
],
167+
'/path/my.conf': [
168+
'Subsystem sftp internal-sftp'
169+
],
170+
'/path/another.conf': [
171+
'permitrootlogin no'
172+
]
173+
}
174+
175+
primary_config_path = '/etc/ssh/sshd_config'
176+
primary_config_contents = config_contents[primary_config_path]
177+
178+
def glob_mocked(pattern):
179+
assert pattern == '/path/*.conf'
180+
return ['/path/my.conf', '/path/another.conf']
181+
182+
def read_config_mocked(path):
183+
return config_contents[path]
184+
185+
monkeypatch.setattr(glob, 'glob', glob_mocked)
186+
monkeypatch.setattr(readopensshconfig, 'read_sshd_config', read_config_mocked)
187+
188+
output = parse_config(primary_config_contents)
189+
190+
assert isinstance(output, OpenSshConfig)
191+
assert len(output.permit_root_login) == 1
192+
assert output.permit_root_login[0].value == 'no'
193+
assert output.permit_root_login[0].in_match is None
194+
assert output.use_privilege_separation is None
195+
assert output.protocol is None
196+
assert output.subsystem_sftp == 'internal-sftp'
197+
198+
199+
def test_parse_config_include_recursive(monkeypatch):
200+
""" The recursive include should gracefully fail """
201+
202+
config_contents = {
203+
'/etc/ssh/sshd_config': [
204+
"Include /path/*.conf"
205+
],
206+
'/path/recursive.conf': [
207+
"Include /path/*.conf"
208+
],
209+
}
210+
211+
primary_config_path = '/etc/ssh/sshd_config'
212+
primary_config_contents = config_contents[primary_config_path]
213+
214+
def glob_mocked(pattern):
215+
assert pattern == '/path/*.conf'
216+
return ['/path/recursive.conf']
217+
218+
def read_config_mocked(path):
219+
return config_contents[path]
220+
221+
monkeypatch.setattr(glob, 'glob', glob_mocked)
222+
monkeypatch.setattr(readopensshconfig, 'read_sshd_config', read_config_mocked)
223+
224+
with pytest.raises(StopActorExecutionError) as recursive_error:
225+
parse_config(primary_config_contents)
226+
assert 'Failed to parse sshd configuration file' in str(recursive_error)
227+
228+
229+
def test_parse_config_include_relative(monkeypatch):
230+
""" When the include argument is relative path, it should point into the /etc/ssh/ """
231+
232+
config_contents = {
233+
'/etc/ssh/sshd_config': [
234+
"Include relative/*.conf"
235+
],
236+
'/etc/ssh/relative/default.conf': [
237+
'Match address 192.168.1.42',
238+
'PermitRootLogin yes'
239+
],
240+
'/etc/ssh/relative/other.conf': [
241+
'Match all',
242+
'PermitRootLogin prohibit-password'
243+
],
244+
'/etc/ssh/relative/wrong.extension': [
245+
"macs hmac-md5",
246+
],
247+
}
248+
249+
primary_config_path = '/etc/ssh/sshd_config'
250+
primary_config_contents = config_contents[primary_config_path]
251+
252+
def glob_mocked(pattern):
253+
assert pattern == '/etc/ssh/relative/*.conf'
254+
return ['/etc/ssh/relative/other.conf', '/etc/ssh/relative/default.conf']
255+
256+
def read_config_mocked(path):
257+
return config_contents[path]
258+
259+
monkeypatch.setattr(glob, 'glob', glob_mocked)
260+
monkeypatch.setattr(readopensshconfig, 'read_sshd_config', read_config_mocked)
261+
262+
output = parse_config(primary_config_contents)
263+
264+
assert isinstance(output, OpenSshConfig)
265+
assert len(output.permit_root_login) == 2
266+
assert output.permit_root_login[0].value == 'yes'
267+
assert output.permit_root_login[0].in_match == ['address', '192.168.1.42']
268+
assert output.permit_root_login[1].value == 'prohibit-password'
269+
assert output.permit_root_login[1].in_match == ['all']
270+
assert output.use_privilege_separation is None
271+
assert output.ciphers is None
272+
assert output.macs is None
273+
assert output.protocol is None
274+
assert output.subsystem_sftp is None
275+
276+
277+
def test_parse_config_include_complex(monkeypatch):
278+
""" This already require some files to touch """
279+
280+
config_contents = {
281+
'/etc/ssh/sshd_config': [
282+
"Include /path/*.conf /other/path/*.conf \"/last/path with spaces/*.conf\" "
283+
],
284+
'/path/my.conf': [
285+
'permitrootlogin prohibit-password'
286+
],
287+
'/other/path/another.conf': [
288+
'ciphers aes128-ctr'
289+
],
290+
'/last/path with spaces/filename with spaces.conf': [
291+
'subsystem sftp other-internal'
292+
]
293+
}
294+
glob_contents = {
295+
'/path/*.conf': [
296+
'/path/my.conf'
297+
],
298+
'/other/path/*.conf': [
299+
'/other/path/another.conf'
300+
],
301+
'/last/path with spaces/*.conf': [
302+
'/last/path with spaces/filename with spaces.conf'
303+
],
304+
}
305+
306+
primary_config_path = '/etc/ssh/sshd_config'
307+
primary_config_contents = config_contents[primary_config_path]
308+
309+
def glob_mocked(pattern):
310+
return glob_contents[pattern]
311+
312+
def read_config_mocked(path):
313+
return config_contents[path]
314+
315+
monkeypatch.setattr(glob, 'glob', glob_mocked)
316+
monkeypatch.setattr(readopensshconfig, 'read_sshd_config', read_config_mocked)
317+
318+
output = parse_config(primary_config_contents)
319+
320+
assert isinstance(output, OpenSshConfig)
321+
assert len(output.permit_root_login) == 1
322+
assert output.permit_root_login[0].value == 'prohibit-password'
323+
assert output.permit_root_login[0].in_match is None
324+
assert output.use_privilege_separation is None
325+
assert output.ciphers == "aes128-ctr"
326+
assert output.protocol is None
327+
assert output.subsystem_sftp == 'other-internal'
328+
329+
152330
def test_produce_config():
153331
output = []
154332

0 commit comments

Comments
 (0)