[Pkg-freeipa-devel] [Git][freeipa-team/freeipa-healthcheck][upstream] 21 commits: ipa-idns: Handle AAAA RRs

Timo Aaltonen gitlab at salsa.debian.org
Thu Nov 12 11:43:24 GMT 2020



Timo Aaltonen pushed to branch upstream at FreeIPA packaging / freeipa-healthcheck


Commits:
02c3b277 by Stanislav Levin at 2020-07-10T09:31:57-04:00
ipa-idns: Handle AAAA RRs

Fixes: https://github.com/freeipa/freeipa-healthcheck/issues/135
Signed-off-by: Stanislav Levin <slev at altlinux.org>

- - - - -
7afcaa3d by Stanislav Levin at 2020-07-10T09:31:57-04:00
tests: Mock AAAA DNS answers

Fixes: https://github.com/freeipa/freeipa-healthcheck/issues/135
Signed-off-by: Stanislav Levin <slev at altlinux.org>

- - - - -
4deeae9b by Stanislav Levin at 2020-07-10T09:31:57-04:00
tests: Update idns expectations

With added AAAA support the number of expected RRs were changed
respectively.

Fixes: https://github.com/freeipa/freeipa-healthcheck/issues/135
Signed-off-by: Stanislav Levin <slev at altlinux.org>

- - - - -
26eaed04 by Stanislav Levin at 2020-07-10T09:32:46-04:00
tests: Don't mark TestRegistry class as Pytest class

Pytest warns about:

"""
tests/test_cluster_ruv.py:15
  /usr/src/RPM/BUILD/freeipa-healthcheck/tests/test_cluster_ruv.py:15: PytestCollectionWarning: cannot collect test class 'TestRegistry' because it has a __init__ constructor (from: tests/test_cluster_ruv.py)
    class TestRegistry(ClusterRegistry):
"""

According to Pytest's naming convention [0]:

> If tests are defined as methods on a class, the class name should start
with “Test”, as in TestExample. The class should not have an __init__
method.

But TestRegistry is not tests class.

[0]: https://docs.pytest.org/en/reorganize-docs/new-docs/user/naming_conventions.html

Fixes: https://github.com/freeipa/freeipa-healthcheck/issues/137
Signed-off-by: Stanislav Levin <slev at altlinux.org>

- - - - -
860ca32a by Stanislav Levin at 2020-07-10T09:32:46-04:00
Fix deprecation warnings about 'installutils.remove_file'

'installutils.remove_file' has been deprecated since freeipa 4.8.

Fixes: https://github.com/freeipa/freeipa-healthcheck/issues/137
Signed-off-by: Stanislav Levin <slev at altlinux.org>

- - - - -
4cb24943 by Rob Crittenden at 2020-07-24T10:39:11-04:00
Require that dirsrv be running to run the IPAMetaCheck

Without it we don't have an LDAP connection to collect the
list of servers.

Signed-off-by: Rob Crittenden <rcritten at redhat.com>

- - - - -
cbd480e7 by Rob Crittenden at 2020-07-28T11:21:59-04:00
Add missing space to msg in IPACertmongerExpirationCheck

The lines are cut off to make the linters happy and a trailing
space was missing jamming together "withgetcert".

- - - - -
e62e05a5 by Rob Crittenden at 2020-07-29T17:07:41-04:00
Allow consuming projects to not use all available options

Some consumers of the framework may not be interested in the
filtering options. Don't fail if some or all of those are not
defined as options. Set logical defaults in this case.

https://github.com/freeipa/freeipa-healthcheck/issues/144

Signed-off-by: Rob Crittenden <rcritten at redhat.com>

- - - - -
84cdc10c by Stanislav Levin at 2020-08-06T10:21:57-04:00
Explicitly convert cert_expiration_days to int

According to docs [0]:
> Supported Datatypes
Config parsers do not guess datatypes of values in configuration files,
always storing them internally as strings. This means that if you need
other datatypes, you should convert on your own.

[0]: https://docs.python.org/3/library/configparser.html#supported-datatypes

Fixes: https://github.com/freeipa/freeipa-healthcheck/issues/146
Signed-off-by: Stanislav Levin <slev at altlinux.org>

- - - - -
ae35e569 by Rob Crittenden at 2020-10-27T15:54:13+01:00
Verify on trusts that the IPA admin users exists with right SID

The admin user is required for trusts and must have the Domain
Admin RID 500.

https://github.com/freeipa/freeipa-healthcheck/issues/120
Signed-off-by: Rob Crittenden <rcritten at redhat.com>

- - - - -
ac323230 by Rob Crittenden at 2020-10-27T16:21:09+01:00
Add check to verify that IPA server certs have a SAN

If a CA is installed also verify that the Apache cert has a
SAN for ipa-ca.$DOMAIN for ACME support.

https://github.com/freeipa/freeipa-healthcheck/issues/112

Signed-off-by: Rob Crittenden <rcritten at redhat.com>

- - - - -
94ca4256 by Rob Crittenden at 2020-11-02T13:04:09-05:00
Mock the ipa-ca resolution for the DNS system records check

IPA uses the resolver to look up the IP addresses for those
servers with a CA to ensure that the ipa-ca SAN will work
properly.

Mock this call and return our own rrsets.

- - - - -
2d49f118 by Rob Crittenden at 2020-11-02T13:04:09-05:00
Run Travis CI against Fedora 32 and 33 instead of 30 and 31.

- - - - -
c887d6db by Rob Crittenden at 2020-11-02T13:04:09-05:00
dnspython >= 2.0.0 changed the Answer API, generate it by version

We just need an empty Answer class to push in our fake data. Generate
it based on the version of dnspython.

- - - - -
3e95470d by Rob Crittenden at 2020-11-02T14:38:58-05:00
Don't collect the list of masters in MetaCheck

It is already collected in IPAMetaCheck.

Signed-off-by: Rob Crittenden <rcritten at redhat.com>

- - - - -
508f6987 by Rob Crittenden at 2020-11-02T14:38:58-05:00
Report on FIPS status: enabled, disabled, inconsistent, etc.

If fips-mode-setup is installed then we can check the status
otherwise report the missing binary.

The script provides 3 possible return values:
0 enabled
1 inconsistent
2 disabled

These are handled along with a catch-all "unknown"

https://github.com/freeipa/freeipa-healthcheck/issues/105

Signed-off-by: Rob Crittenden <rcritten at redhat.com>

- - - - -
c3539b60 by Rob Crittenden at 2020-11-02T14:38:58-05:00
Test the fips mode check in the MetaCheck plugin

https://github.com/freeipa/freeipa-healthcheck/issues/105

Signed-off-by: Rob Crittenden <rcritten at redhat.com>

- - - - -
94ab15ca by Rob Crittenden at 2020-11-02T14:38:58-05:00
Work around the FIPS_MODE_SETUP not present in all releases

If it's present in ipaplatform.paths then use it, otherwise
use the Fedora/RHEL default of /usr/bin/fips-mode-setup

https://github.com/freeipa/freeipa-healthcheck/issues/105

Signed-off-by: Rob Crittenden <rcritten at redhat.com>

- - - - -
de4d26f9 by Rob Crittenden at 2020-11-03T14:29:20-05:00
Support an additional trust type, don't rely on strings for type

ipaserver/dcerpc_common.py defines the possible trust types:

_trust_type_dict = {
 1: _('Non-Active Directory domain'),
 2: _('Active Directory domain'),
 3: _('RFC4120-compliant Kerberos realm'),
 10: _('Non-transitive external trust to a domain in '
       'another Active Directory forest'),
 11: _('Non-transitive external trust to an RFC4120-'
       'compliant Kerberos realm')
}

In practice, only '2' and '10' can appear as trust types right now.
The other three describe possible options for the attributes on AD
side. When IPA-IPA trust will be added, it will be seen as '2'.

Since this is a translated value use ipanttrusttype instead of
trusttype.

https://bugzilla.redhat.com/show_bug.cgi?id=1891505

Signed-off-by: Rob Crittenden <rcritten at redhat.com>

- - - - -
30d97eff by Rob Crittenden at 2020-11-03T14:29:46-05:00
Work with existing resolve_rrsets and newer resolve_rrsets_nss

Up to freeipa 4.8.9 resolve_rrsets is used to look up the
ipa-ca values. After that, and in master, resovle_rrsets_nss
is used instead. Handle both in the DNS mock testing.

- - - - -
f97efcd1 by Rob Crittenden at 2020-11-03T14:30:57-05:00
Become 0.7

