diff --git a/repos/system_upgrade/common/actors/openssl/checkopensslconf/actor.py b/repos/system_upgrade/common/actors/openssl/checkopensslconf/actor.py new file mode 100644 index 0000000000..dd05db9c27 --- /dev/null +++ b/repos/system_upgrade/common/actors/openssl/checkopensslconf/actor.py @@ -0,0 +1,33 @@ +from leapp.actors import Actor +from leapp.libraries.actor import checkopensslconf +from leapp.models import DistributionSignedRPM, Report, TrackedFilesInfoSource +from leapp.tags import ChecksPhaseTag, IPUWorkflowTag + + +class CheckOpenSSLConf(Actor): + """ + Check whether the openssl configuration and openssl-IBMCA. + + See the report messages for more details. The summary is that since RHEL 8 + it's expected to configure OpenSSL via crypto policies. Also, OpenSSL has + different versions between major versions of RHEL: + * RHEL 7: 1.0, + * RHEL 8: 1.1, + * RHEL 9: 3.0 + So OpenSSL configuration from older system does not have to be 100% + compatible with the new system. In some cases, the old configuration could + make the system inaccessible remotely. So new approach is to ensure the + upgraded system will use always new default /etc/pki/tls/openssl.cnf + configuration file (the original one will be backed up if modified by user). + + Similar for OpenSSL-IBMCA, when it's expected to configure it again on + each newer system. + """ + + name = 'check_openssl_conf' + consumes = (DistributionSignedRPM, TrackedFilesInfoSource) + produces = (Report,) + tags = (IPUWorkflowTag, ChecksPhaseTag) + + def process(self): + checkopensslconf.process() diff --git a/repos/system_upgrade/common/actors/openssl/checkopensslconf/libraries/checkopensslconf.py b/repos/system_upgrade/common/actors/openssl/checkopensslconf/libraries/checkopensslconf.py new file mode 100644 index 0000000000..06a30fa102 --- /dev/null +++ b/repos/system_upgrade/common/actors/openssl/checkopensslconf/libraries/checkopensslconf.py @@ -0,0 +1,135 @@ +from leapp import reporting +from leapp.libraries.common.config import architecture, version +from leapp.libraries.common.rpms import has_package +from leapp.libraries.stdlib import api +from leapp.models import DistributionSignedRPM, TrackedFilesInfoSource + +DEFAULT_OPENSSL_CONF = '/etc/pki/tls/openssl.cnf' +URL_8_CRYPTOPOLICIES = 'https://red.ht/rhel-8-system-wide-crypto-policies' +URL_9_CRYPTOPOLICIES = 'https://red.ht/rhel-9-system-wide-crypto-policies' + + +def check_ibmca(): + if not architecture.matches_architecture(architecture.ARCH_S390X): + # not needed check really, but keeping it to make it clear + return + if not has_package(DistributionSignedRPM, 'openssl-ibmca'): + return + # In RHEL 9 has been introduced new technology: openssl providers. The engine + # is deprecated, so keep proper teminology to not confuse users. + dst_tech = 'engine' if version.get_target_major_version() == '8' else 'providers' + summary = ( + 'The presence of openssl-ibmca package suggests that the system may be configured' + ' to use the IBMCA OpenSSL engine.' + ' Due to major changes in OpenSSL and libica between RHEL {source} and RHEL {target} it is not' + ' possible to migrate OpenSSL configuration files automatically. Therefore,' + ' it is necessary to enable IBMCA {tech} in the OpenSSL config file manually' + ' after the system upgrade.' + .format( + source=version.get_source_major_version(), + target=version.get_target_major_version(), + tech=dst_tech + ) + ) + + hint = ( + 'Configure the IBMCA {tech} manually after the upgrade.' + ' Please, be aware that it is not recommended to configure the system default' + ' {fpath}. Instead, it is recommended to configure a copy of' + ' that file and use this copy only for particular applications that are supposed' + ' to utilize the IBMCA {tech}. The location of the OpenSSL configuration file' + ' can be specified using the OPENSSL_CONF environment variable.' + .format(tech=dst_tech, fpath=DEFAULT_OPENSSL_CONF) + ) + + reporting.create_report([ + reporting.Title('Detected possible use of IBMCA in OpenSSL'), + reporting.Summary(summary), + reporting.Remediation(hint=hint), + reporting.Severity(reporting.Severity.MEDIUM), + reporting.Groups([ + reporting.Groups.POST, + reporting.Groups.ENCRYPTION + ]), + ]) + + +def _is_openssl_modified(): + tracked_files = next(api.consume(TrackedFilesInfoSource), None) + if not tracked_files: + # unexpected at all, skipping testing, but keeping the log just in case + api.current_logger.warning('The TrackedFilesInfoSource message is missing! Skipping check of openssl config.') + return False + for finfo in tracked_files.files: + if finfo.path == DEFAULT_OPENSSL_CONF: + return finfo.is_modified + return False + + +def check_default_openssl(): + if not _is_openssl_modified(): + return + + crypto_url = URL_8_CRYPTOPOLICIES if version.get_target_major_version == '8' else URL_9_CRYPTOPOLICIES + + # TODO(pstodulk): Needs in future some rewording, as OpenSSL engines are + # deprecated since "RHEL 8" and people should use OpenSSL providers instead. + # (IIRC, they are required to use OpenSSL providers since RHEL 9.) The + # current wording could be inaccurate. + summary = ( + 'The OpenSSL configuration file ({fpath}) has been' + ' modified on the system. RHEL 8 (and newer) systems provide a crypto-policies' + ' mechanism ensuring usage of system-wide secure cryptography algorithms.' + ' Also the target system uses newer version of OpenSSL that is not fully' + ' compatible with the current one.' + ' To ensure the upgraded system uses crypto-policies as expected,' + ' the new version of the openssl configuration file must be installed' + ' during the upgrade. This will be done automatically.' + ' The original configuration file will be saved' + ' as "{fpath}.leappsave".' + '\n\nNote this can affect the ability to connect to the system after' + ' the upgrade if it depends on the current OpenSSL configuration.' + ' Such a problem may be caused by using a particular OpenSSL engine, as' + ' OpenSSL engines built for the' + ' RHEL {source} system are not compatible with RHEL {target}.' + .format( + fpath=DEFAULT_OPENSSL_CONF, + source=version.get_source_major_version(), + target=version.get_target_major_version() + ) + ) + if version.get_target_major_version() == '9': + # NOTE(pstodulk): that a try to make things with engine/providers a + # little bit better (see my TODO note above) + summary += ( + '\n\nNote the legacy ENGINE API is deprecated since RHEL 8 and' + ' it is required to use the new OpenSSL providers API instead on' + ' RHEL 9 systems.' + ) + hint = ( + 'Check that your ability to login to the system does not depend on' + ' the OpenSSL configuration. After the upgrade, review the system configuration' + ' and configure the system as needed.' + ' Please, be aware that it is not recommended to configure the system default' + ' {fpath}. Instead, it is recommended to copy the file and use this copy' + ' to configure particular applications.' + ' The default OpenSSL configuration file should be modified only' + ' when it is really necessary.' + ) + reporting.create_report([ + reporting.Title('The /etc/pki/tls/openssl.cnf file is modified and will be replaced during the upgrade.'), + reporting.Summary(summary), + reporting.Remediation(hint=hint), + reporting.Severity(reporting.Severity.HIGH), + reporting.Groups([reporting.Groups.POST, reporting.Groups.SECURITY]), + reporting.RelatedResource('file', DEFAULT_OPENSSL_CONF), + reporting.ExternalLink( + title='Using system-wide cryptographic policies.', + url=crypto_url + ) + ]) + + +def process(): + check_ibmca() + check_default_openssl() diff --git a/repos/system_upgrade/common/actors/openssl/checkopensslconf/tests/unit_test_checkopensslconf.py b/repos/system_upgrade/common/actors/openssl/checkopensslconf/tests/unit_test_checkopensslconf.py new file mode 100644 index 0000000000..541ff75d48 --- /dev/null +++ b/repos/system_upgrade/common/actors/openssl/checkopensslconf/tests/unit_test_checkopensslconf.py @@ -0,0 +1,102 @@ +import pytest + +from leapp import reporting +from leapp.libraries.actor import checkopensslconf +from leapp.libraries.common.config import architecture +from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, logger_mocked +from leapp.libraries.stdlib import api +from leapp.models import DistributionSignedRPM, FileInfo, RPM, TrackedFilesInfoSource + +_DUMP_PKG_NAMES = ['random', 'pkgs', 'openssl-ibmca-nope', 'ibmca', 'nope-openssl-ibmca'] +_SSL_CONF = checkopensslconf.DEFAULT_OPENSSL_CONF + + +def _msg_pkgs(pkgnames): + rpms = [] + for pname in pkgnames: + rpms.append(RPM( + name=pname, + epoch='0', + version='1.0', + release='1', + arch='noarch', + pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51', + packager='Red Hat, Inc. (auxiliary key 2) ' + + )) + return DistributionSignedRPM(items=rpms) + + +@pytest.mark.parametrize('arch,pkgnames,ibmca_report', ( + (architecture.ARCH_S390X, [], False), + (architecture.ARCH_S390X, _DUMP_PKG_NAMES, False), + (architecture.ARCH_S390X, ['openssl-ibmca'], True), + (architecture.ARCH_S390X, _DUMP_PKG_NAMES + ['openssl-ibmca'], True), + (architecture.ARCH_S390X, ['openssl-ibmca'] + _DUMP_PKG_NAMES, True), + + # stay false for non-IBM-z arch - invalid scenario basically + (architecture.ARCH_X86_64, ['openssl-ibmca'], False), + (architecture.ARCH_PPC64LE, ['openssl-ibmca'], False), + (architecture.ARCH_ARM64, ['openssl-ibmca'], False), + +)) +@pytest.mark.parametrize('src_maj_ver', ('7', '8', '9')) +def test_check_ibmca(monkeypatch, src_maj_ver, arch, pkgnames, ibmca_report): + monkeypatch.setattr(reporting, "create_report", create_report_mocked()) + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked( + arch=arch, + msgs=[_msg_pkgs(pkgnames)], + src_ver='{}.6'.format(src_maj_ver), + dst_ver='{}.0'.format(int(src_maj_ver) + 1) + )) + checkopensslconf.check_ibmca() + + if not ibmca_report: + assert not reporting.create_report.called, 'IBMCA report created when it should not.' + else: + assert reporting.create_report.called, 'IBMCA report has not been created.' + + +def _msg_files(fnames_changed, fnames_untouched): + res = [] + for fname in fnames_changed: + res.append(FileInfo( + path=fname, + exists=True, + is_modified=True + )) + + for fname in fnames_untouched: + res.append(FileInfo( + path=fname, + exists=True, + is_modified=False + )) + + return TrackedFilesInfoSource(files=res) + + +# NOTE(pstodulk): Ignoring situation when _SSL_CONF is missing (modified, do not exists). +# It's not a valid scenario actually, as this file just must exists on the system to +# consider it in a supported state. +@pytest.mark.parametrize('msg,openssl_report', ( + # matrix focused on openssl reports only (positive) + (_msg_files([], []), False), + (_msg_files([_SSL_CONF], []), True), + (_msg_files(['what/ever', _SSL_CONF, 'something'], []), True), + (_msg_files(['what/ever'], [_SSL_CONF]), False), +)) +@pytest.mark.parametrize('src_maj_ver', ('7', '8', '9')) +def test_check_openssl(monkeypatch, src_maj_ver, msg, openssl_report): + monkeypatch.setattr(reporting, "create_report", create_report_mocked()) + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked( + msgs=[msg], + src_ver='{}.6'.format(src_maj_ver), + dst_ver='{}.0'.format(int(src_maj_ver) + 1) + )) + checkopensslconf.process() + + if not openssl_report: + assert not reporting.create_report.called, 'OpenSSL report created when it should not.' + else: + assert reporting.create_report.called, 'OpenSSL report has not been created.' diff --git a/repos/system_upgrade/common/actors/openssl/migrateopensslconf/actor.py b/repos/system_upgrade/common/actors/openssl/migrateopensslconf/actor.py new file mode 100644 index 0000000000..16bb3ae0eb --- /dev/null +++ b/repos/system_upgrade/common/actors/openssl/migrateopensslconf/actor.py @@ -0,0 +1,26 @@ +from leapp.actors import Actor +from leapp.libraries.actor import migrateopensslconf +from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag + + +class MigrateOpenSslConf(Actor): + """ + Enforce the target default configuration file to be used. + + If the /etc/pki/tls/openssl.cnf has been modified and openssl.cnf.rpmnew + file is created, backup the original one and replace it by the new default. + + tl;dr: + if the file is modified; then + mv /etc/pki/tls/openssl.cnf{,rpmsave_leapp} + mv /etc/pki/tls/openssl.cnf{.rpmnew,} + fi + """ + + name = 'migrate_openssl_conf' + consumes = () + produces = () + tags = (IPUWorkflowTag, ApplicationsPhaseTag) + + def process(self): + migrateopensslconf.process() diff --git a/repos/system_upgrade/common/actors/openssl/migrateopensslconf/libraries/migrateopensslconf.py b/repos/system_upgrade/common/actors/openssl/migrateopensslconf/libraries/migrateopensslconf.py new file mode 100644 index 0000000000..140c57181a --- /dev/null +++ b/repos/system_upgrade/common/actors/openssl/migrateopensslconf/libraries/migrateopensslconf.py @@ -0,0 +1,54 @@ +import os + +from leapp.libraries.stdlib import api, CalledProcessError, run + +DEFAULT_OPENSSL_CONF = '/etc/pki/tls/openssl.cnf' +OPENSSL_CONF_RPMNEW = '{}.rpmnew'.format(DEFAULT_OPENSSL_CONF) +OPENSSL_CONF_BACKUP = '{}.leappsave'.format(DEFAULT_OPENSSL_CONF) + + +def _is_openssl_modified(): + """ + Return True if modified in any way + """ + # NOTE(pstodulk): this is different from the approach in scansourcefiles, + # where we are interested about modified content. In this case, if the + # file is modified in any way, let's do something about that.. + try: + run(['rpm', '-Vf', DEFAULT_OPENSSL_CONF]) + except CalledProcessError: + return True + return False + + +def _safe_mv_file(src, dst): + """ + Move the file from src to dst. Return True on success, otherwise False. + """ + try: + run(['mv', src, dst]) + except CalledProcessError: + return False + return True + + +def process(): + if not _is_openssl_modified(): + return + if not os.path.exists(OPENSSL_CONF_RPMNEW): + api.current_logger().debug('The {} file is modified, but *.rpmsave not found. Cannot do anything.') + return + if not _safe_mv_file(DEFAULT_OPENSSL_CONF, OPENSSL_CONF_BACKUP): + # NOTE(pstodulk): One of reasons could be the file is missing, however + # that's not expected to happen at all. If the file is missing before + # the upgrade, it will be installed by new openssl* package + api.current_logger().error( + 'Could not back up the {} file. Skipping other actions.' + .format(DEFAULT_OPENSSL_CONF) + ) + return + if not _safe_mv_file(OPENSSL_CONF_RPMNEW, DEFAULT_OPENSSL_CONF): + # unexpected, it's double seatbelt + api.current_logger().error('Cannot apply the new openssl configuration file! Restore it from the backup.') + if not _safe_mv_file(OPENSSL_CONF_BACKUP, DEFAULT_OPENSSL_CONF): + api.current_logger().error('Cannot restore the openssl configuration file!') diff --git a/repos/system_upgrade/common/actors/scansourcefiles/libraries/scansourcefiles.py b/repos/system_upgrade/common/actors/scansourcefiles/libraries/scansourcefiles.py index 33e0275fe8..16c0e8aa08 100644 --- a/repos/system_upgrade/common/actors/scansourcefiles/libraries/scansourcefiles.py +++ b/repos/system_upgrade/common/actors/scansourcefiles/libraries/scansourcefiles.py @@ -9,6 +9,7 @@ # '8' (etc..) -> files supposed to be scanned when particular major version of OS is used TRACKED_FILES = { 'common': [ + '/etc/pki/tls/openssl.cnf', ], '8': [ ],