From fc47662be35c0f5a570b648639b47d255fa0c2ef Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Mon, 12 Apr 2021 13:21:21 -0400 Subject: [PATCH] Add check to validate KRA Agent LDAP entry is correct Resolves: #1947043 --- 0004-Add-check-for-IPA-KRA-Agent.patch | 236 ++++++++++++++++++ 0005-Add-tests-for-KRA-Agent-validation.patch | 207 +++++++++++++++ freeipa-healthcheck.spec | 7 +- 3 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 0004-Add-check-for-IPA-KRA-Agent.patch create mode 100644 0005-Add-tests-for-KRA-Agent-validation.patch diff --git a/0004-Add-check-for-IPA-KRA-Agent.patch b/0004-Add-check-for-IPA-KRA-Agent.patch new file mode 100644 index 0000000..a2528fd --- /dev/null +++ b/0004-Add-check-for-IPA-KRA-Agent.patch @@ -0,0 +1,236 @@ +From 3f6ed4393dfa9ddf982e326065a3ea160bef90b6 Mon Sep 17 00:00:00 2001 +From: Antonio Torres +Date: Tue, 23 Feb 2021 16:11:59 +0100 +Subject: [PATCH] Add check for IPA KRA Agent + +Add check to validate KRA Agent in case KRA is installed, including +checking for the KRA Agent LDAP entry. + +Fixes: https://bugzilla.redhat.com/show_bug.cgi?id=1894781 +Signed-off-by: Antonio Torres +--- + README.md | 16 ++- + src/ipahealthcheck/ipa/certs.py | 167 +++++++++++++++++++------------- + 2 files changed, 112 insertions(+), 71 deletions(-) + +diff --git a/README.md b/README.md +index b9c60a2..0f3ed6a 100644 +--- a/README.md ++++ b/README.md +@@ -547,7 +547,21 @@ Verify the description and userCertificate values in uid=ipara,ou=People,o=ipaca + "kw": { + "expected": "2;125;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST", + "got": "2;7;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST", +- "msg": "RA agent description does not match 2;7;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST in LDAP and expected 2;125;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST" ++ "msg": "RA agent description does not match. Found 2;7;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST in LDAP and expected 2;125;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST" ++ } ++ } ++ ++### IPAKRAAgent ++Verify the description and userCertificate values in uid=ipakra,ou=people,o=kra,o=ipaca. ++ ++ { ++ "source": "ipahealthcheck.ipa.certs", ++ "check": "IPAKRAAgent", ++ "result": "ERROR", ++ "kw": { ++ "expected": "2;125;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST", ++ "got": "2;7;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST", ++ "msg": "KRA agent description does not match. Found 2;7;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST in LDAP and expected 2;125;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST" + } + } + +diff --git a/src/ipahealthcheck/ipa/certs.py b/src/ipahealthcheck/ipa/certs.py +index d3043d0..32c0d76 100644 +--- a/src/ipahealthcheck/ipa/certs.py ++++ b/src/ipahealthcheck/ipa/certs.py +@@ -724,6 +724,83 @@ class IPAOpenSSLChainValidation(IPAPlugin): + self, constants.SUCCESS, key=cert) + + ++def check_agent(plugin, base_dn, agent_type): ++ """Check RA/KRA Agent""" ++ ++ try: ++ cert = x509.load_certificate_from_file(paths.RA_AGENT_PEM) ++ except Exception as e: ++ yield Result(plugin, constants.ERROR, ++ error=str(e), ++ msg='Unable to load RA cert: {error}') ++ return ++ serial_number = cert.serial_number ++ subject = DN(cert.subject) ++ issuer = DN(cert.issuer) ++ description = '2;%d;%s;%s' % (serial_number, issuer, subject) ++ logger.debug('%s agent description should be %s', agent_type, description) ++ db_filter = ldap2.ldap2.combine_filters( ++ [ ++ ldap2.ldap2.make_filter({'objectClass': 'inetOrgPerson'}), ++ ldap2.ldap2.make_filter( ++ {'description': ';%s;%s' % (issuer, subject)}, ++ exact=False, trailing_wildcard=False), ++ ], ++ ldap2.ldap2.MATCH_ALL) ++ try: ++ entries = plugin.conn.get_entries(base_dn, ++ plugin.conn.SCOPE_SUBTREE, ++ db_filter) ++ except errors.NotFound: ++ yield Result(plugin, constants.ERROR, ++ description=description, ++ msg='%s agent not found in LDAP' % agent_type) ++ return ++ except Exception as e: ++ yield Result(plugin, constants.ERROR, ++ error=str(e), ++ msg='Retrieving %s agent from LDAP failed {error}' ++ % agent_type) ++ return ++ else: ++ logger.debug('%s agent description is %s', agent_type, description) ++ if len(entries) != 1: ++ yield Result(plugin, constants.ERROR, ++ found=len(entries), ++ msg='Too many %s agent entries found, {found}' ++ % agent_type) ++ return ++ entry = entries[0] ++ raw_desc = entry.get('description') ++ if raw_desc is None: ++ yield Result(plugin, constants.ERROR, ++ msg='%s agent is missing the description ' ++ 'attribute or it is not readable' % agent_type) ++ return ++ ra_desc = raw_desc[0] ++ ra_certs = entry.get('usercertificate') ++ if ra_desc != description: ++ yield Result(plugin, constants.ERROR, ++ expected=description, ++ got=ra_desc, ++ msg='%s agent description does not match. Found ' ++ '{got} in LDAP and expected {expected}' % agent_type) ++ return ++ found = False ++ for candidate in ra_certs: ++ if candidate == cert: ++ found = True ++ break ++ if not found: ++ yield Result(plugin, constants.ERROR, ++ certfile=paths.RA_AGENT_PEM, ++ dn=str(entry.dn), ++ msg='%s agent certificate in {certfile} not ' ++ 'found in LDAP userCertificate attribute ' ++ 'for the entry {dn}' % agent_type) ++ yield Result(plugin, constants.SUCCESS) ++ ++ + @registry + class IPARAAgent(IPAPlugin): + """Validate the RA Agent used to talk to the CA +@@ -739,82 +816,32 @@ class IPARAAgent(IPAPlugin): + logger.debug('CA is not configured, skipping RA Agent check') + return + +- try: +- cert = x509.load_certificate_from_file(paths.RA_AGENT_PEM) +- except Exception as e: +- yield Result(self, constants.ERROR, +- error=str(e), +- msg='Unable to load RA cert: {error}') +- return ++ base_dn = DN('uid=ipara,ou=people,o=ipaca') ++ yield from check_agent(self, base_dn, 'RA') + +- serial_number = cert.serial_number +- subject = DN(cert.subject) +- issuer = DN(cert.issuer) +- description = '2;%d;%s;%s' % (serial_number, issuer, subject) + +- logger.debug('RA agent description should be %s', description) ++@registry ++class IPAKRAAgent(IPAPlugin): ++ """Validate the KRA Agent + +- db_filter = ldap2.ldap2.combine_filters( +- [ +- ldap2.ldap2.make_filter({'objectClass': 'inetOrgPerson'}), +- ldap2.ldap2.make_filter({'sn': 'ipara'}), +- ldap2.ldap2.make_filter( +- {'description': ';%s;%s' % (issuer, subject)}, +- exact=False, trailing_wildcard=False), +- ], +- ldap2.ldap2.MATCH_ALL) ++ Compare the description and usercertificate values. ++ """ + +- base_dn = DN(('o', 'ipaca')) +- try: +- entries = self.conn.get_entries(base_dn, +- self.conn.SCOPE_SUBTREE, +- db_filter) +- except errors.NotFound: +- yield Result(self, constants.ERROR, +- description=description, +- msg='RA agent not found in LDAP') ++ requires = ('dirsrv',) ++ ++ @duration ++ def check(self): ++ if not self.ca.is_configured(): ++ logger.debug('CA is not configured, skipping KRA Agent check') + return +- except Exception as e: +- yield Result(self, constants.ERROR, +- error=str(e), +- msg='Retrieving RA agent from LDAP failed {error}') ++ ++ kra = krainstance.KRAInstance(api.env.realm) ++ if not kra.is_installed(): ++ logger.debug('KRA is not installed, skipping KRA Agent check') + return +- else: +- logger.debug('RA agent description is %s', description) +- if len(entries) != 1: +- yield Result(self, constants.ERROR, +- found=len(entries), +- msg='Too many RA agent entries found, {found}') +- return +- entry = entries[0] +- raw_desc = entry.get('description') +- if raw_desc is None: +- yield Result(self, constants.ERROR, +- msg='RA agent is missing the description ' +- 'attribute or it is not readable') +- return +- ra_desc = raw_desc[0] +- ra_certs = entry.get('usercertificate') +- if ra_desc != description: +- yield Result(self, constants.ERROR, +- expected=description, +- got=ra_desc, +- msg='RA agent description does not match. Found ' +- '{got} in LDAP and expected {expected}') +- return +- found = False +- for candidate in ra_certs: +- if candidate == cert: +- found = True +- break +- if not found: +- yield Result(self, constants.ERROR, +- certfile=paths.RA_AGENT_PEM, +- dn=str(entry.dn), +- msg='RA agent certificate in {certfile} not ' +- 'found in LDAP userCertificate attribute ' +- 'for the entry {dn}') +- yield Result(self, constants.SUCCESS) ++ ++ base_dn = DN('uid=ipakra,ou=people,o=kra,o=ipaca') ++ yield from check_agent(self, base_dn, 'KRA') + + + @registry +-- +2.26.2 + diff --git a/0005-Add-tests-for-KRA-Agent-validation.patch b/0005-Add-tests-for-KRA-Agent-validation.patch new file mode 100644 index 0000000..ccd5d82 --- /dev/null +++ b/0005-Add-tests-for-KRA-Agent-validation.patch @@ -0,0 +1,207 @@ +From a6504bd7d32fe3553b9f6f807f3d84a1b87bb77c Mon Sep 17 00:00:00 2001 +From: Antonio Torres +Date: Wed, 24 Feb 2021 17:26:08 +0100 +Subject: [PATCH] Add tests for KRA Agent validation + +Add unit tests for KRA Agent validation. + +Signed-off-by: Antonio Torres +--- + tests/test_ipa_agent.py | 174 +++++++++++++++++++++++++++++++++++++++- + 1 file changed, 172 insertions(+), 2 deletions(-) + +diff --git a/tests/test_ipa_agent.py b/tests/test_ipa_agent.py +index 6605745..9b691f7 100644 +--- a/tests/test_ipa_agent.py ++++ b/tests/test_ipa_agent.py +@@ -4,11 +4,11 @@ + + from base import BaseTest + from unittest.mock import Mock, patch +-from util import capture_results, CAInstance ++from util import capture_results, CAInstance, KRAInstance + + from ipahealthcheck.core import config, constants + from ipahealthcheck.ipa.plugin import registry +-from ipahealthcheck.ipa.certs import IPARAAgent ++from ipahealthcheck.ipa.certs import IPARAAgent, IPAKRAAgent + + from ipalib import errors + from ipapython.dn import DN +@@ -218,3 +218,173 @@ class TestNSSAgent(BaseTest): + assert result.result == constants.SUCCESS + assert result.source == 'ipahealthcheck.ipa.certs' + assert result.check == 'IPARAAgent' ++ ++ ++class TestKRAAgent(BaseTest): ++ cert = IPACertificate() ++ patches = { ++ 'ldap.initialize': ++ Mock(return_value=mock_ldap_conn()), ++ 'ipaserver.install.krainstance.KRAInstance': ++ Mock(return_value=KRAInstance()), ++ 'ipalib.x509.load_certificate_from_file': ++ Mock(return_value=cert), ++ } ++ ++ def test_kra_agent_ok(self): ++ ++ attrs = dict( ++ description=['2;1;CN=ISSUER;CN=RA AGENT'], ++ usercertificate=[self.cert], ++ ) ++ fake_conn = LDAPClient('ldap://localhost', no_schema=True) ++ ldapentry = LDAPEntry(fake_conn, ++ DN('uid=ipakra,ou=people,o=kra,o=ipaca')) ++ for attr, values in attrs.items(): ++ ldapentry[attr] = values ++ ++ framework = object() ++ registry.initialize(framework, config.Config()) ++ f = IPAKRAAgent(registry) ++ ++ f.conn = mock_ldap([ldapentry]) ++ self.results = capture_results(f) ++ ++ assert len(self.results) == 1 ++ ++ result = self.results.results[0] ++ assert result.result == constants.SUCCESS ++ assert result.source == 'ipahealthcheck.ipa.certs' ++ assert result.check == 'IPAKRAAgent' ++ ++ def test_kra_agent_no_description(self): ++ ++ attrs = dict( ++ usercertificate=[self.cert], ++ ) ++ fake_conn = LDAPClient('ldap://localhost', no_schema=True) ++ ldapentry = LDAPEntry(fake_conn, ++ DN('uid=ipakra,ou=people,o=kra,o=ipaca')) ++ for attr, values in attrs.items(): ++ ldapentry[attr] = values ++ ++ framework = object() ++ registry.initialize(framework, config.Config()) ++ f = IPAKRAAgent(registry) ++ ++ f.conn = mock_ldap([ldapentry]) ++ self.results = capture_results(f) ++ result = self.results.results[0] ++ ++ assert result.result == constants.ERROR ++ assert 'description' in result.kw.get('msg') ++ ++ @patch('ipalib.x509.load_certificate_from_file') ++ def test_kra_agent_load_failure(self, mock_load_cert): ++ ++ mock_load_cert.side_effect = IOError('test') ++ ++ framework = object() ++ registry.initialize(framework, config.Config()) ++ f = IPAKRAAgent(registry) ++ ++ self.results = capture_results(f) ++ result = self.results.results[0] ++ ++ assert result.result == constants.ERROR ++ assert result.kw.get('error') == 'test' ++ ++ def test_kra_agent_no_entry_found(self): ++ ++ framework = object() ++ registry.initialize(framework, config.Config()) ++ f = IPAKRAAgent(registry) ++ ++ f.conn = mock_ldap(None) # None == NotFound ++ self.results = capture_results(f) ++ result = self.results.results[0] ++ ++ assert result.result == constants.ERROR ++ assert result.kw.get('msg') == 'KRA agent not found in LDAP' ++ ++ def test_kra_agent_too_many(self): ++ ++ attrs = dict( ++ description=['2;1;CN=ISSUER;CN=RA AGENT'], ++ usercertificate=[self.cert], ++ ) ++ fake_conn = LDAPClient('ldap://localhost', no_schema=True) ++ ldapentry = LDAPEntry(fake_conn, ++ DN('uid=ipakra,ou=people,o=kra,o=ipaca')) ++ for attr, values in attrs.items(): ++ ldapentry[attr] = values ++ ++ ldapentry2 = LDAPEntry(fake_conn, ++ DN('uid=ipakra,ou=people,o=kra,o=ipaca')) ++ for attr, values in attrs.items(): ++ ldapentry[attr] = values ++ ++ framework = object() ++ registry.initialize(framework, config.Config()) ++ f = IPAKRAAgent(registry) ++ ++ f.conn = mock_ldap([ldapentry, ldapentry2]) ++ self.results = capture_results(f) ++ result = self.results.results[0] ++ ++ assert result.result == constants.ERROR ++ assert result.kw.get('found') == 2 ++ ++ def test_kra_agent_nonmatching_cert(self): ++ ++ cert2 = IPACertificate(2) ++ ++ attrs = dict( ++ description=['2;1;CN=ISSUER;CN=RA AGENT'], ++ usercertificate=[cert2], ++ ) ++ fake_conn = LDAPClient('ldap://localhost', no_schema=True) ++ ldapentry = LDAPEntry(fake_conn, ++ DN('uid=ipakra,ou=people,o=kra,o=ipaca')) ++ for attr, values in attrs.items(): ++ ldapentry[attr] = values ++ ++ framework = object() ++ registry.initialize(framework, config.Config()) ++ f = IPAKRAAgent(registry) ++ ++ f.conn = mock_ldap([ldapentry]) ++ self.results = capture_results(f) ++ result = self.results.results[0] ++ ++ assert result.result == constants.ERROR ++ assert result.kw.get('certfile') == paths.RA_AGENT_PEM ++ assert result.kw.get('dn') == 'uid=ipakra,ou=people,o=kra,o=ipaca' ++ ++ def test_kra_agent_multiple_certs(self): ++ ++ cert2 = IPACertificate(2) ++ ++ attrs = dict( ++ description=['2;1;CN=ISSUER;CN=RA AGENT'], ++ usercertificate=[cert2, self.cert], ++ ) ++ fake_conn = LDAPClient('ldap://localhost', no_schema=True) ++ ldapentry = LDAPEntry(fake_conn, ++ DN('uid=ipakra,ou=people,o=kra,o=ipaca')) ++ for attr, values in attrs.items(): ++ ldapentry[attr] = values ++ ++ framework = object() ++ registry.initialize(framework, config.Config) ++ f = IPAKRAAgent(registry) ++ ++ f.conn = mock_ldap([ldapentry]) ++ self.results = capture_results(f) ++ ++ assert len(self.results) == 1 ++ ++ result = self.results.results[0] ++ assert result.result == constants.SUCCESS ++ assert result.source == 'ipahealthcheck.ipa.certs' ++ assert result.check == 'IPAKRAAgent' +-- +2.26.2 + diff --git a/freeipa-healthcheck.spec b/freeipa-healthcheck.spec index f25bc4d..dcb14d8 100644 --- a/freeipa-healthcheck.spec +++ b/freeipa-healthcheck.spec @@ -17,7 +17,7 @@ Name: %{prefix}-healthcheck Version: 0.8 -Release: 6.1%{?dist} +Release: 7%{?dist} Summary: Health check tool for %{productname} BuildArch: noarch License: GPLv3 @@ -28,6 +28,8 @@ Source1: ipahealthcheck.conf Patch0001: 0001-Remove-ipaclustercheck.patch Patch0002: 0002-Handle-slight-different-in-exception-output-in-Pytho.patch Patch0003: 0003-Failed-expected-group-should-be-a-string-not-a-list.patch +Patch0004: 0004-Add-check-for-IPA-KRA-Agent.patch +Patch0005: 0005-Add-tests-for-KRA-Agent-validation.patch Requires: %{name}-core = %{version}-%{release} Requires: %{prefix}-server @@ -158,6 +160,9 @@ tox -epy3 %changelog +* Tue Apr 6 2021 Rob Crittenden - 0.8-7 +- Add check to validate the KRA Agent is correct (#1894781) + * Thu Apr 15 2021 Mohan Boddu - 0.8-6.1 - Rebuilt for RHEL 9 BETA on Apr 15th 2021. Related: rhbz#1947937