- - - - -


21 changed files:

- .travis.yml
- man/man5/ipahealthcheck.conf.5
- setup.py
- src/ipahealthcheck/core/output.py
- src/ipahealthcheck/ipa/certs.py
- src/ipahealthcheck/ipa/host.py
- src/ipahealthcheck/ipa/idns.py
- src/ipahealthcheck/ipa/meta.py
- src/ipahealthcheck/ipa/trust.py
- src/ipahealthcheck/meta/core.py
- tests/mock_certmonger.py
- tests/test_cluster_ruv.py
- + tests/test_init.py
- tests/test_ipa_certfile_expiration.py
- tests/test_ipa_dns.py
- + tests/test_ipa_dnssan.py
- tests/test_ipa_expiration.py
- tests/test_ipa_tracking.py
- tests/test_ipa_trust.py
- + tests/test_meta.py
- tests/util.py


Changes:

=====================================
.travis.yml
=====================================
@@ -13,8 +13,8 @@ cache: pip
 env:
     matrix:
         - TASK_TO_RUN="lint"
-        - TASK_TO_RUN="fedora:30"
-        - TASK_TO_RUN="fedora:31"
+        - TASK_TO_RUN="fedora:32"
+        - TASK_TO_RUN="fedora:33"
 
 install:
     - pip3 install --upgrade pip


=====================================
man/man5/ipahealthcheck.conf.5
=====================================
@@ -40,5 +40,13 @@ The number of days left before a certificate expires to start displaying a warni
 .TP
 .I /etc/ipahealthcheck/ipahealthcheck.conf
 configuration file
+
+.SH "EXAMPLES"
+.TP
+7 days left before a certificate expires to start displaying a warning:
+
+ [default]
+ cert_expiration_days=7
+
 .SH "SEE ALSO"
 .BR ipa-healthcheck (8)


