Skip to content

Commit 7bb320c

Browse files
authored
SSH CA cert feature SAT-28038 (#20453)
Automate SSH CA cert feature SAT-28038
1 parent a70bfa4 commit 7bb320c

File tree

1 file changed

+339
-1
lines changed

1 file changed

+339
-1
lines changed

tests/foreman/destructive/test_remoteexecution.py

Lines changed: 339 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,93 @@
1717
from nailgun.entity_mixins import TaskFailedError
1818
import pytest
1919

20-
from robottelo.config import get_credentials
20+
from robottelo.config import get_credentials, settings
21+
from robottelo.exceptions import CLIFactoryError
2122
from robottelo.hosts import get_sat_version
23+
from robottelo.logging import logger
24+
from robottelo.utils.installer import InstallerCommand
2225

2326
CAPSULE_TARGET_VERSION = f'6.{get_sat_version().minor}.z'
2427

2528
pytestmark = pytest.mark.destructive
2629

2730

31+
def create_CA(host, path='/root/CA', name=None):
32+
"""Create a new CA keypair"""
33+
assert host.execute(f'mkdir -p {path}').status == 0
34+
filename = 'id_ca' if name is None else f'id_{name}_ca'
35+
assert (
36+
host.execute(
37+
f'cd {path} && if ! [ -f {filename} ]; then ssh-keygen -t ed25519 -f {filename} -N ""; fi'
38+
).status
39+
== 0
40+
)
41+
return filename
42+
43+
44+
@pytest.fixture
45+
def ca_sat(target_sat):
46+
"""Setup SSH key certs and CA public key on Satellite"""
47+
path = "/root/CA"
48+
sat_ssh_path = '/var/lib/foreman-proxy/ssh/'
49+
filename = create_CA(target_sat, path)
50+
ca_path = f'{path}/{filename}'
51+
key_name = 'id_rsa_foreman_proxy'
52+
cert_name = f'{key_name}-cert.pub'
53+
assert (
54+
target_sat.execute(
55+
f'cd {sat_ssh_path} && cp {ca_path}.pub . && restorecon {filename}.pub && chown foreman-proxy {filename}.pub && chgrp foreman-proxy {filename}.pub'
56+
).status
57+
== 0
58+
)
59+
assert (
60+
target_sat.execute(
61+
f'cd {sat_ssh_path} && ssh-keygen -s {ca_path} -I satellite -n root {key_name}.pub && restorecon {cert_name} && chown foreman-proxy {cert_name} && chgrp foreman-proxy {cert_name}'
62+
).status
63+
== 0
64+
)
65+
return (target_sat, f'{sat_ssh_path}/{filename}.pub')
66+
67+
68+
@pytest.fixture
69+
def ca_contenthost(rhel_contenthost):
70+
"""Setup SSH key certs and CA public key on content host"""
71+
path = '/root/CA'
72+
host_ssh_path = '/etc/ssh'
73+
filename = create_CA(rhel_contenthost, path, 'host')
74+
ca_path = f'{path}/{filename}'
75+
# create a host key and sign it
76+
key_name = 'ssh_host_ed25519_key'
77+
cert_name = f'{key_name}-cert.pub'
78+
assert (
79+
rhel_contenthost.execute(
80+
f'cd {host_ssh_path} && if ! [ -f {key_name} ]; then ssh-keygen -t ed25519 -f {key_name} -N ""; fi'
81+
).status
82+
== 0
83+
)
84+
assert (
85+
rhel_contenthost.execute(
86+
f'cd {host_ssh_path} && ssh-keygen -s {ca_path} -I host -n {rhel_contenthost.hostname} -h {key_name}.pub'
87+
).status
88+
== 0
89+
)
90+
# setup cert usage
91+
assert (
92+
rhel_contenthost.execute(
93+
f'mkdir -p {host_ssh_path}/sshd_config.d && cd {host_ssh_path}/sshd_config.d && echo "HostCertificate {host_ssh_path}/{cert_name}" > 60-host-cert.conf'
94+
).status
95+
== 0
96+
)
97+
assert rhel_contenthost.execute('systemctl restart sshd').status == 0
98+
return (rhel_contenthost, f'{ca_path}.pub')
99+
100+
101+
@pytest.fixture
102+
def host_ca_file_on_satellite(ca_contenthost):
103+
"""Return path of CA public key on Satellite"""
104+
return f'/var/lib/foreman-proxy/ssh/{ca_contenthost[1].split("/")[-1]}'
105+
106+
28107
def test_negative_run_capsule_upgrade_playbook_on_satellite(target_sat):
29108
"""Run Capsule Upgrade playbook against the Satellite itself
30109
@@ -135,3 +214,262 @@ def test_positive_use_alternate_directory(
135214
task = target_sat.cli.Task.list_tasks({'search': command})[0]
136215
search = target_sat.cli.Task.list_tasks({'search': f'id={task["id"]}'})
137216
assert search[0]['action'] == task['action']
217+
218+
219+
def register_host(satellite, host, org, cockpit=False):
220+
"""Register a content host to Satellite"""
221+
if cockpit:
222+
rhelver = host.os_version.major
223+
if rhelver > 7:
224+
repo = [settings.repos[f'rhel{rhelver}_os']['baseos']]
225+
else:
226+
repo = [settings.repos['rhel7_os'], settings.repos['rhel7_extras']]
227+
else:
228+
repo = []
229+
satellite.register_host_custom_repo(org, host, repo)
230+
return org
231+
232+
233+
def test_execution(satellite, host):
234+
"""Run a job invocation and return its results"""
235+
command = "echo rex_passed $(date) > /root/test"
236+
invocation_command = satellite.cli_factory.job_invocation(
237+
{
238+
'job-template': 'Run Command - Script Default',
239+
'inputs': f'command={command}',
240+
'search-query': f"name ~ {host.hostname}",
241+
}
242+
)
243+
return satellite.cli.JobInvocation.info({'id': invocation_command['id']})
244+
245+
246+
def log_count(satellite, host):
247+
"""Return number of lines mentioning CA was used in sshd log,
248+
for later use
249+
"""
250+
return int(
251+
host.execute(
252+
f'journalctl -u sshd | grep {satellite.ip_addr} | grep CA | wc -l'
253+
).stdout.strip()
254+
)
255+
256+
257+
def copy_host_CA(host, satellite, host_path, satellite_path):
258+
"""Copy CA public key from host to Satellite (for use in installer)"""
259+
host_ca_file_local = f'/tmp/{gen_string("alpha")}'
260+
host.get(host_path, host_ca_file_local)
261+
satellite.put(host_ca_file_local, satellite_path)
262+
263+
264+
@pytest.mark.no_containers
265+
@pytest.mark.rhel_ver_match([settings.content_host.default_rhel_version])
266+
def test_positive_ssh_ca_sat_only(ca_sat, rhel_contenthost, function_org):
267+
"""Setup Satellite's SSH cert, register host and run REX on that host
268+
269+
:id: 353a21bf-f379-440a-9dc6-e17bf6414713
270+
271+
:expectedresults: Verify the job has been run successfully against the host, Sat's public key hasn't been added to host's authorized_keys and CA verification has been used instead
272+
273+
:parametrized: yes
274+
"""
275+
sat = ca_sat[0]
276+
host = rhel_contenthost
277+
sat_ca_file = ca_sat[1]
278+
saved_count = log_count(sat, host)
279+
command = InstallerCommand(
280+
foreman_proxy_plugin_remote_execution_script_ssh_user_ca_public_key_file=sat_ca_file,
281+
)
282+
assert sat.install(command).status == 0
283+
register_host(sat, host, function_org, cockpit=True)
284+
result = test_execution(sat, host)
285+
# assert the run actually happened and it was authenticated using cert
286+
assert result['success'] == '1'
287+
logger.debug(result)
288+
assert log_count(sat, host) == saved_count + 1
289+
check = host.execute('grep rex_passed /root/test')
290+
assert check.status == 0
291+
check = host.execute(f'grep {sat.hostname} /root/.ssh/authorized_keys')
292+
assert check.status != 0
293+
294+
295+
@pytest.mark.no_containers
296+
@pytest.mark.rhel_ver_match([settings.content_host.default_rhel_version])
297+
def test_negative_ssh_ca_sat_wrong_cert(ca_sat, rhel_contenthost, function_org):
298+
"""Setup Satellite's SSH cert, register host, setup incorrect cert on Satellite and run REX on the host
299+
300+
:id: 7ddd170b-d489-4e2a-93af-ccf0c1a9d4ca
301+
302+
:expectedresults: Verify the job has failed against the host
303+
304+
:parametrized: yes
305+
"""
306+
sat = ca_sat[0]
307+
host = rhel_contenthost
308+
sat_ca_file = ca_sat[1]
309+
command = InstallerCommand(
310+
foreman_proxy_plugin_remote_execution_script_ssh_user_ca_public_key_file=sat_ca_file,
311+
)
312+
assert sat.install(command).status == 0
313+
register_host(sat, host, function_org, cockpit=True)
314+
315+
# create a different cert for the Satellite, with a wrong principal
316+
sat_ssh_path = '/var/lib/foreman-proxy/ssh/'
317+
key_name = 'id_rsa_foreman_proxy'
318+
cert_name = f'{key_name}-cert.pub'
319+
assert (
320+
sat.execute(
321+
f'cd {sat_ssh_path} && ssh-keygen -s /root/CA/id_ca -I satellite -n wronguser {key_name}.pub && restorecon {cert_name} && chown foreman-proxy {cert_name} && chgrp foreman-proxy {cert_name}'
322+
).status
323+
== 0
324+
)
325+
326+
# assert the run fails
327+
with pytest.raises(CLIFactoryError) as err:
328+
test_execution(sat, host)
329+
assert 'A sub task failed' in err.value.args[0]
330+
check = host.execute('grep rex_passed /root/test')
331+
assert check.status != 0
332+
333+
334+
@pytest.mark.no_containers
335+
@pytest.mark.rhel_ver_match([settings.content_host.default_rhel_version])
336+
def test_positive_ssh_ca_host_only(
337+
target_sat, ca_contenthost, host_ca_file_on_satellite, function_org
338+
):
339+
"""Setup host's SSH cert, add CA to Sat, register host and run REX on that host
340+
341+
:id: 0ad9bbf7-0be5-49ca-8d79-969242b6b9bc
342+
343+
:expectedresults: Verify the job has been run successfully against the host
344+
345+
:parametrized: yes
346+
"""
347+
sat = target_sat
348+
host = ca_contenthost[0]
349+
host_ca_file = ca_contenthost[1]
350+
copy_host_CA(host, sat, host_ca_file, host_ca_file_on_satellite)
351+
saved_count = log_count(sat, host)
352+
command = InstallerCommand(
353+
foreman_proxy_plugin_remote_execution_script_ssh_host_ca_public_keys_file=host_ca_file_on_satellite,
354+
)
355+
assert sat.install(command).status == 0
356+
register_host(sat, host, function_org, cockpit=True)
357+
result = test_execution(sat, host)
358+
# assert the run actually happened and it was NOT authenticated using cert
359+
assert result['success'] == '1'
360+
logger.debug(result)
361+
assert log_count(sat, host) == saved_count
362+
check = host.execute('grep rex_passed /root/test')
363+
assert check.status == 0
364+
365+
366+
@pytest.mark.no_containers
367+
@pytest.mark.rhel_ver_match([settings.content_host.default_rhel_version])
368+
def test_negative_ssh_ca_host_wrong_cert(
369+
target_sat, ca_contenthost, host_ca_file_on_satellite, function_org
370+
):
371+
"""Setup host's SSH cert, add a different CA to Sat, register host and run REX on that host
372+
373+
:id: 9e23d27d-a3a8-4c0d-9a0f-892d392aa660
374+
375+
:expectedresults: Verify the job has failed against the host
376+
377+
:parametrized: yes
378+
"""
379+
sat = target_sat
380+
host = ca_contenthost[0]
381+
fake_ca_dir = '/'.join(host_ca_file_on_satellite.split('/')[:-1])
382+
fake_ca_filename = host_ca_file_on_satellite.split('/')[-1]
383+
sat.execute(
384+
f'cd {fake_ca_dir} && {{ rm -f {fake_ca_filename}; ssh-keygen -t ed25519 -f {fake_ca_filename} -N ""; }}'
385+
)
386+
command = InstallerCommand(
387+
foreman_proxy_plugin_remote_execution_script_ssh_host_ca_public_keys_file=host_ca_file_on_satellite,
388+
)
389+
assert sat.install(command).status == 0
390+
register_host(sat, host, function_org, cockpit=True)
391+
# assert the run failed
392+
with pytest.raises(CLIFactoryError) as err:
393+
test_execution(sat, host)
394+
assert 'A sub task failed' in err.value.args[0]
395+
check = host.execute('grep rex_passed /root/test')
396+
assert check.status != 0
397+
398+
399+
@pytest.mark.e2e
400+
@pytest.mark.no_containers
401+
@pytest.mark.rhel_ver_match([settings.content_host.default_rhel_version])
402+
def test_positive_ssh_ca_sat_and_host_ssh_ansible_cockpit(
403+
ca_sat, ca_contenthost, host_ca_file_on_satellite, function_org
404+
):
405+
"""Setup Satellite's SSH cert, setup host's SSH cert, add CA to Sat, register host and run REX on that host
406+
407+
:id: 97c17417-3b20-4876-bf9c-7219a91acee2
408+
409+
:expectedresults: Verify the job has been run successfully against the host, Sat's public key hasn't been added to host's authorized_keys and CA verification has been used instead
410+
411+
412+
:parametrized: yes
413+
"""
414+
sat = ca_sat[0]
415+
sat_ca_file = ca_sat[1]
416+
host = ca_contenthost[0]
417+
host_ca_file = ca_contenthost[1]
418+
copy_host_CA(host, sat, host_ca_file, host_ca_file_on_satellite)
419+
# setup CA
420+
saved_count = log_count(sat, host)
421+
command = InstallerCommand(
422+
foreman_proxy_plugin_remote_execution_script_ssh_user_ca_public_key_file=sat_ca_file,
423+
foreman_proxy_plugin_remote_execution_script_ssh_host_ca_public_keys_file=host_ca_file_on_satellite,
424+
)
425+
assert sat.install(command).status == 0
426+
register_host(sat, host, function_org, cockpit=True)
427+
# SSH REX
428+
result = test_execution(sat, host)
429+
# assert the run actually happened and it was authenticated using cert
430+
assert result['success'] == '1'
431+
logger.debug(result)
432+
assert log_count(sat, host) == saved_count + 1
433+
check = host.execute('grep rex_passed /root/test')
434+
assert check.status == 0
435+
check = host.execute(f'grep {sat.hostname} /root/.ssh/authorized_keys')
436+
assert check.status != 0
437+
# ANSIBLE REX
438+
saved_count = log_count(sat, host)
439+
command = "echo rex2_passed $(date) > /root/test"
440+
invocation_command = sat.cli_factory.job_invocation(
441+
{
442+
'job-template': 'Run Command - Ansible Default',
443+
'inputs': f'command={command}',
444+
'search-query': f'name ~ {host.hostname}',
445+
}
446+
)
447+
result = sat.cli.JobInvocation.info({'id': invocation_command['id']})
448+
# assert the run actually happened and it was authenticated using cert
449+
assert result['success'] == '1'
450+
logger.debug(result)
451+
assert log_count(sat, host) == saved_count + 1
452+
check = host.execute('grep rex2_passed /root/test')
453+
assert check.status == 0
454+
# COCKPIT
455+
saved_count = log_count(sat, host)
456+
# setup Satellite for cockpit
457+
sat.register_to_cdn()
458+
sat.install_cockpit()
459+
sat.cli.Service.restart()
460+
# setup cockpit on host
461+
host.install_cockpit()
462+
# run cockpit
463+
# note that merely opening a cockpit in UI connects to ssh already
464+
# note that this is a UI part, as opposed to the previous parts and previous SSH CA tests
465+
with sat.ui_session() as session:
466+
session.organization.select(org_name=function_org.name)
467+
hostname_inside_cockpit = session.host.get_webconsole_content(
468+
entity_name=host.hostname,
469+
rhel_version=host.os_version.major,
470+
)
471+
assert host.hostname in hostname_inside_cockpit, (
472+
f'cockpit page shows hostname {hostname_inside_cockpit} instead of {host.hostname}'
473+
)
474+
# assert the cockpit access was authenticated using cert
475+
assert log_count(sat, host) == saved_count + 1

0 commit comments

Comments
 (0)