=====================================
setup.py
=====================================
@@ -3,7 +3,7 @@ from setuptools import find_packages, setup
 
 setup(
     name='ipahealthcheck',
-    version='0.6',
+    version='0.7',
     namespace_packages=['ipahealthcheck', 'ipaclustercheck'],
     package_dir={'': 'src'},
     # packages=find_packages(where='src'),


=====================================
src/ipahealthcheck/core/output.py
=====================================
@@ -37,9 +37,19 @@ class Output:
     """
     def __init__(self, options):
         self.filename = options.outfile
-        self.failures_only = options.failures_only
-        self.all = options.all
-        self.severity = options.severity
+
+        # Non-required options in the framework, set logical defaults to
+        # pre 0.6 behavior with everything reported.
+        self.severity = None
+        self.failures_only = False
+        self.all = True
+
+        if 'failures_only' in options:
+            self.failures_only = options.failures_only
+        if 'all' in options:
+            self.all = options.all
+        if 'severity' in options:
+            self.severity = options.severity
 
     def render(self, results):
         """Process the results into output"""


=====================================
src/ipahealthcheck/ipa/certs.py
=====================================
@@ -6,6 +6,7 @@ from datetime import datetime, timezone, timedelta
 import itertools
 import logging
 import os
+import socket
 import tempfile
 
 from ipahealthcheck.ipa.plugin import IPAPlugin, registry
@@ -17,13 +18,12 @@ from ipalib import api
 from ipalib import errors
 from ipalib import x509
 from ipalib.install import certmonger
-from ipalib.constants import RENEWAL_CA_NAME
+from ipalib.constants import RENEWAL_CA_NAME, IPA_CA_RECORD
 from ipaplatform.paths import paths
 from ipaserver.install import certs
 from ipaserver.install import dsinstance
 from ipaserver.install import krainstance
 from ipaserver.install import krbinstance
-from ipaserver.install import installutils
 from ipaserver.plugins import ldap2
 from ipapython import certdb
 from ipapython import ipautil
@@ -208,14 +208,14 @@ class IPACertmongerExpirationCheck(IPAPlugin):
             else:
                 delta = nafter - now
                 diff = int(delta.total_seconds() / DAY)
-                if diff < self.config.cert_expiration_days:
+                if diff < int(self.config.cert_expiration_days):
                     yield Result(self, constants.WARNING,
                                  key=id,
                                  expiration_date=generalized_time(nafter),
                                  days=diff,
                                  msg='Request id {key} expires in {days} '
                                      'days. certmonger should renew this '
-                                     'automatically. Watch the status with'
+                                     'automatically. Watch the status with '
                                      'getcert list -i {key}.')
                 else:
                     yield Result(self, constants.SUCCESS,
@@ -309,7 +309,7 @@ class IPACertfileExpirationCheck(IPAPlugin):
 
             delta = notafter - now
             diff = int(delta.total_seconds() / DAY)
-            if diff < self.config.cert_expiration_days:
+            if diff < int(self.config.cert_expiration_days):
                 yield Result(self, constants.WARNING,
                              key=id,
                              expiration_date=generalized_time(notafter),
@@ -394,6 +394,111 @@ class IPACertTracking(IPAPlugin):
                                  'is not expected on an IPA master.')
 
 
+ at registry
+class IPACertDNSSAN(IPAPlugin):
+    """Check whether a IPA-issued certificates have a SAN configured
+
+       Steps:
+       1. Collect all expected certificates into `requests`
+       2. Iterate over the list of certificates
+       3. If issued by IPA and a caIPAserviceCert then verify that
+          the host FQDN is in the list of SAN
+       4. If a CA is configured on this host then also verify that
+          ipa-ca.$DOMAIN is in the SAN.
+    """
+
+    requires = ('dirsrv',)
+
+    @duration
+    def check(self):
+        fqdn = socket.getfqdn()
+        requests = get_expected_requests(self.ca, self.ds, self.serverid)
+
+        for request in requests:
+            request_id = certmonger.get_request_id(request)
+            if request_id is None:
+                yield Result(self, constants.ERROR,
+                             key=request_id,
+                             msg='Found request id {key} but it is not tracked'
+                                 'by certmonger!?')
+                continue
+
+            ca_name = certmonger.get_request_value(request_id, 'ca-name')
+            if ca_name != 'IPA':
+                logger.debug('Skipping request %s with CA %s',
+                             request_id, ca_name)
+                continue
+            profile = certmonger.get_request_value(request_id,
+                                                   'template_profile')
+            if profile != 'caIPAserviceCert':
+                logger.debug('Skipping request %s with profile %s',
+                             request_id, profile)
+                continue
+
+            certfile = None
+            if request.get('cert-file') is not None:
+                certfile = request.get('cert-file')
+                try:
+                    cert = x509.load_certificate_from_file(certfile)
+                except Exception as e:
+                    yield Result(self, constants.ERROR,
+                                 key=request_id,
+                                 certfile=certfile,
+                                 error=str(e),
+                                 msg='Unable to open cert file {certfile}: '
+                                     '{error}')
+                    continue
+            elif request.get('cert-database') is not None:
+                nickname = request.get('cert-nickname')
+                dbdir = request.get('cert-database')
+                try:
+                    db = certdb.NSSDatabase(dbdir)
+                except Exception as e:
+                    yield Result(self, constants.ERROR,
+                                 key=request_id,
+                                 dbdir=dbdir,
+                                 error=str(e),
+                                 msg='Unable to open NSS database {dbdir}: '
+                                     '{error}')
+                    continue
+                try:
+                    cert = db.get_cert(nickname)
+                except Exception as e:
+                    yield Result(self, constants.ERROR,
+                                 key=id,
+                                 dbdir=dbdir,
+                                 nickname=nickname,
+                                 error=str(e),
+                                 msg='Unable to retrieve certificate '
+                                     '\'{nickname}\' from {dbdir}: {error}')
+                    continue
+
+            hostlist = [fqdn]
+            if self.ca.is_configured() and certfile == paths.HTTPD_CERT_FILE:
+                hostlist.append(f'{IPA_CA_RECORD}.{api.env.domain}')
+            error = False
+            for host in hostlist:
+                if host not in cert.san_a_label_dns_names:
+                    error = True
+                    yield Result(self, constants.ERROR,
+                                 key=request_id,
+                                 hostname=host,
+                                 san=cert.san_a_label_dns_names,
+                                 ca=ca_name,
+                                 profile=profile,
+                                 msg='Certificate request id {key} with '
+                                     'profile {profile} for CA {ca} does not '
+                                     'have a DNS SAN {san} matching name '
+                                     '{hostname}')
+            if not error:
+                yield Result(self, constants.SUCCESS,
+                             key=request_id,
+                             hostname=hostlist,
+                             san=cert.san_a_label_dns_names,
+                             ca=ca_name,
+                             profile=profile)
+
+
 @registry
 class IPACertNSSTrust(IPAPlugin):
     """Compare the NSS trust for the CA certs to a known good value"""
@@ -550,7 +655,7 @@ class IPANSSChainValidation(IPAPlugin):
                                      key=key)
         finally:
             if ca_pw_fname:
-                installutils.remove_file(ca_pw_fname)
+                ipautil.remove_file(ca_pw_fname)
 
 
 @registry
@@ -876,7 +981,7 @@ class IPACAChainExpirationCheck(IPAPlugin):
             return
 
         now = datetime.now(timezone.utc)
-        soon = now + timedelta(days=self.config.cert_expiration_days)
+        soon = now + timedelta(days=int(self.config.cert_expiration_days))
         for cert in ca_certs:
             subject = DN(cert.subject)
             subject = str(subject).replace('\\;', '\\3b')


=====================================
src/ipahealthcheck/ipa/host.py
=====================================
@@ -14,7 +14,7 @@ from ipahealthcheck.core import constants
 from ipalib import api
 from ipalib.install.kinit import kinit_keytab
 from ipaplatform.paths import paths
-from ipaserver.install import installutils
+from ipapython import ipautil
 
 
 logger = logging.getLogger()
@@ -38,5 +38,5 @@ class IPAHostKeytab(IPAPlugin):
                 yield Result(self, constants.ERROR,
                              msg='Failed to obtain host TGT: %s' % e)
         finally:
-            installutils.remove_file(ccache_name)
+            ipautil.remove_file(ccache_name)
             os.rmdir(ccache_dir)


=====================================
src/ipahealthcheck/ipa/idns.py
=====================================
@@ -44,6 +44,7 @@ class IPADNSSystemRecordsCheck(IPAPlugin):
         txt_rec = dict()
         srv_rec = dict()
         a_rec = list()
+        aaaa_rec = list()
 
         for name, node in base_records.items():
             for rdataset in node:
@@ -60,6 +61,8 @@ class IPADNSSystemRecordsCheck(IPAPlugin):
                             txt_rec[name.ToASCII()] = [rd.to_text()]
                     elif rd.rdtype == rdatatype.A:
                         a_rec.append(rd.to_text())
+                    elif rd.rdtype == rdatatype.AAAA:
+                        aaaa_rec.append(rd.to_text())
                     else:
                         logger.error("Unhandler rdtype %d", rd.rdtype)
 
@@ -113,38 +116,70 @@ class IPADNSSystemRecordsCheck(IPAPlugin):
                                  key=realm,
                                  msg='expected realm missing')
 
-        # Look up the ipa-ca records
-        qname = "ipa-ca." + api.env.domain + "."
-        logger.debug("Search DNS for A record of %s", qname)
+        if a_rec:
+            # Look up the ipa-ca records
+            qname = "ipa-ca." + api.env.domain + "."
+            logger.debug("Search DNS for A record of %s", qname)
+            try:
+                answers = resolver.query(qname, rdatatype.A)
+            except DNSException as e:
+                logger.debug("DNS record not found: %s", e.__class__.__name__)
+                answers = []
+
+            for answer in answers:
+                logger.debug("DNS record found: %s", answer)
+                ipaddr = answer.to_text()
+                try:
+                    yield Result(self, constants.SUCCESS,
+                                 key=ipaddr)
+                except ValueError:
+                    yield Result(self, constants.WARNING,
+                                 key=ipaddr,
+                                 msg='expected ipa-ca IPv4 address missing')
 
-        try:
-            answers = resolver.query(qname, rdatatype.A)
-        except DNSException as e:
-            logger.debug("DNS record not found: %s", e.__class__.__name__)
-            answers = []
+            ca_count = 0
+            for server in system_records.servers_data:
+                master = system_records.servers_data.get(server)
+                if 'CA server' in master.get('roles'):
+                    ca_count += 1
 
-        hosts = a_rec
-        for answer in answers:
-            logger.debug("DNS record found: %s", answer)
-            ipaddr = answer.to_text()
+            if len(answers) != ca_count:
+                yield Result(
+                    self, constants.WARNING,
+                    msg='Got {count} ipa-ca A records, expected {expected}',
+                    count=len(answers),
+                    expected=ca_count)
+
+        if aaaa_rec:
+            # Look up the ipa-ca records
+            qname = "ipa-ca." + api.env.domain + "."
+            logger.debug("Search DNS for AAAA record of %s", qname)
             try:
-                a_rec.remove(ipaddr)
-                yield Result(self, constants.SUCCESS,
-                             key=ipaddr)
-            except ValueError:
-                yield Result(self, constants.WARNING,
-                             key=ipaddr,
-                             msg='expected ipa-ca IPAddr missing')
-
-        ca_count = 0
-        for server in system_records.servers_data:
-            master = system_records.servers_data.get(server)
-            if 'CA server' in master.get('roles'):
-                ca_count += 1
-
-        if len(answers) != ca_count:
-            yield Result(
-                self, constants.WARNING,
-                msg='Got {count} ipa-ca A records, expected {expected}',
-                count=len(answers),
-                expected=ca_count)
+                answers = resolver.query(qname, rdatatype.AAAA)
+            except DNSException as e:
+                logger.debug("DNS record not found: %s", e.__class__.__name__)
+                answers = []
+
+            for answer in answers:
+                logger.debug("DNS record found: %s", answer)
+                ipaddr = answer.to_text()
+                try:
+                    yield Result(self, constants.SUCCESS,
+                                 key=ipaddr)
+                except ValueError:
+                    yield Result(self, constants.WARNING,
+                                 key=ipaddr,
+                                 msg='expected ipa-ca IPv6 address missing')
+
+            ca_count = 0
+            for server in system_records.servers_data:
+                master = system_records.servers_data.get(server)
+                if 'CA server' in master.get('roles'):
+                    ca_count += 1
+
+            if len(answers) != ca_count:
+                yield Result(
+                    self, constants.WARNING,
+                    msg='Got {count} ipa-ca AAAA records, expected {expected}',
+                    count=len(answers),
+                    expected=ca_count)


=====================================
src/ipahealthcheck/ipa/meta.py
=====================================
@@ -11,6 +11,8 @@ from ipalib import api
 @registry
 class IPAMetaCheck(IPAPlugin):
     """Return meta data for the IPA installation"""
+    requires = ('dirsrv',)
+
     @duration
     def check(self):
         try:


=====================================
src/ipahealthcheck/ipa/trust.py
=====================================
@@ -15,6 +15,10 @@ from ipalib import api
 from ipaplatform.paths import paths
 from ipapython import ipautil
 from ipapython.dn import DN
+from ipaserver.dcerpc_common import (
+    trust_type_string,
+    _trust_type_dict_unknown
+)
 
 try:
     import pysss_nss_idmap
@@ -42,16 +46,22 @@ def get_trust_domains():
 
     Each entry is a dictionary representating an AD domain.
     """
-    result = api.Command.trust_find()
+    result = api.Command.trust_find(all=True, raw=True)
     results = result['result']
     trust_domains = []
     for result in results:
-        if result.get('trusttype')[0] == 'Active Directory domain':
+        attributes = int(result.get('ipanttrustattributes', [0])[0])
+        if (
+            trust_type_string(result.get('ipanttrusttype')[0], attributes) !=
+            _trust_type_dict_unknown
+        ):
             domain = dict()
             domain['domain'] = result.get('cn')[0]
             domain['domainsid'] = result.get('ipanttrusteddomainsid')[0]
             domain['netbios'] = result.get('ipantflatname')[0]
             trust_domains.append(domain)
+        else:
+            logger.debug('Unhandled trust type %s', _trust_type_dict_unknown)
     return trust_domains
 
 
@@ -586,6 +596,44 @@ class IPATrustControllerGroupSIDCheck(IPAPlugin):
                          key='ipantsecurityidentifier')
 
 
+ at registry
+class IPATrustControllerAdminSIDCheck(IPAPlugin):
+    """
+    Verify that the admin user's SID ends with 500
+    """
+    @duration
+    def check(self):
+        if not self.registry.trust_controller:
+            logger.debug('Not a trust controller, skipping')
+            return
+
+        admin_dn = DN(('uid', 'admin'),
+                      api.env.container_user, api.env.basedn)
+
+        try:
+            entry = self.conn.get_entry(
+                admin_dn,
+                attrs_list=['ipantsecurityidentifier'])
+        except Exception as e:
+            yield Result(self, constants.ERROR,
+                         key=str(admin_dn),
+                         error=str(e),
+                         msg='Error retrieving the admin user at {key}: '
+                         '{error}')
+            return
+
+        identifier = entry.get('ipantsecurityidentifier', [None])[0]
+        if not identifier or not identifier.endswith('500'):
+            yield Result(self, constants.ERROR,
+                         key='ipantsecurityidentifier',
+                         rid=identifier,
+                         msg='{key} is not a Domain Admin RID')
+        else:
+            yield Result(self, constants.SUCCESS,
+                         rid=identifier,
+                         key='ipantsecurityidentifier')
+
+
 @registry
 class IPATrustPackageCheck(IPAPlugin):
     """


=====================================
src/ipahealthcheck/meta/core.py
=====================================
@@ -2,26 +2,55 @@
 # Copyright (C) 2019 FreeIPA Contributors see COPYING for license
 #
 
+import logging
+import os
 import socket
 from ipahealthcheck.core import constants
 from ipahealthcheck.core.plugin import Result, duration
 from ipahealthcheck.meta.plugin import Plugin, registry
+from ipapython import ipautil
 from ipapython.version import VERSION, API_VERSION
-from ipapython.dn import DN
-from ipalib import api
+from ipaplatform.paths import paths
+
+if 'FIPS_MODE_SETUP' not in dir(paths):
+    paths.FIPS_MODE_SETUP = '/usr/bin/fips-mode-setup'
+
+logger = logging.getLogger()
 
 
 @registry
 class MetaCheck(Plugin):
     @duration
     def check(self):
-        conn = api.Backend.ldap2
-        masters_dn = DN(api.env.container_masters, api.env.basedn)
-        masters = conn.get_entries(masters_dn, conn.SCOPE_ONELEVEL)
-        known = [master.single_value['cn'] for master in masters]
 
-        yield Result(self, constants.SUCCESS,
+        rval = constants.SUCCESS
+        if not os.path.exists(paths.FIPS_MODE_SETUP):
+            fips = "missing {}".format(paths.FIPS_MODE_SETUP)
+            logger.debug('%s is not installed, skipping',
+                         paths.FIPS_MODE_SETUP)
+        else:
+            try:
+                result = ipautil.run([paths.FIPS_MODE_SETUP,
+                                      '--is-enabled'],
+                                     capture_output=True,
+                                     raiseonerr=False,)
+            except Exception as e:
+                logger.debug('fips-mode-setup failed: %s', e)
+                fips = "failed to check"
+                rval = constants.ERROR
+            else:
+                logger.debug(result.raw_output.decode('utf-8'))
+                if result.returncode == 0:
+                    fips = "enabled"
+                elif result.returncode == 1:
+                    fips = "inconsistent"
+                elif result.returncode == 2:
+                    fips = "disabled"
+                else:
+                    fips = "unknown"
+
+        yield Result(self, rval,
                      fqdn=socket.getfqdn(),
-                     masters=known,
+                     fips=fips,
                      ipa_version=VERSION,
                      ipa_api_version=API_VERSION,)


=====================================
tests/mock_certmonger.py
=====================================
@@ -16,6 +16,7 @@ pristine_cm_requests = [
         'cert-file': paths.RA_AGENT_PEM,
         'key-file': paths.RA_AGENT_KEY,
         'ca-name': 'dogtag-ipa-ca-renew-agent',
+        'template_profile': 'caSubsystemCert',
         'cert-storage': 'FILE',
         'cert-presave-command': template % 'renew_ra_cert_pre',
         'cert-postsave-command': template % 'renew_ra_cert',
@@ -26,6 +27,7 @@ pristine_cm_requests = [
         'cert-file': paths.HTTPD_CERT_FILE,
         'key-file': paths.HTTPD_KEY_FILE,
         'ca-name': 'IPA',
+        'template_profile': 'caIPAserviceCert',
         'cert-storage': 'FILE',
         'cert-postsave-command': template % 'restart_httpd',
         'not-valid-after': 1607204930,


=====================================
tests/test_cluster_ruv.py
=====================================
@@ -12,7 +12,7 @@ from ipaclustercheck.ipa.ruv import ClusterRUVCheck
 import clusterdata
 
 
-class TestRegistry(ClusterRegistry):
+class RUVRegistry(ClusterRegistry):
     def load_files(self, dir):
         self.json = dir
 
@@ -26,7 +26,7 @@ class Options:
         return self.data
 
 
-registry = TestRegistry()
+registry = RUVRegistry()
 
 
 class TestClusterRUV(BaseTest):


=====================================
tests/test_init.py
=====================================
@@ -0,0 +1,28 @@
+#
+# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
+#
+
+import argparse
+
+from ipahealthcheck.core.output import output_registry
+
+
+class RunChecks:
+    def run_healthcheck(self):
+        options = argparse.Namespace(check=None, debug=False, indent=2,
+                                     list_sources=False, outfile=None,
+                                     output='json', source=None,
+                                     verbose=False)
+
+        for out in output_registry.plugins:
+            if out.__name__.lower() == options.output:
+                out(options)
+                break
+
+
+def test_run_healthcheck():
+    """
+    Test typical initialization in run_healthcheck (based ok pki-healthcheck)
+    """
+    run = RunChecks()
+    run.run_healthcheck()


=====================================
tests/test_ipa_certfile_expiration.py
=====================================
@@ -44,7 +44,7 @@ class TestIPACertificateFile(BaseTest):
         registry.initialize(framework, config.Config)
         f = IPACertfileExpirationCheck(registry)
 
-        f.config.cert_expiration_days = 28
+        f.config.cert_expiration_days = '28'
         self.results = capture_results(f)
 
         assert len(self.results) == 1
@@ -67,7 +67,7 @@ class TestIPACertificateFile(BaseTest):
         registry.initialize(framework, config.Config)
         f = IPACertfileExpirationCheck(registry)
 
-        f.config.cert_expiration_days = 30
+        f.config.cert_expiration_days = '30'
         self.results = capture_results(f)
 
         assert len(self.results) == 1
@@ -91,7 +91,7 @@ class TestIPACertificateFile(BaseTest):
         registry.initialize(framework, config.Config)
         f = IPACertfileExpirationCheck(registry)
 
-        f.config.cert_expiration_days = 30
+        f.config.cert_expiration_days = '30'
         self.results = capture_results(f)
 
         assert len(self.results) == 1


=====================================
tests/test_ipa_dns.py
=====================================
@@ -1,6 +1,7 @@
 #
 # Copyright (C) 2019 FreeIPA Contributors see COPYING for license
 #
+import re
 
 from dns import (
     rdata,
@@ -8,8 +9,9 @@ from dns import (
     rdatatype,
     message,
     rrset,
+    version,
 )
-from dns.resolver import Answer, NoAnswer
+from dns.resolver import Answer
 
 from base import BaseTest
 from util import capture_results, m_api
@@ -25,6 +27,13 @@ from ipaserver.dns_data_management import (
     IPA_DEFAULT_ADTRUST_SRV_REC
 )
 
+try:
+    from ipaserver.install.installutils import resolve_rrsets_nss  # noqa: F401
+except ImportError:
+    resolve_rrsets_import = 'ipaserver.dns_data_management.resolve_rrsets'
+else:
+    resolve_rrsets_import = 'ipaserver.install.installutils.resolve_rrsets_nss'
+
 
 def add_srv_records(qname, port_map, priority=0, weight=100):
     rdlist = []
@@ -43,6 +52,19 @@ def add_srv_records(qname, port_map, priority=0, weight=100):
     return rdlist
 
 
+def resolve_rrsets(fqdn, rdtypes):
+    """
+    Return an A record for the hostname in an RRset type in a list.
+    """
+    rset = []
+    for rdtype in rdtypes:
+        rlist = rrset.from_text_list(fqdn, 86400, rdataclass.IN,
+                                     rdtype, gen_addrs(rdtype, 1))
+        rset.append(rlist)
+
+    return rset
+
+
 def query_srv(qname, ad_records=False):
     """
     Return a SRV for each service IPA cares about for all the hosts.
@@ -55,25 +77,16 @@ def query_srv(qname, ad_records=False):
     return rdlist
 
 
-class Response:
-    """Fake class so that a DNS NoAnswer can be raised"""
-    def __init__(self, question=None):
-        self.question = question
-
-    @property
-    def question(self):
-        return self.__question
-
-    @question.setter
-    def question(self, question):
-        self.__question = question
-
-
-def gen_addrs(num):
+def gen_addrs(rdtype=rdatatype.A, num=1):
     """Generate sequential IP addresses for the ipa-ca A record lookup"""
     ips = []
+    if rdtype == rdatatype.A:
+        ip_template = '192.168.0.%d'
+    if rdtype == rdatatype.AAAA:
+        ip_template = '2001:db8:1::%d'
+
     for i in range(num):
-        ips.append('192.168.0.%d' % (i + 1))
+        ips.append(ip_template % (i + 1))
 
     return ips
 
@@ -83,30 +96,30 @@ def fake_query(qname, rdtype=rdatatype.A, rdclass=rdataclass.IN, count=1,
     """Fake a DNS query, returning count responses to the request
 
        Three kinds of lookups are faked:
-       1. A query for A records for a service will return the count
+       1. A query for A/AAAA records for a service will return the count
           as requested in the test. This simulates lookups for the
-          ipa-ca A record. To force a difference in responses one can
+          ipa-ca A/AAAA record. To force a difference in responses one can
           vary the count.
-       2. AAAA records are not yet supported, return no answer
-       3. TXT queries will return the Kerberos realm
+       2. TXT queries will return the Kerberos realm
 
        fake_txt will set an invalid Kerberos realm entry to provoke a
        warning.
     """
     m = message.Message()
-    if rdtype == rdatatype.A:
+    if rdtype in (rdatatype.A, rdatatype.AAAA):
         fqdn = DNSName(qname)
         fqdn = fqdn.make_absolute()
 
-        answers = Answer(fqdn, rdataclass.IN, rdatatype.A, m,
-                         raise_on_no_answer=False)
+        if version.MAJOR < 2:
+            answers = Answer(fqdn, rdataclass.IN, rdtype, m,
+                             raise_on_no_answer=False)
+        else:
+            answers = Answer(fqdn, rdataclass.IN, rdtype, m)
 
         rlist = rrset.from_text_list(fqdn, 86400, rdataclass.IN,
-                                     rdatatype.A, gen_addrs(count))
+                                     rdtype, gen_addrs(rdtype, count))
 
         answers.rrset = rlist
-    elif rdtype == rdatatype.AAAA:
-        raise NoAnswer(response=Response('no AAAA'))
     elif rdtype == rdatatype.TXT:
         if fake_txt:
             realm = 'FAKE_REALM'
@@ -115,8 +128,11 @@ def fake_query(qname, rdtype=rdatatype.A, rdclass=rdataclass.IN, count=1,
         qname = DNSName('_kerberos.' + m_api.env.domain)
         qname = qname.make_absolute()
 
-        answers = Answer(qname, rdataclass.IN, rdatatype.TXT, m,
-                         raise_on_no_answer=False)
+        if version.MAJOR < 2:
+            answers = Answer(qname, rdataclass.IN, rdatatype.TXT, m,
+                             raise_on_no_answer=False)
+        else:
+            answers = Answer(qname, rdataclass.IN, rdatatype.TXT, m)
 
         rlist = rrset.from_text_list(qname, 86400, rdataclass.IN,
                                      rdatatype.TXT, [realm])
@@ -173,12 +189,16 @@ class TestDNSSystemRecords(BaseTest):
        2. fake_query() overrides dns.resolver.query to simulate
           A, AAAA and TXT record lookups.
     """
+    @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
     @patch('dns.resolver.query')
-    def test_dnsrecords_single(self, mock_query, mock_query_srv):
+    def test_dnsrecords_single(self, mock_query, mock_query_srv, mock_rrset):
         """Test single CA master, all SRV records"""
         mock_query.side_effect = fake_query_one
         mock_query_srv.side_effect = query_srv([m_api.env.host])
+        mock_rrset.side_effect = [
+            resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA))
+        ]
 
         m_api.Command.server_find.side_effect = [{
             'result': [
@@ -197,22 +217,28 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 9
+        assert len(self.results) == 10
 
         for result in self.results.results:
             assert result.result == constants.SUCCESS
             assert result.source == 'ipahealthcheck.ipa.idns'
             assert result.check == 'IPADNSSystemRecordsCheck'
 
+    @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
     @patch('dns.resolver.query')
-    def test_dnsrecords_two(self, mock_query, mock_query_srv):
+    def test_dnsrecords_two(self, mock_query, mock_query_srv, mock_rrset):
         """Test two CA masters, all SRV records"""
         mock_query_srv.side_effect = query_srv([
             m_api.env.host,
             'replica.' + m_api.env.domain
         ])
         mock_query.side_effect = fake_query_two
+        mock_rrset.side_effect = [
+            resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
+            resolve_rrsets('replica.' + m_api.env.domain,
+                           (rdatatype.A, rdatatype.AAAA)),
+        ]
 
         m_api.Command.server_find.side_effect = [{
             'result': [
@@ -239,16 +265,17 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 17
+        assert len(self.results) == 19
 
         for result in self.results.results:
             assert result.result == constants.SUCCESS
             assert result.source == 'ipahealthcheck.ipa.idns'
             assert result.check == 'IPADNSSystemRecordsCheck'
 
+    @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
     @patch('dns.resolver.query')
-    def test_dnsrecords_three(self, mock_query, mock_query_srv):
+    def test_dnsrecords_three(self, mock_query, mock_query_srv, mock_rrset):
         """Test three CA masters, all SRV records"""
         mock_query_srv.side_effect = query_srv([
             m_api.env.host,
@@ -256,6 +283,13 @@ class TestDNSSystemRecords(BaseTest):
             'replica2.' + m_api.env.domain
         ])
         mock_query.side_effect = fake_query_three
+        mock_rrset.side_effect = [
+            resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
+            resolve_rrsets('replica.' + m_api.env.domain,
+                           (rdatatype.A, rdatatype.AAAA)),
+            resolve_rrsets('replica2.' + m_api.env.domain,
+                           (rdatatype.A, rdatatype.AAAA)),
+        ]
 
         m_api.Command.server_find.side_effect = [{
             'result': [
@@ -289,16 +323,18 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 25
+        assert len(self.results) == 28
 
         for result in self.results.results:
             assert result.result == constants.SUCCESS
             assert result.source == 'ipahealthcheck.ipa.idns'
             assert result.check == 'IPADNSSystemRecordsCheck'
 
+    @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
     @patch('dns.resolver.query')
-    def test_dnsrecords_three_mixed(self, mock_query, mock_query_srv):
+    def test_dnsrecords_three_mixed(self, mock_query, mock_query_srv,
+                                    mock_rrset):
         """Test three masters, only one with a CA, all SRV records"""
         mock_query_srv.side_effect = query_srv([
             m_api.env.host,
@@ -306,6 +342,13 @@ class TestDNSSystemRecords(BaseTest):
             'replica2.' + m_api.env.domain
         ])
         mock_query.side_effect = fake_query_one
+        mock_rrset.side_effect = [
+            resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
+            resolve_rrsets('replica.' + m_api.env.domain,
+                           (rdatatype.A, rdatatype.AAAA)),
+            resolve_rrsets('replica2.' + m_api.env.domain,
+                           (rdatatype.A, rdatatype.AAAA))
+        ]
 
         m_api.Command.server_find.side_effect = [{
             'result': [
@@ -337,15 +380,17 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 23
+        assert len(self.results) == 24
 
         for result in self.results.results:
             assert result.result == constants.SUCCESS
             assert result.source == 'ipahealthcheck.ipa.idns'
 
+    @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
     @patch('dns.resolver.query')
-    def test_dnsrecords_missing_server(self, mock_query, mock_query_srv):
+    def test_dnsrecords_missing_server(self, mock_query, mock_query_srv,
+                                       mock_rrset):
         """Drop one of the masters from query_srv
 
            This will simulate missing SRV records and cause a number of
@@ -357,6 +402,13 @@ class TestDNSSystemRecords(BaseTest):
             # replica2 is missing
         ])
         mock_query.side_effect = fake_query_three
+        mock_rrset.side_effect = [
+            resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
+            resolve_rrsets('replica.' + m_api.env.domain,
+                           (rdatatype.A, rdatatype.AAAA)),
+            resolve_rrsets('replica2.' + m_api.env.domain,
+                           (rdatatype.A, rdatatype.AAAA)),
+        ]
 
         m_api.Command.server_find.side_effect = [{
             'result': [
@@ -390,19 +442,21 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 25
+        assert len(self.results) == 28
 
         ok = get_results_by_severity(self.results.results, constants.SUCCESS)
         warn = get_results_by_severity(self.results.results, constants.WARNING)
-        assert len(ok) == 18
+        assert len(ok) == 21
         assert len(warn) == 7
 
         for result in warn:
             assert result.kw.get('msg') == 'Expected SRV record missing'
 
+    @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
     @patch('dns.resolver.query')
-    def test_dnsrecords_missing_ipa_ca(self, mock_query, mock_query_srv):
+    def test_dnsrecords_missing_ipa_ca(self, mock_query, mock_query_srv,
+                                       mock_rrset):
         """Drop one of the masters from query_srv
 
            This will simulate missing SRV records and cause a number of
@@ -414,6 +468,13 @@ class TestDNSSystemRecords(BaseTest):
             'replica2.' + m_api.env.domain
         ])
         mock_query.side_effect = fake_query_two
+        mock_rrset.side_effect = [
+            resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
+            resolve_rrsets('replica.' + m_api.env.domain,
+                           (rdatatype.A, rdatatype.AAAA)),
+            resolve_rrsets('replica2.' + m_api.env.domain,
+                           (rdatatype.A, rdatatype.AAAA))
+        ]
 
         m_api.Command.server_find.side_effect = [{
             'result': [
@@ -447,22 +508,26 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 25
+        assert len(self.results) == 28
 
         ok = get_results_by_severity(self.results.results, constants.SUCCESS)
         warn = get_results_by_severity(self.results.results, constants.WARNING)
-        assert len(ok) == 24
-        assert len(warn) == 1
+        assert len(ok) == 26
+        assert len(warn) == 2
 
         for result in warn:
-            assert result.kw.get('msg') == \
-                'Got {count} ipa-ca A records, expected {expected}'
+            assert re.match(
+                r'^Got {count} ipa-ca (A|AAAA) records, expected {expected}$',
+                result.kw.get('msg')
+            )
             assert result.kw.get('count') == 2
             assert result.kw.get('expected') == 3
 
+    @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
     @patch('dns.resolver.query')
-    def test_dnsrecords_extra_srv(self, mock_query, mock_query_srv):
+    def test_dnsrecords_extra_srv(self, mock_query, mock_query_srv,
+                                  mock_rrset):
         """An extra SRV record set exists, report it.
 
            Add an extra master to the query_srv() which will generate
@@ -475,6 +540,15 @@ class TestDNSSystemRecords(BaseTest):
             'replica3.' + m_api.env.domain
         ])
         mock_query.side_effect = fake_query_three
+        mock_rrset.side_effect = [
+            resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
+            resolve_rrsets('replica.' + m_api.env.domain,
+                           (rdatatype.A, rdatatype.AAAA)),
+            resolve_rrsets('replica2.' + m_api.env.domain,
+                           (rdatatype.A, rdatatype.AAAA)),
+            resolve_rrsets('replica3.' + m_api.env.domain,
+                           (rdatatype.A, rdatatype.AAAA)),
+        ]
 
         m_api.Command.server_find.side_effect = [{
             'result': [
@@ -508,23 +582,28 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 32
+        assert len(self.results) == 35
 
         ok = get_results_by_severity(self.results.results, constants.SUCCESS)
         warn = get_results_by_severity(self.results.results, constants.WARNING)
-        assert len(ok) == 25
+        assert len(ok) == 28
         assert len(warn) == 7
 
         for result in warn:
             assert result.kw.get('msg') == \
                 'Unexpected SRV entry in DNS'
 
+    @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
     @patch('dns.resolver.query')
-    def test_dnsrecords_bad_realm(self, mock_query, mock_query_srv):
+    def test_dnsrecords_bad_realm(self, mock_query, mock_query_srv,
+                                  mock_rrset):
         """Unexpected Kerberos TXT record"""
         mock_query.side_effect = fake_query_one_txt
         mock_query_srv.side_effect = query_srv([m_api.env.host])
+        mock_rrset.side_effect = [
+            resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA))
+        ]
 
         m_api.Command.server_find.side_effect = [{
             'result': [
@@ -543,22 +622,27 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 9
+        assert len(self.results) == 10
 
         ok = get_results_by_severity(self.results.results, constants.SUCCESS)
         warn = get_results_by_severity(self.results.results, constants.WARNING)
-        assert len(ok) == 8
+        assert len(ok) == 9
         assert len(warn) == 1
 
         result = warn[0]
         assert result.kw.get('msg') == 'expected realm missing'
         assert result.kw.get('key') == '\"FAKE_REALM\"'
 
+    @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
     @patch('dns.resolver.query')
-    def test_dnsrecords_one_with_ad(self, mock_query, mock_query_srv):
+    def test_dnsrecords_one_with_ad(self, mock_query, mock_query_srv,
+                                    mock_rrset):
         mock_query.side_effect = fake_query_one
         mock_query_srv.side_effect = query_srv([m_api.env.host], True)
+        mock_rrset.side_effect = [
+            resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA))
+        ]
 
         m_api.Command.server_find.side_effect = [{
             'result': [
@@ -578,7 +662,7 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 15
+        assert len(self.results) == 16
 
         for result in self.results.results:
             assert result.result == constants.SUCCESS


=====================================
tests/test_ipa_dnssan.py
=====================================
@@ -0,0 +1,110 @@
+#
+# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
+#
+
+from util import capture_results, CAInstance
+from util import m_api
+from base import BaseTest
+from unittest.mock import Mock, patch
+
+from ipahealthcheck.core import config, constants
+from ipahealthcheck.ipa.plugin import registry
+from ipahealthcheck.ipa.certs import IPACertDNSSAN
+from mock_certmonger import create_mock_dbus, _certmonger
+from mock_certmonger import get_expected_requests, set_requests
+
+
+class IPACertificate:
+    def __init__(self, serial_number=1, no_san=False):
+        self.subject = 'CN=%s' % m_api.env.host
+        self.issuer = 'CN=ISSUER'
+        self.serial_number = serial_number
+        self.san_a_label_dns_names = [m_api.env.host]
+        if not no_san:
+            self.san_a_label_dns_names.append('ipa-ca.%s' % m_api.env.domain)
+
+
+class TestDNSSAN(BaseTest):
+    patches = {
+        'ipaserver.install.certs.is_ipa_issued_cert':
+        Mock(return_value=True),
+        'ipahealthcheck.ipa.certs.get_expected_requests':
+        Mock(return_value=get_expected_requests()),
+        'ipalib.install.certmonger._cm_dbus_object':
+        Mock(side_effect=create_mock_dbus),
+        'ipalib.install.certmonger._certmonger':
+        Mock(return_value=_certmonger()),
+        'ipaserver.install.cainstance.CAInstance':
+        Mock(return_value=CAInstance()),
+        'socket.getfqdn':
+        Mock(return_value=m_api.env.host),
+    }
+
+    @patch('ipalib.install.certmonger.get_request_value')
+    @patch('ipalib.x509.load_certificate_from_file')
+    def test_dnssan_ok(self, mock_cert, mock_value):
+        set_requests()
+
+        mock_value.side_effect = ['dogtag-ipa-ca-renew-agent',
+                                  'IPA', 'caIPAserviceCert']
+        mock_cert.return_value = IPACertificate()
+
+        framework = object()
+        registry.initialize(framework, config.Config)
+        f = IPACertDNSSAN(registry)
+
+        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.kw.get('san') == [m_api.env.host,
+                                        'ipa-ca.%s' % m_api.env.domain]
+        assert result.kw.get('hostname') == [m_api.env.host,
+                                             'ipa-ca.%s' % m_api.env.domain]
+        assert result.kw.get('profile') == 'caIPAserviceCert'
+        assert result.check == 'IPACertDNSSAN'
+
+    @patch('ipalib.install.certmonger.get_request_value')
+    def test_sandns_no_certs(self, mock_value):
+        set_requests()
+
+        mock_value.side_effect = ['dogtag-ipa-ca-renew-agent',
+                                  'dogtag-ipa-ca-renew-agent']
+
+        framework = object()
+        registry.initialize(framework, config.Config)
+        f = IPACertDNSSAN(registry)
+
+        self.results = capture_results(f)
+
+        # No IPA CA, no results
+        assert len(self.results) == 0
+
+    @patch('ipalib.install.certmonger.get_request_value')
+    @patch('ipalib.x509.load_certificate_from_file')
+    def test_dnssan_missing_ipaca(self, mock_cert, mock_value):
+        set_requests()
+
+        mock_value.side_effect = ['dogtag-ipa-ca-renew-agent',
+                                  'IPA', 'caIPAserviceCert']
+        mock_cert.return_value = IPACertificate(no_san=True)
+
+        framework = object()
+        registry.initialize(framework, config.Config)
+        f = IPACertDNSSAN(registry)
+
+        self.results = capture_results(f)
+
+        assert len(self.results) == 1
+
+        result = self.results.results[0]
+        assert result.result == constants.ERROR
+        assert result.source == 'ipahealthcheck.ipa.certs'
+        assert result.kw.get('san') == [m_api.env.host]
+        assert result.kw.get('hostname') == 'ipa-ca.%s' % m_api.env.domain
+        assert result.kw.get('profile') == 'caIPAserviceCert'
+        assert result.kw.get('ca') == 'IPA'
+        assert result.kw.get('key') == '5678'


=====================================
tests/test_ipa_expiration.py
=====================================
@@ -33,7 +33,7 @@ class TestExpiration(BaseTest):
         registry.initialize(framework, config.Config)
         f = IPACertmongerExpirationCheck(registry)
 
-        f.config.cert_expiration_days = 7
+        f.config.cert_expiration_days = '7'
         self.results = capture_results(f)
 
         assert len(self.results) == 2
@@ -67,7 +67,7 @@ class TestExpiration(BaseTest):
         registry.initialize(framework, config.Config)
         f = IPACertmongerExpirationCheck(registry)
 
-        f.config.cert_expiration_days = 30
+        f.config.cert_expiration_days = '30'
         self.results = capture_results(f)
 
         assert len(self.results) == 2
@@ -122,7 +122,7 @@ class TestChainExpiration(BaseTest):
         registry.initialize(framework, config.Config)
         f = IPACAChainExpirationCheck(registry)
 
-        f.config.cert_expiration_days = 7
+        f.config.cert_expiration_days = '7'
         self.results = capture_results(f)
 
         assert len(self.results) == 2
@@ -159,7 +159,7 @@ class TestChainExpiration(BaseTest):
         registry.initialize(framework, config.Config)
         f = IPACAChainExpirationCheck(registry)
 
-        f.config.cert_expiration_days = 7
+        f.config.cert_expiration_days = '7'
         self.results = capture_results(f)
 
         assert len(self.results) == 2
@@ -198,7 +198,7 @@ class TestChainExpiration(BaseTest):
         registry.initialize(framework, config.Config)
         f = IPACAChainExpirationCheck(registry)
 
-        f.config.cert_expiration_days = 7
+        f.config.cert_expiration_days = '7'
         self.results = capture_results(f)
 
         assert len(self.results) == 2
@@ -235,7 +235,7 @@ class TestChainExpiration(BaseTest):
         registry.initialize(framework, config.Config)
         f = IPACAChainExpirationCheck(registry)
 
-        f.config.cert_expiration_days = 7
+        f.config.cert_expiration_days = '7'
         self.results = capture_results(f)
 
         assert len(self.results) == 2


=====================================
tests/test_ipa_tracking.py
=====================================
@@ -54,6 +54,7 @@ class TestTracking(BaseTest):
             "cert-file=/var/lib/ipa/ra-agent.pem, " \
             "key-file=/var/lib/ipa/ra-agent.key, " \
             "ca-name=dogtag-ipa-ca-renew-agent, " \
+            "template_profile=caSubsystemCert, " \
             "cert-storage=FILE, "\
             "cert-presave-command=" \
             "/usr/libexec/ipa/certmonger/renew_ra_cert_pre, " \


=====================================
tests/test_ipa_trust.py
=====================================
@@ -21,6 +21,7 @@ from ipahealthcheck.ipa.trust import (IPATrustAgentCheck,
                                       IPATrustControllerPrincipalCheck,
                                       IPATrustControllerServiceCheck,
                                       IPATrustControllerGroupSIDCheck,
+                                      IPATrustControllerAdminSIDCheck,
                                       IPATrustControllerConfCheck,
                                       IPATrustPackageCheck)
 
@@ -260,13 +261,18 @@ class TestTrustDomains(BaseTest):
                     'cn': ['ad.example'],
                     'ipantflatname': ['ADROOT'],
                     'ipanttrusteddomainsid': ['S-1-5-21-abc'],
+                    'ipanttrusttype': ['2'],
+                    'ipanttrustattributes': ['8'],
                     'trusttype': ['Active Directory domain'],
                 },
                 {
                     'cn': ['child.example'],
-                    'ipantflatname': ['ADROOT'],
-                    'ipanttrusteddomainsid': ['S-1-5-21-def'],
-                    'trusttype': ['Active Directory domain'],
+                    'ipantflatname': ['ADCHILD'],
+                    'ipanttrusteddomainsid': ['S-1-5-22-def'],
+                    'ipanttrusttype': ['2'],
+                    'ipanttrustattributes': ['9'],
+                    'trusttype': ['Non-transitive external trust to a domain '
+                                  'in another Active Directory forest']
                 },
             ]
         }]
@@ -324,13 +330,18 @@ class TestTrustDomains(BaseTest):
                     'cn': ['ad.example'],
                     'ipantflatname': ['ADROOT'],
                     'ipanttrusteddomainsid': ['S-1-5-21-abc'],
+                    'ipanttrusttype': ['2'],
+                    'ipanttrustattributes': ['8'],
                     'trusttype': ['Active Directory domain'],
                 },
                 {
                     'cn': ['child.example'],
-                    'ipantflatname': ['ADROOT'],
-                    'ipanttrusteddomainsid': ['S-1-5-21-def'],
-                    'trusttype': ['Active Directory domain'],
+                    'ipantflatname': ['ADCHILD'],
+                    'ipanttrusteddomainsid': ['S-1-5-22-def'],
+                    'ipanttrusttype': ['2'],
+                    'ipanttrustattributes': ['9'],
+                    'trusttype': ['Non-transitive external trust to a domain '
+                                  'in another Active Directory forest']
                 },
             ]
         }]
@@ -440,13 +451,18 @@ class TestTrustCatalog(BaseTest):
                     'cn': ['ad.example'],
                     'ipantflatname': ['ADROOT'],
                     'ipanttrusteddomainsid': ['S-1-5-21-abc'],
+                    'ipanttrusttype': ['2'],
+                    'ipanttrustattributes': ['8'],
                     'trusttype': ['Active Directory domain'],
                 },
                 {
                     'cn': ['child.example'],
-                    'ipantflatname': ['ADROOT'],
-                    'ipanttrusteddomainsid': ['S-1-5-21-def'],
-                    'trusttype': ['Active Directory domain'],
+                    'ipantflatname': ['ADCHILD'],
+                    'ipanttrusteddomainsid': ['S-1-5-22-def'],
+                    'ipanttrusttype': ['2'],
+                    'ipanttrustattributes': ['9'],
+                    'trusttype': ['Non-transitive external trust to a domain '
+                                  'in another Active Directory forest']
                 },
             ]
         }]
@@ -486,7 +502,7 @@ class TestTrustCatalog(BaseTest):
         assert result.source == 'ipahealthcheck.ipa.trust'
         assert result.check == 'IPATrustCatalogCheck'
         assert result.kw.get('key') == 'Domain Security Identifier'
-        assert result.kw.get('sid') == 'S-1-5-21-def'
+        assert result.kw.get('sid') == 'S-1-5-22-def'
 
         result = self.results.results[4]
         assert result.result == constants.SUCCESS
@@ -878,6 +894,80 @@ class TestControllerGroupSID(BaseTest):
         assert result.kw.get('rid') == 'S-1-5-21-1234-5678-1976041503-500'
 
 
+class TestControllerAdminSID(BaseTest):
+    patches = {
+        'ldap.initialize':
+        Mock(return_value=mock_ldap_conn()),
+    }
+
+    def test_not_trust_controller(self):
+        framework = object()
+        registry.initialize(framework, config.Config)
+        registry.trust_controller = False
+        f = IPATrustControllerAdminSIDCheck(registry)
+
+        self.results = capture_results(f)
+
+        # Zero because the call was skipped altogether
+        assert len(self.results) == 0
+
+    def test_principal_ok(self):
+        admin_dn = DN(('uid', 'admin'))
+        attrs = {
+            'ipantsecurityidentifier':
+            ['S-1-5-21-1234-5678-1976041503-500'],
+        }
+        fake_conn = LDAPClient('ldap://localhost', no_schema=True)
+        ldapentry = LDAPEntry(fake_conn, admin_dn)
+        for attr, values in attrs.items():
+            ldapentry[attr] = values
+
+        framework = object()
+        registry.initialize(framework, config.Config)
+        registry.trust_controller = True
+        f = IPATrustControllerAdminSIDCheck(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.trust'
+        assert result.check == 'IPATrustControllerAdminSIDCheck'
+        assert result.kw.get('key') == 'ipantsecurityidentifier'
+        assert result.kw.get('rid') == 'S-1-5-21-1234-5678-1976041503-500'
+
+    def test_principal_fail(self):
+        admin_dn = DN(('uid', 'admin'))
+        attrs = {
+            'ipantsecurityidentifier':
+            ['S-1-5-21-1234-5678-1976041503-400'],
+        }
+        fake_conn = LDAPClient('ldap://localhost', no_schema=True)
+        ldapentry = LDAPEntry(fake_conn, admin_dn)
+        for attr, values in attrs.items():
+            ldapentry[attr] = values
+
+        framework = object()
+        registry.initialize(framework, config.Config)
+        registry.trust_controller = True
+        f = IPATrustControllerAdminSIDCheck(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.ERROR
+        assert result.source == 'ipahealthcheck.ipa.trust'
+        assert result.check == 'IPATrustControllerAdminSIDCheck'
+        assert result.kw.get('key') == 'ipantsecurityidentifier'
+        assert result.kw.get('rid') == 'S-1-5-21-1234-5678-1976041503-400'
+
+
 class TestControllerConf(BaseTest):
     patches = {
         'ldap.initialize':


=====================================
tests/test_meta.py
=====================================
@@ -0,0 +1,164 @@
+#
+# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
+#
+
+from base import BaseTest
+from collections import namedtuple
+from unittest.mock import patch
+from util import capture_results
+
+from ipahealthcheck.core import config, constants
+from ipahealthcheck.meta.plugin import registry
+from ipahealthcheck.meta.core import MetaCheck
+from ipapython import ipautil
+from ipaplatform.paths import paths
+
+if 'FIPS_MODE_SETUP' not in dir(paths):
+    paths.FIPS_MODE_SETUP = '/usr/bin/fips-mode-setup'
+
+
+class TestMetaFIPS(BaseTest):
+    @patch('os.path.exists')
+    def test_fips_no_fips_mode_setup(self, mock_exists):
+        mock_exists.return_value = False
+
+        framework = object()
+        registry.initialize(framework, config.Config())
+        f = MetaCheck(registry)
+
+        self.results = capture_results(f)
+
+        assert len(self.results) == 1
+
+        result = self.results.results[0]
+        assert result.result == constants.SUCCESS
+        assert result.source == 'ipahealthcheck.meta.core'
+        assert result.check == 'MetaCheck'
+        assert result.kw.get('fips') == 'missing %s' % paths.FIPS_MODE_SETUP
+
+    @patch('os.path.exists')
+    @patch('ipapython.ipautil.run')
+    def test_fips_disabled(self, mock_run, mock_exists):
+        mock_exists.return_value = True
+
+        run_result = namedtuple('run', ['returncode', 'raw_output'])
+        run_result.returncode = 2
+        run_result.raw_output = b''
+
+        mock_run.return_value = run_result
+
+        framework = object()
+        registry.initialize(framework, config.Config())
+        f = MetaCheck(registry)
+
+        self.results = capture_results(f)
+
+        assert len(self.results) == 1
+
+        result = self.results.results[0]
+        assert result.result == constants.SUCCESS
+        assert result.source == 'ipahealthcheck.meta.core'
+        assert result.check == 'MetaCheck'
+        assert result.kw.get('fips') == 'disabled'
+
+    @patch('os.path.exists')
+    @patch('ipapython.ipautil.run')
+    def test_fips_enabled(self, mock_run, mock_exists):
+        mock_exists.return_value = True
+
+        run_result = namedtuple('run', ['returncode', 'raw_output'])
+        run_result.returncode = 0
+        run_result.raw_output = b''
+
+        mock_run.return_value = run_result
+
+        framework = object()
+        registry.initialize(framework, config.Config())
+        f = MetaCheck(registry)
+
+        self.results = capture_results(f)
+
+        assert len(self.results) == 1
+
+        result = self.results.results[0]
+        assert result.result == constants.SUCCESS
+        assert result.source == 'ipahealthcheck.meta.core'
+        assert result.check == 'MetaCheck'
+        assert result.kw.get('fips') == 'enabled'
+
+    @patch('os.path.exists')
+    @patch('ipapython.ipautil.run')
+    def test_fips_inconsistent(self, mock_run, mock_exists):
+        mock_exists.return_value = True
+
+        run_result = namedtuple('run', ['returncode', 'raw_output'])
+        run_result.returncode = 1
+        run_result.raw_output = b''
+
+        mock_run.return_value = run_result
+
+        framework = object()
+        registry.initialize(framework, config.Config())
+        f = MetaCheck(registry)
+
+        self.results = capture_results(f)
+
+        assert len(self.results) == 1
+
+        result = self.results.results[0]
+        assert result.result == constants.SUCCESS
+        assert result.source == 'ipahealthcheck.meta.core'
+        assert result.check == 'MetaCheck'
+        assert result.kw.get('fips') == 'inconsistent'
+
+    @patch('os.path.exists')
+    @patch('ipapython.ipautil.run')
+    def test_fips_unknown(self, mock_run, mock_exists):
+        mock_exists.return_value = True
+
+        run_result = namedtuple('run', ['returncode', 'raw_output'])
+        run_result.returncode = 103
+        run_result.raw_output = b''
+
+        mock_run.return_value = run_result
+
+        framework = object()
+        registry.initialize(framework, config.Config())
+        f = MetaCheck(registry)
+
+        self.results = capture_results(f)
+
+        assert len(self.results) == 1
+
+        result = self.results.results[0]
+        assert result.result == constants.SUCCESS
+        assert result.source == 'ipahealthcheck.meta.core'
+        assert result.check == 'MetaCheck'
+        assert result.kw.get('fips') == 'unknown'
+
+    @patch('os.path.exists')
+    @patch('ipapython.ipautil.run')
+    def test_fips_failed(self, mock_run, mock_exists):
+        mock_exists.return_value = True
+
+        run_result = namedtuple('run', ['returncode', 'raw_output'])
+        run_result.returncode = 103
+        run_result.raw_output = b''
+
+        mock_run.side_effect = ipautil.CalledProcessError(
+           1, 'fips-mode-setup', output='execution failed'
+        )
+
+        framework = object()
+        registry.initialize(framework, config.Config())
+        f = MetaCheck(registry)
+
+        self.results = capture_results(f)
+
+        assert len(self.results) == 1
+
+        result = self.results.results[0]
+        assert result.result == constants.ERROR
+        assert result.source == 'ipahealthcheck.meta.core'
+        assert result.check == 'MetaCheck'
+        assert result.kw.get('fips') == 'failed to check'


=====================================
tests/util.py
=====================================
@@ -115,6 +115,7 @@ m_api.env.server = 'server.ipa.example'
 m_api.env.realm = u'IPA.EXAMPLE'
 m_api.env.domain = u'ipa.example'
 m_api.env.basedn = u'dc=ipa,dc=example'
+m_api.env.container_user = DN(('cn', 'users'), ('cn', 'accounts'))
 m_api.env.container_group = DN(('cn', 'groups'), ('cn', 'accounts'))
 m_api.env.container_host = DN(('cn', 'computers'), ('cn', 'accounts'))
 m_api.env.container_sysaccounts = DN(('cn', 'sysaccounts'), ('cn', 'etc'))



View it on GitLab: https://salsa.debian.org/freeipa-team/freeipa-healthcheck/-/compare/5d998b6667bf0f8d39d7f916d71a60badd5d3035...f97efcd1cf6c37fa50fd5f92a94d403cf29388eb

-- 
View it on GitLab: https://salsa.debian.org/freeipa-team/freeipa-healthcheck/-/compare/5d998b6667bf0f8d39d7f916d71a60badd5d3035...f97efcd1cf6c37fa50fd5f92a94d403cf29388eb
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-freeipa-devel/attachments/20201112/c65895b4/attachment-0001.html>


More information about the Pkg-freeipa-devel mailing list