Update to 0.19 release

- Add a message if not a trust agent/controller (RHEL-116896)
- Check that expected NSS token matches the current FIPS state (RHEL-116897)
- Add /etc/pki/tls/certs/ directory to file checker (RHEL-116898)
- Check that allowed_uids in the SSSD config is valid (RHEL-79092)

Resolves: RHEL-116896
Resolves: RHEL-116897
Resolves: RHEL-116898
Resolves: RHEL-79092
This commit is contained in:
Rob Crittenden 2025-09-24 13:32:08 -04:00
parent bbebf077cf
commit 22689a6229
15 changed files with 17 additions and 1211 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@
/0.9.tar.gz
/0.12.tar.gz
/0.16.tar.gz
/0.19.tar.gz

View File

@ -1,6 +1,6 @@
From 378e0a353e670a6b498d454558a9139a859890d4 Mon Sep 17 00:00:00 2001
From dee0c0842f92b9fb7caf64eb498c4c27d1aa5326 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
Date: Thu, 9 Nov 2023 10:49:05 -0500
Date: Mon, 22 Sep 2025 16:33:24 -0400
Subject: [PATCH] Remove ipaclustercheck
---
@ -26,13 +26,13 @@ Subject: [PATCH] Remove ipaclustercheck
delete mode 100644 tests/test_cluster_ruv.py
diff --git a/setup.py b/setup.py
index d926302..cb6265a 100644
index 578fc52..6f47cda 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@ from setuptools import find_packages, setup
setup(
name='ipahealthcheck',
version='0.16',
version='0.19',
- namespace_packages=['ipahealthcheck', 'ipaclustercheck'],
+ namespace_packages=['ipahealthcheck'],
package_dir={'': 'src'},
@ -53,7 +53,7 @@ index d926302..cb6265a 100644
],
# subsystem registries
'ipahealthcheck.registry': [
@@ -72,13 +69,6 @@ setup(
@@ -73,13 +70,6 @@ setup(
'ipahealthcheck.system': [
'filesystemspace = ipahealthcheck.system.filesystemspace',
],
@ -635,5 +635,5 @@ index 7583c84..0000000
- assert result.kw.get('name') == 'dangling_csruv'
- assert result.kw.get('value') == '9'
--
2.38.1
2.49.0

View File

@ -1,44 +0,0 @@
From 35ff77300758c12110132d6d638802d5b223bd6d Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
Date: Mon, 13 Nov 2023 14:09:16 -0500
Subject: [PATCH] Don't fail if a service name cannot be looked up in LDAP
A new method was introduced to handle more IPA services. This
requires looking some of them up in LDAP. dirsrv not running
was not being caught so raised an error instead.
Fixes: https://github.com/freeipa/freeipa-healthcheck/issues/312
Signed-off-by: Rob Crittenden <rcritten@redhat.com>
---
src/ipahealthcheck/meta/services.py | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/src/ipahealthcheck/meta/services.py b/src/ipahealthcheck/meta/services.py
index 10fa83f..9838128 100644
--- a/src/ipahealthcheck/meta/services.py
+++ b/src/ipahealthcheck/meta/services.py
@@ -25,10 +25,18 @@ class IPAServiceCheck(ServiceCheck):
def get_service_name(self, role):
"""Roles define broad services. Translate a role name into
an individual service name.
+
+ Returns a string on success, None if the service is not
+ configured or cannot be determined.
"""
conn = api.Backend.ldap2
- if not api.Backend.ldap2.isconnected():
- api.Backend.ldap2.connect()
+ try:
+ if not api.Backend.ldap2.isconnected():
+ api.Backend.ldap2.connect()
+ except errors.NetworkError:
+ logger.debug("Service '%s' is not running", self.service_name)
+ return None
+
dn = DN(
("cn", role), ("cn", api.env.host),
("cn", "masters"), ("cn", "ipa"), ("cn", "etc"),
--
2.41.0

View File

@ -1,44 +0,0 @@
From d1cb1997737c938bbc61d547aae277e308e78fce Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
Date: Tue, 14 Nov 2023 20:32:54 -0500
Subject: [PATCH] Temporarily disable the ipa-ods-exporter service status check
There is a bug in this service such that it will almost always
report as down. Rather than spamming users with this error give
time for it to be fixed in IPA upstream.
See https://pagure.io/freeipa/issue/9463
Signed-off-by: Rob Crittenden <rcritten@redhat.com>
---
src/ipahealthcheck/meta/services.py | 14 --------------
1 file changed, 14 deletions(-)
diff --git a/src/ipahealthcheck/meta/services.py b/src/ipahealthcheck/meta/services.py
index 9838128..b8973cb 100644
--- a/src/ipahealthcheck/meta/services.py
+++ b/src/ipahealthcheck/meta/services.py
@@ -202,20 +202,6 @@ class ods_enforcerd(IPAServiceCheck):
return super().check()
-@registry
-class ipa_ods_exporter(IPAServiceCheck):
- requires = ('dirsrv',)
-
- def check(self, instance=''):
- self.service_name = self.get_service_name('DNSKeyExporter')
-
- if self.service_name is None:
- # No service name means it is not configured
- return ()
-
- return super().check()
-
-
@registry
class ipa_dnskeysyncd(IPAServiceCheck):
requires = ('dirsrv',)
--
2.40.1

View File

@ -1,46 +0,0 @@
From e556edc0b1cb607caa50f760d5059877f35fbcdc Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
Date: Thu, 11 Jan 2024 14:40:02 -0500
Subject: [PATCH] Skip DogtagCertsConfigCheck for PKI versions >= 11.5.0
In 11.5.0 the PKI project stopped storing the certificate
blobs in CS.cfg. If we continue to check it we will report a
false positive so skip it in that case.
Fixes: https://github.com/freeipa/freeipa-healthcheck/issues/317
Signed-off-by: Rob Crittenden <rcritten@redhat.com>
---
src/ipahealthcheck/dogtag/ca.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/ipahealthcheck/dogtag/ca.py b/src/ipahealthcheck/dogtag/ca.py
index 4afa5d7..ddf5ece 100644
--- a/src/ipahealthcheck/dogtag/ca.py
+++ b/src/ipahealthcheck/dogtag/ca.py
@@ -16,6 +16,8 @@ from ipaserver.install import krainstance
from ipapython.directivesetter import get_directive
from cryptography.hazmat.primitives.serialization import Encoding
+import pki.util
+
logger = logging.getLogger()
@@ -30,6 +32,13 @@ class DogtagCertsConfigCheck(DogtagPlugin):
logger.debug("No CA configured, skipping dogtag config check")
return
+ pki_version = pki.util.Version(pki.specification_version())
+ if pki_version >= pki.util.Version("11.5.0"):
+ logger.debug(
+ "PKI 11.5.0 no longer stores certificats in CS.cfg"
+ )
+ return
+
kra = krainstance.KRAInstance(api.env.realm)
blobs = {'auditSigningCert cert-pki-ca': 'ca.audit_signing.cert',
--
2.42.0

View File

@ -1,59 +0,0 @@
From cafe01a23a36c408c8c60357ad2651688dc63599 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
Date: Fri, 12 Jan 2024 10:17:18 -0500
Subject: [PATCH] test: Handle PKI >= 11.5.0 not storing certs in CS.cfg
Update the test to expect 0 results if the PKI version is
>= 11.5.0.
Fixes: https://github.com/freeipa/freeipa-healthcheck/issues/317
Signed-off-by: Rob Crittenden <rcritten@redhat.com>
---
tests/test_dogtag_ca.py | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/tests/test_dogtag_ca.py b/tests/test_dogtag_ca.py
index 0820aba..1f61dea 100644
--- a/tests/test_dogtag_ca.py
+++ b/tests/test_dogtag_ca.py
@@ -2,12 +2,16 @@
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
+import pki.util
from util import capture_results, CAInstance, KRAInstance
from base import BaseTest
from ipahealthcheck.core import config, constants
from ipahealthcheck.dogtag.plugin import registry
from ipahealthcheck.dogtag.ca import DogtagCertsConfigCheck
from unittest.mock import Mock, patch
+import pytest
+
+pki_version = pki.util.Version(pki.specification_version())
class mock_Cert:
@@ -43,6 +47,9 @@ class TestCACerts(BaseTest):
Mock(return_value=KRAInstance()),
}
+ @pytest.mark.skipif(
+ pki_version >= pki.util.Version("11.5.0"),
+ reason='Does not apply to PKI 11.5.0+')
@patch('ipahealthcheck.dogtag.ca.get_directive')
@patch('ipaserver.install.certs.CertDB')
def test_ca_certs_ok(self, mock_certdb, mock_directive):
@@ -71,6 +78,9 @@ class TestCACerts(BaseTest):
assert result.source == 'ipahealthcheck.dogtag.ca'
assert result.check == 'DogtagCertsConfigCheck'
+ @pytest.mark.skipif(
+ pki_version >= pki.util.Version("11.5.0"),
+ reason='Does not apply to PKI 11.5.0+')
@patch('ipahealthcheck.dogtag.ca.get_directive')
@patch('ipaserver.install.certs.CertDB')
def test_cert_missing_from_file(self, mock_certdb, mock_directive):
--
2.42.0

View File

@ -1,47 +0,0 @@
From e0c09f9f1388bbce43775f40a39266e692e231da Mon Sep 17 00:00:00 2001
From: Thorsten Scherf <tscherf@redhat.com>
Date: Wed, 13 Mar 2024 12:57:34 +0100
Subject: [PATCH 1/4] Fixes log file permissions as per CIS benchmark
As per CIS benchmark the log file permissions should be 640 for some log
files but if we change /var/log/ipa-custodia.audit.log permissions to
640 then "ipa-healthcheck" reports a permission issue.
Fixes: https://github.com/freeipa/freeipa-healthcheck/issues/325
Signed-off-by: Thorsten Scherf <tscherf@redhat.com>
---
src/ipahealthcheck/ipa/files.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/src/ipahealthcheck/ipa/files.py b/src/ipahealthcheck/ipa/files.py
index b7ca116..d914014 100644
--- a/src/ipahealthcheck/ipa/files.py
+++ b/src/ipahealthcheck/ipa/files.py
@@ -121,7 +121,7 @@ class IPAFileCheck(IPAPlugin, FileCheck):
self.files.append((filename, 'root', 'root', '0600'))
self.files.append((paths.IPA_CUSTODIA_AUDIT_LOG,
- 'root', 'root', '0644'))
+ 'root', 'root', '0644', '0640'))
self.files.append((paths.KADMIND_LOG, 'root', 'root',
('0600', '0640')))
@@ -133,11 +133,13 @@ class IPAFileCheck(IPAPlugin, FileCheck):
self.files.append((paths.SLAPD_INSTANCE_ERROR_LOG_TEMPLATE % inst,
constants.DS_USER, constants.DS_GROUP, '0600'))
- self.files.append((paths.VAR_LOG_HTTPD_ERROR, 'root', 'root', '0644'))
+ self.files.append((paths.VAR_LOG_HTTPD_ERROR, 'root', 'root',
+ '0644', '0640'))
for globpath in glob.glob("%s/debug*.log" % paths.TOMCAT_CA_DIR):
self.files.append(
- (globpath, constants.PKI_USER, constants.PKI_GROUP, "0644")
+ (globpath, constants.PKI_USER, constants.PKI_GROUP,
+ "0644", "0640")
)
for globpath in glob.glob(
--
2.45.0

View File

@ -1,190 +0,0 @@
From 2206b9915606c555163dec775a99a355dc02bee0 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
Date: Tue, 28 May 2024 11:15:48 -0400
Subject: [PATCH 3/4] Fix some file mode format issues
When specifying multiple possible modes for a file the values must
be a tuple. There were two occurances where they were listed
separately.
Add in a pre-check on the formatting to raise an error for badly
formatted files. This may be annoying for users if one sneaks in
again but the CI should catch it.
Related: https://github.com/freeipa/freeipa-healthcheck/issues/325
Signed-off-by: Rob Crittenden <rcritten@redhat.com>
---
src/ipahealthcheck/core/files.py | 12 +++++-
src/ipahealthcheck/ipa/files.py | 6 +--
tests/test_core_files.py | 72 +++++++++++++++++++++++++++++++-
tests/util.py | 1 +
4 files changed, 85 insertions(+), 6 deletions(-)
diff --git a/src/ipahealthcheck/core/files.py b/src/ipahealthcheck/core/files.py
index 85d42bc..32bc5b2 100644
--- a/src/ipahealthcheck/core/files.py
+++ b/src/ipahealthcheck/core/files.py
@@ -31,7 +31,17 @@ class FileCheck:
@duration
def check(self):
- for (path, owner, group, mode) in self.files:
+ # first validate that the list of files to check is in the correct
+ # format
+ process_files = []
+ for file in self.files:
+ if len(file) == 4:
+ process_files.append(file)
+ else:
+ yield Result(self, constants.ERROR, key=file,
+ msg='Code format is incorrect for file')
+
+ for (path, owner, group, mode) in process_files:
if not isinstance(owner, tuple):
owner = tuple((owner,))
if not isinstance(group, tuple):
diff --git a/src/ipahealthcheck/ipa/files.py b/src/ipahealthcheck/ipa/files.py
index d914014..c80fd5b 100644
--- a/src/ipahealthcheck/ipa/files.py
+++ b/src/ipahealthcheck/ipa/files.py
@@ -121,7 +121,7 @@ class IPAFileCheck(IPAPlugin, FileCheck):
self.files.append((filename, 'root', 'root', '0600'))
self.files.append((paths.IPA_CUSTODIA_AUDIT_LOG,
- 'root', 'root', '0644', '0640'))
+ 'root', 'root', ('0644', '0640')))
self.files.append((paths.KADMIND_LOG, 'root', 'root',
('0600', '0640')))
@@ -134,12 +134,12 @@ class IPAFileCheck(IPAPlugin, FileCheck):
constants.DS_USER, constants.DS_GROUP, '0600'))
self.files.append((paths.VAR_LOG_HTTPD_ERROR, 'root', 'root',
- '0644', '0640'))
+ ('0644', '0640')))
for globpath in glob.glob("%s/debug*.log" % paths.TOMCAT_CA_DIR):
self.files.append(
(globpath, constants.PKI_USER, constants.PKI_GROUP,
- "0644", "0640")
+ ("0644", "0640"))
)
for globpath in glob.glob(
diff --git a/tests/test_core_files.py b/tests/test_core_files.py
index 924d7fa..e7010a9 100644
--- a/tests/test_core_files.py
+++ b/tests/test_core_files.py
@@ -2,14 +2,22 @@
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
+from ldap import OPT_X_SASL_SSF_MIN
import pwd
import posix
+from util import m_api
+from util import capture_results
+
+from ipahealthcheck.core import config
from ipahealthcheck.core.files import FileCheck
from ipahealthcheck.core import constants
from ipahealthcheck.core.plugin import Results
+from ipahealthcheck.ipa.files import IPAFileCheck
+from ipahealthcheck.system.plugin import registry
from unittest.mock import patch
+from ipapython.dn import DN
+from ipapython.ipaldap import LDAPClient, LDAPEntry
-from util import capture_results
nobody = pwd.getpwnam('nobody')
@@ -20,6 +28,37 @@ files = (('foo', 'root', 'root', '0660'),
('fiz', ('root', 'bin'), ('root', 'bin'), '0664'),
('zap', ('root', 'bin'), ('root', 'bin'), ('0664', '0640'),))
+bad_modes = (('biz', ('root', 'bin'), ('root', 'bin'), '0664', '0640'),)
+
+
+class mock_ldap:
+ SCOPE_BASE = 1
+ SCOPE_ONELEVEL = 2
+ SCOPE_SUBTREE = 4
+
+ def __init__(self, ldapentry):
+ """Initialize the results that we will return from get_entries"""
+ self.results = ldapentry
+
+ def get_entry(self, dn, attrs_list=None, time_limit=None,
+ size_limit=None, get_effective_rights=False):
+ return [] # the call doesn't check the value
+
+
+class mock_ldap_conn:
+ def set_option(self, option, invalue):
+ pass
+
+ def get_option(self, option):
+ if option == OPT_X_SASL_SSF_MIN:
+ return 256
+
+ return None
+
+ def search_s(self, base, scope, filterstr=None,
+ attrlist=None, attrsonly=0):
+ return tuple()
+
def make_stat(mode=33200, uid=0, gid=0):
"""Return a mocked-up stat.
@@ -234,4 +273,33 @@ def test_files_group_not_found(mock_grgid, mock_grnam, mock_stat):
my_results = get_results(results, 'group')
for result in my_results.results:
assert result.result == constants.WARNING
- assert result.kw.get('got') == 'Unknown gid 0'
+
+
+def test_bad_modes():
+ f = FileCheck()
+ f.files = bad_modes
+
+ results = capture_results(f)
+
+ for result in results.results:
+ assert result.result == constants.ERROR
+ assert result.kw.get('msg') == 'Code format is incorrect for file'
+
+
+@patch('ipaserver.install.krbinstance.is_pkinit_enabled')
+def test_ipa_files_format(mock_pkinit):
+ mock_pkinit.return_value = True
+
+ fake_conn = LDAPClient('ldap://localhost', no_schema=True)
+ ldapentry = LDAPEntry(fake_conn, DN(m_api.env.container_dns,
+ m_api.env.basedn))
+ framework = object()
+ registry.initialize(framework, config.Config)
+ f = IPAFileCheck(registry)
+
+ f.conn = mock_ldap(ldapentry)
+
+ results = capture_results(f)
+
+ for result in results.results:
+ assert result.result == constants.SUCCESS
diff --git a/tests/util.py b/tests/util.py
index 12c1688..fb8750a 100644
--- a/tests/util.py
+++ b/tests/util.py
@@ -141,6 +141,7 @@ m_api.env.container_host = DN(('cn', 'computers'), ('cn', 'accounts'))
m_api.env.container_sysaccounts = DN(('cn', 'sysaccounts'), ('cn', 'etc'))
m_api.env.container_service = DN(('cn', 'services'), ('cn', 'accounts'))
m_api.env.container_masters = DN(('cn', 'masters'))
+m_api.env.container_dns = DN(('cn', 'dns'))
m_api.Backend = Mock()
m_api.Command = Mock()
m_api.Command.ping.return_value = {
--
2.45.0

View File

@ -1,28 +0,0 @@
From 79cca342b3c440a045cadbff871ff977e35222c6 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
Date: Thu, 20 Jun 2024 14:27:16 -0400
Subject: [PATCH] Allow WARNING in the files test
We are only validating the format and don't need to actually
enforce the results in CI. The validation raises ERROR.
Related: https://github.com/freeipa/freeipa-healthcheck/issues/325
Signed-off-by: Rob Crittenden <rcritten@redhat.com>
---
tests/test_core_files.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_core_files.py b/tests/test_core_files.py
index e7010a9..d308410 100644
--- a/tests/test_core_files.py
+++ b/tests/test_core_files.py
@@ -302,4 +302,4 @@ def test_ipa_files_format(mock_pkinit):
results = capture_results(f)
for result in results.results:
- assert result.result == constants.SUCCESS
+ assert result.result in (constants.SUCCESS, constants.WARNING)
--
2.45.0

View File

@ -1,345 +0,0 @@
From 7480a65f13cd17cc57a82f36ddb4fe3fbc7fdf64 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
Date: Mon, 10 Feb 2025 13:08:50 -0500
Subject: [PATCH] Check user-provided certificates for expiration
Validate the Apache, DS and PKINIT certificates and
warn if they are going to expire soon. This should help
users avoid expired certificates they provide themselves.
These were originally logged and skipped.
Fixes: https://github.com/freeipa/freeipa-healthcheck/issues/347
Signed-off-by: Rob Crittenden <rcritten@redhat.com>
---
src/ipahealthcheck/ipa/certs.py | 81 ++++++++++++++++
tests/common.py | 71 ++++++++++++++
tests/test_ipa_nssdb.py | 23 +----
tests/test_ipa_userprovided_expiration.py | 110 ++++++++++++++++++++++
4 files changed, 263 insertions(+), 22 deletions(-)
create mode 100644 tests/common.py
create mode 100644 tests/test_ipa_userprovided_expiration.py
diff --git a/src/ipahealthcheck/ipa/certs.py b/src/ipahealthcheck/ipa/certs.py
index b8332e0..fc626a6 100644
--- a/src/ipahealthcheck/ipa/certs.py
+++ b/src/ipahealthcheck/ipa/certs.py
@@ -453,6 +453,87 @@ class IPACertfileExpirationCheck(IPAPlugin):
yield Result(self, constants.SUCCESS, key=id)
+@registry
+class IPAUserProvidedExpirationCheck(IPAPlugin):
+ """
+ If we detect user-provided certificates then check to see if
+ they are expiring soon.
+
+ This only applies to the HTTP, DS and KRB certificates.
+ """
+ def validate_cert(self, id, cert, certfile=None, nssdb=None,
+ nickname=None):
+ now = datetime.now(tz=timezone.utc)
+ notafter = cert.not_valid_after_utc
+
+ if certfile:
+ location = certfile
+ if nssdb:
+ location = "{}:{}".format(nssdb, nickname)
+
+ if now > notafter:
+ logger.debug("expired")
+ yield Result(self, constants.ERROR,
+ key=id,
+ location=location,
+ expiration_date=generalized_time(notafter),
+ msg='Request id {key} expired on '
+ '{expiration_date}')
+ return
+
+ delta = notafter - now
+ diff = int(delta.total_seconds() / DAY)
+ if diff < int(self.config.cert_expiration_days):
+ logger.debug("expiring soon")
+ yield Result(self, constants.WARNING,
+ key=id,
+ location=location,
+ expiration_date=generalized_time(notafter),
+ days=diff,
+ msg='Request id {key} expires in {days} '
+ 'days. You need to manually renew this '
+ 'certificate.')
+ return
+
+ yield Result(self, constants.SUCCESS, location=location, key=id)
+
+ @duration
+ def check(self):
+ for id, certfile in (
+ ("HTTP", paths.HTTPD_CERT_FILE),
+ ("KDC", paths.KDC_CERT)
+ ):
+ try:
+ cert = x509.load_certificate_from_file(certfile)
+ except Exception as e:
+ yield Result(self, constants.ERROR,
+ key=id,
+ certfile=certfile,
+ error=str(e),
+ msg='Request id {key}: Unable to open cert '
+ 'file \'{certfile}\': {error}')
+ continue
+ issued = is_ipa_issued_cert(api, cert)
+ if issued is None:
+ logger.debug('Unable to determine if \'%s\' was issued by IPA '
+ 'because no LDAP connection, assuming yes.')
+ continue
+ elif issued is True:
+ logging.debug("Issued by IPA, skipping")
+ continue
+ yield from self.validate_cert(id, cert, certfile=certfile)
+
+ ds_db_dirname = dsinstance.config_dirname(self.serverid)
+ ds_db = certs.CertDB(api.env.realm, nssdir=ds_db_dirname)
+ nickname = self.ds.get_server_cert_nickname(self.serverid)
+ cert = ds_db.get_cert_from_db(nickname)
+ if is_ipa_issued_cert(api, cert):
+ logging.debug("Issued by IPA, skipping")
+ return
+ yield from self.validate_cert("LDAP", cert, nssdb=ds_db_dirname,
+ nickname=nickname)
+
+
@registry
class IPACertTracking(IPAPlugin):
"""Compare the certificates tracked by certmonger to those that
diff --git a/tests/common.py b/tests/common.py
new file mode 100644
index 0000000..91799cc
--- /dev/null
+++ b/tests/common.py
@@ -0,0 +1,71 @@
+#
+# Copyright (C) 2025 FreeIPA Contributors see COPYING for license
+#
+from datetime import datetime, timedelta, timezone
+
+
+class mock_Cert:
+ """Fake up a certificate.
+
+ The contents are the NSS nickname of the certificate.
+ """
+
+ def __init__(
+ self,
+ text,
+ issuer="CN=Someone",
+ not_after=datetime.now(tz=timezone.utc),
+ ):
+ self.text = text
+ self._issuer = issuer
+ self._not_valid_after_utc = not_after
+
+ def public_bytes(self, encoding):
+ return self.text.encode("utf-8")
+
+ @property
+ def issuer(self):
+ return self._issuer
+
+ @property
+ def not_valid_after_utc(self):
+ return self._not_valid_after_utc
+
+
+class mock_CertDB:
+ def __init__(self, trust, expiration_days=0):
+ """A dict of nickname + NSSdb trust flags"""
+ self.trust = trust
+ self._expiration_days = expiration_days
+
+ def list_certs(self):
+ return [
+ (nickname, self.trust[nickname]) for nickname in self.trust
+ ]
+
+ def get_cert_from_db(self, nickname):
+ """Return the nickname. This will match the value of get_directive"""
+ notafter = datetime.now(tz=timezone.utc) + timedelta(
+ days=self._expiration_days
+ )
+ return mock_Cert(nickname, not_after=notafter)
+
+
+class mock_NSSDatabase:
+ def __init__(self, nssdir, token=None, pwd_file=None, trust=None):
+ self.trust = trust
+ self.token = token
+
+ def list_certs(self):
+ return [
+ (nickname, self.trust[nickname]) for nickname in self.trust
+ ]
+
+
+def my_unparse_trust_flags(trust_flags):
+ return trust_flags
+
+
+class DsInstance:
+ def get_server_cert_nickname(self, serverid):
+ return "Server-Cert"
diff --git a/tests/test_ipa_nssdb.py b/tests/test_ipa_nssdb.py
index cbf7277..8694f82 100644
--- a/tests/test_ipa_nssdb.py
+++ b/tests/test_ipa_nssdb.py
@@ -9,28 +9,7 @@ from ipahealthcheck.ipa.plugin import registry
from ipahealthcheck.ipa.certs import IPACertNSSTrust
from ipaplatform.paths import paths
from unittest.mock import Mock, patch
-
-
-class mock_CertDB:
- def __init__(self, trust):
- """A dict of nickname + NSSdb trust flags"""
- self.trust = trust
-
- def list_certs(self):
- return [(nickname, self.trust[nickname]) for nickname in self.trust]
-
-
-class mock_NSSDatabase:
- def __init__(self, nssdir, token=None, pwd_file=None, trust=None):
- self.trust = trust
- self.token = token
-
- def list_certs(self):
- return [(nickname, self.trust[nickname]) for nickname in self.trust]
-
-
-def my_unparse_trust_flags(trust_flags):
- return trust_flags
+from common import mock_NSSDatabase, my_unparse_trust_flags
# These tests make some assumptions about the order in which the
diff --git a/tests/test_ipa_userprovided_expiration.py b/tests/test_ipa_userprovided_expiration.py
new file mode 100644
index 0000000..71ceb6c
--- /dev/null
+++ b/tests/test_ipa_userprovided_expiration.py
@@ -0,0 +1,110 @@
+#
+# Copyright (C) 2025 FreeIPA Contributors see COPYING for license
+#
+
+from util import capture_results
+from base import BaseTest
+from common import DsInstance
+from ipahealthcheck.core import config, constants
+from ipahealthcheck.ipa.plugin import registry
+from ipahealthcheck.ipa.certs import IPAUserProvidedExpirationCheck
+from unittest.mock import Mock, patch
+from ipapython.dn import DN
+from common import mock_CertDB
+
+from datetime import datetime, timedelta, timezone
+
+CERT_EXPIRATION_DAYS = 30
+
+
+class IPACertificate:
+ def __init__(self, not_valid_after, serial_number=1):
+ self.subject = 'CN=RA AGENT'
+ self.issuer = 'CN=ISSUER'
+ self.serial_number = serial_number
+ self.not_valid_after_utc = not_valid_after
+
+
+class TestIPACertificateFile(BaseTest):
+ patches = {
+ 'ipaserver.install.dsinstance.DsInstance':
+ Mock(return_value=DsInstance()),
+ 'ipalib.install.certstore.get_ca_subject':
+ Mock(return_value=DN("CN=EXTERNAL")),
+ 'ipaserver.install.certs.is_ipa_issued_cert':
+ Mock(return_value=False),
+ }
+
+ @patch('ipalib.x509.load_certificate_from_file')
+ @patch('ipaserver.install.certs.CertDB')
+ def test_certfile_expiration(self, mock_certdb, mock_load_cert):
+ cert = IPACertificate(not_valid_after=datetime.now(tz=timezone.utc) +
+ timedelta(days=CERT_EXPIRATION_DAYS))
+ mock_load_cert.return_value = cert
+ mock_certdb.return_value = mock_CertDB({
+ 'Server-Cert cert-pki-ca': 'u,u,u',
+ }, expiration_days=CERT_EXPIRATION_DAYS)
+
+ framework = object()
+ registry.initialize(framework, config.Config)
+ f = IPAUserProvidedExpirationCheck(registry)
+
+ f.config.cert_expiration_days = '28'
+ self.results = capture_results(f)
+
+ assert len(self.results) == 3
+
+ for result in self.results.results:
+ assert result.result == constants.SUCCESS
+ assert result.source == 'ipahealthcheck.ipa.certs'
+ assert result.check == 'IPAUserProvidedExpirationCheck'
+
+ @patch('ipalib.x509.load_certificate_from_file')
+ @patch('ipaserver.install.certs.CertDB')
+ def test_certfile_expiration_warning(self, mock_certdb, mock_load_cert):
+ cert = IPACertificate(not_valid_after=datetime.now(tz=timezone.utc) +
+ timedelta(days=7))
+ mock_load_cert.return_value = cert
+ mock_certdb.return_value = mock_CertDB({
+ 'Server-Cert cert-pki-ca': 'u,u,u',
+ }, expiration_days=7)
+
+ framework = object()
+ registry.initialize(framework, config.Config)
+ f = IPAUserProvidedExpirationCheck(registry)
+
+ f.config.cert_expiration_days = str(CERT_EXPIRATION_DAYS)
+ self.results = capture_results(f)
+
+ assert len(self.results) == 3
+
+ for result in self.results.results:
+ assert result.result == constants.WARNING
+ assert result.source == 'ipahealthcheck.ipa.certs'
+ assert result.check == 'IPAUserProvidedExpirationCheck'
+ assert result.kw.get('days') == 6
+
+ @patch('ipalib.x509.load_certificate_from_file')
+ @patch('ipaserver.install.certs.CertDB')
+ def test_certfile_expiration_expired(self, mock_certdb, mock_load_cert):
+ cert = IPACertificate(not_valid_after=datetime.now(tz=timezone.utc) +
+ timedelta(days=-100))
+ mock_load_cert.return_value = cert
+ mock_certdb.return_value = mock_CertDB({
+ 'Server-Cert cert-pki-ca': 'u,u,u',
+ }, expiration_days=-100)
+
+ framework = object()
+ registry.initialize(framework, config.Config)
+ f = IPAUserProvidedExpirationCheck(registry)
+
+ f.config.cert_expiration_days = str(CERT_EXPIRATION_DAYS)
+ self.results = capture_results(f)
+
+ assert len(self.results) == 3
+
+ for result in self.results.results:
+ assert result.result == constants.ERROR
+ assert result.source == 'ipahealthcheck.ipa.certs'
+ assert result.check == 'IPAUserProvidedExpirationCheck'
+ assert 'expiration_date' in result.kw
--
2.48.1

View File

@ -1,146 +0,0 @@
From 4127298975d4625006a8cb5183583df34c67107d Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
Date: Thu, 27 Feb 2025 12:17:41 -0500
Subject: [PATCH] Warn if krbLastSuccessfulAuth replication is enabled
This is controlled in IPA via the ipaconfigstring value including
KDC:Disable Last Success.
Fixes: https://github.com/freeipa/freeipa-healthcheck/issues/315
Signed-off-by: Rob Crittenden <rcritten@redhat.com>
---
setup.py | 1 +
src/ipahealthcheck/ipa/config.py | 46 +++++++++++++++++++++++++++
tests/test_ipa_config.py | 54 ++++++++++++++++++++++++++++++++
3 files changed, 101 insertions(+)
create mode 100644 src/ipahealthcheck/ipa/config.py
create mode 100644 tests/test_ipa_config.py
diff --git a/setup.py b/setup.py
index fe47b55..7126082 100644
--- a/setup.py
+++ b/setup.py
@@ -39,6 +39,7 @@ setup(
# plugin modules for ipahealthcheck.ipa registry
'ipahealthcheck.ipa': [
'ipacerts = ipahealthcheck.ipa.certs',
+ 'ipaconfig = ipahealthcheck.ipa.config',
'ipadna = ipahealthcheck.ipa.dna',
'ipadns = ipahealthcheck.ipa.idns',
'ipafiles = ipahealthcheck.ipa.files',
diff --git a/src/ipahealthcheck/ipa/config.py b/src/ipahealthcheck/ipa/config.py
new file mode 100644
index 0000000..c14ee8f
--- /dev/null
+++ b/src/ipahealthcheck/ipa/config.py
@@ -0,0 +1,46 @@
+
+# Copyright (C) 2025 FreeIPA Contributors see COPYING for license
+#
+
+from ipahealthcheck.ipa.plugin import IPAPlugin, registry
+from ipahealthcheck.core.plugin import Result, duration
+from ipahealthcheck.core import constants
+
+from ipalib import api
+
+
+@registry
+class IPAkrbLastSuccessfulAuth(IPAPlugin):
+ """Warn if krbLastSuccessfulAuth is enabled. It can cause
+ performance issues.
+ """
+ requires = ('dirsrv',)
+
+ @duration
+ def check(self):
+ try:
+ result = api.Command.config_show()
+ except Exception as e:
+ yield Result(self, constants.ERROR,
+ key='krbLastSuccessfulAuth',
+ msg='Request for configuration failed, %s' % e)
+ return
+
+ configstring = result["result"].get(
+ "ipaconfigstring", []
+ )
+
+ if 'KDC:Disable Last Success' not in configstring:
+ yield Result(
+ self,
+ constants.WARNING,
+ key='krbLastSuccessfulAuth',
+ configstring=configstring,
+ msg="Last Successful Auth is enabled. It may cause "
+ "performance problems.")
+ else:
+ yield Result(
+ self,
+ constants.SUCCESS,
+ key='krbLastSuccessfulAuth'
+ )
diff --git a/tests/test_ipa_config.py b/tests/test_ipa_config.py
new file mode 100644
index 0000000..5cc0c1e
--- /dev/null
+++ b/tests/test_ipa_config.py
@@ -0,0 +1,54 @@
+#
+# Copyright (C) 2025 FreeIPA Contributors see COPYING for license
+#
+
+from util import capture_results, m_api
+from base import BaseTest
+from ipahealthcheck.core import config, constants
+from ipahealthcheck.ipa.plugin import registry
+from ipahealthcheck.ipa.config import IPAkrbLastSuccessfulAuth
+
+
+class TestkrbLastSuccessfulAuth(BaseTest):
+
+ def test_last_success_disabled(self):
+ """Test that no warning is issued in the default config"""
+
+ m_api.Command.config_show.side_effect = [{
+ 'result': {
+ 'ipaconfigstring': ['KDC:Disable Last Success',]
+ }
+ }]
+
+ framework = object()
+ registry.initialize(framework, config.Config())
+ f = IPAkrbLastSuccessfulAuth(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.config'
+ assert result.check == 'IPAkrbLastSuccessfulAuth'
+
+ def test_last_success_enabled(self):
+ """Test that a warning is issued when krbLastSuccessfulAuth is
+ replicated.
+ """
+
+ m_api.Command.config_show.side_effect = [{
+ 'result': {
+ 'ipaconfigstring': ['',]
+ }
+ }]
+
+ framework = object()
+ registry.initialize(framework, config.Config())
+ f = IPAkrbLastSuccessfulAuth(registry)
+ self.results = capture_results(f)
+
+ assert len(self.results) == 1
+ result = self.results.results[0]
+ assert result.result == constants.WARNING
+ assert result.source == 'ipahealthcheck.ipa.config'
+ assert result.check == 'IPAkrbLastSuccessfulAuth'
--
2.48.1

View File

@ -1,103 +0,0 @@
From 1607150d30a6f7da4dc4661b4e946d037da513e3 Mon Sep 17 00:00:00 2001
From: David Hanina <dhanina@redhat.com>
Date: Wed, 26 Mar 2025 09:26:39 +0100
Subject: [PATCH] Warn about unexpected umask
We should let users know, that their umask set wrong to
an unsupported value, it does not make much sense to check file
permissions when wrong umask is set.
Fixes: https://github.com/freeipa/freeipa-healthcheck/issues/353
Signed-off-by: David Hanina <dhanina@redhat.com>
---
src/ipahealthcheck/core/files.py | 25 ++++++++++++++++++++++++-
tests/test_core_files.py | 19 ++++++++++++++++++-
2 files changed, 42 insertions(+), 2 deletions(-)
diff --git a/src/ipahealthcheck/core/files.py b/src/ipahealthcheck/core/files.py
index 32bc5b2..6749d26 100644
--- a/src/ipahealthcheck/core/files.py
+++ b/src/ipahealthcheck/core/files.py
@@ -12,6 +12,19 @@ from ipahealthcheck.core.plugin import Result, duration
logger = logging.getLogger()
+EXPECTED_UMASK = 0o022
+
+
+def current_umask():
+ """
+ Retrieves current umask by setting it temporarily
+
+ :returns: int umask
+ """
+ umask = os.umask(EXPECTED_UMASK)
+ os.umask(umask)
+ return umask
+
class FileCheck:
"""Generic check to validate permission and ownership of files
@@ -41,6 +54,16 @@ class FileCheck:
yield Result(self, constants.ERROR, key=file,
msg='Code format is incorrect for file')
+ umask = current_umask()
+ correct_umask = umask == EXPECTED_UMASK
+ if not correct_umask:
+ yield Result(self, constants.WARNING, type='umask',
+ expected=oct(EXPECTED_UMASK), got=oct(umask),
+ msg='Unexpected umask %s expected %s, '
+ 'skipping file permissions.' %
+ ('0o' + format(umask, 'o').zfill(3),
+ '0o' + format(EXPECTED_UMASK, 'o').zfill(3)))
+
for (path, owner, group, mode) in process_files:
if not isinstance(owner, tuple):
owner = tuple((owner,))
@@ -58,7 +81,7 @@ class FileCheck:
stat = os.stat(path)
fmode = str(oct(stat.st_mode)[-4:])
key = '%s_mode' % path.replace('/', '_')
- if fmode not in mode:
+ if correct_umask and fmode not in mode:
if len(mode) == 1:
modes = mode[0]
else:
diff --git a/tests/test_core_files.py b/tests/test_core_files.py
index d308410..094305a 100644
--- a/tests/test_core_files.py
+++ b/tests/test_core_files.py
@@ -9,7 +9,7 @@ from util import m_api
from util import capture_results
from ipahealthcheck.core import config
-from ipahealthcheck.core.files import FileCheck
+from ipahealthcheck.core.files import EXPECTED_UMASK, FileCheck
from ipahealthcheck.core import constants
from ipahealthcheck.core.plugin import Results
from ipahealthcheck.ipa.files import IPAFileCheck
@@ -303,3 +303,20 @@ def test_ipa_files_format(mock_pkinit):
for result in results.results:
assert result.result in (constants.SUCCESS, constants.WARNING)
+
+
+@patch('os.umask')
+def test_bad_umask(mock_umask):
+ mock_umask.return_value = 0o027
+
+ f = FileCheck()
+ f.files = files
+
+ results = capture_results(f)
+ my_results = get_results(results, 'umask')
+ assert my_results.results[0].result == constants.WARNING
+ assert my_results.results[0].kw.get('got') == oct(0o027)
+ assert my_results.results[0].kw.get('expected') == oct(EXPECTED_UMASK)
+ assert my_results.results[0].kw.get('type') == 'umask'
+ assert my_results.results[0].kw.get('msg') == \
+ 'Unexpected umask 0o027 expected 0o022, skipping file permissions.'
--
2.49.0

View File

@ -1,139 +0,0 @@
From 57cda16e67c5b5b5972452b8623534b0a567a264 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
Date: Mon, 23 Jun 2025 16:07:34 -0400
Subject: [PATCH] Don't rely on order in trust agent/controller role check
The code expected that the local server would always be the
first one returned. Instead loop through the returned list
to find the current server and set the state based on that.
Fixes: https://github.com/freeipa/freeipa-healthcheck/issues/356
Signed-off-by: Rob Crittenden <rcritten@redhat.com>
---
src/ipahealthcheck/ipa/plugin.py | 15 ++++---
tests/test_ipa_trust.py | 71 +++++++++++++++++++++++++++++++-
2 files changed, 79 insertions(+), 7 deletions(-)
diff --git a/src/ipahealthcheck/ipa/plugin.py b/src/ipahealthcheck/ipa/plugin.py
index f1a325c..efaa947 100644
--- a/src/ipahealthcheck/ipa/plugin.py
+++ b/src/ipahealthcheck/ipa/plugin.py
@@ -35,6 +35,13 @@ class IPARegistry(Registry):
self.trust_controller = False
self.ca_configured = False
+ def has_role(self, roles):
+ for role in roles:
+ if role.get('server_server') == api.env.host:
+ if role.get('status') == 'enabled':
+ return True
+ return False
+
def initialize(self, framework, config, options=None):
super().initialize(framework, config)
# deferred import for mock
@@ -81,12 +88,8 @@ class IPARegistry(Registry):
component_services=['ADTRUST']
),
)
- role = roles[0].status(api)[0]
- if role.get('status') == 'enabled':
- self.trust_agent = True
- role = roles[1].status(api)[0]
- if role.get('status') == 'enabled':
- self.trust_controller = True
+ self.trust_agent = self.has_role(roles[0].status(api))
+ self.trust_controller = self.has_role(roles[1].status(api))
registry = IPARegistry()
diff --git a/tests/test_ipa_trust.py b/tests/test_ipa_trust.py
index 6c4754a..0faa702 100644
--- a/tests/test_ipa_trust.py
+++ b/tests/test_ipa_trust.py
@@ -11,7 +11,8 @@ from util import capture_results
from util import m_api
from ipahealthcheck.core import config, constants
-from ipahealthcheck.ipa.plugin import registry
+from ipahealthcheck.core.plugin import Results
+from ipahealthcheck.ipa.plugin import registry, IPARegistry
from ipahealthcheck.ipa.trust import (IPATrustAgentCheck,
IPATrustDomainsCheck,
IPADomainCheck,
@@ -1287,3 +1288,71 @@ class TestPackageCheck(BaseTest):
assert result.source == 'ipahealthcheck.ipa.trust'
assert result.check == 'IPATrustPackageCheck'
sys.modules['ipaserver.install'] = save
+
+
+class TestHasRole(BaseTest):
+ """Verify that the output of server-role-find which is used to
+ determine whether a host is a trust agent or controller
+ (or neither) isn't dependent upon the order the hosts are
+ returned.
+
+ Only trust agent is tested here but there is no difference
+ between an agent and a trust in the way they are stored in
+ a server role.
+ """
+ def test_role_last(self):
+ self.results = Results()
+ reg = IPARegistry()
+
+ roles = [
+ {
+ "role_servrole": "AD trust agent",
+ "server_server": "replica.ipa.example",
+ "status": "absent",
+ },
+ {
+ "role_servrole": "AD trust agent",
+ "server_server": "server.ipa.example",
+ "status": "enabled",
+ },
+ ]
+
+ assert reg.has_role(roles) is True
+
+ def test_role_first(self):
+ self.results = Results()
+ reg = IPARegistry()
+
+ roles = [
+ {
+ "role_servrole": "AD trust agent",
+ "server_server": "server.ipa.example",
+ "status": "enabled",
+ },
+ {
+ "role_servrole": "AD trust agent",
+ "server_server": "replica.ipa.example",
+ "status": "absent",
+ },
+ ]
+
+ assert reg.has_role(roles) is True
+
+ def test_no_role(self):
+ self.results = Results()
+ reg = IPARegistry()
+
+ roles = [
+ {
+ "role_servrole": "AD trust agent",
+ "server_server": "server.ipa.example",
+ "status": "absent",
+ },
+ {
+ "role_servrole": "AD trust agent",
+ "server_server": "replica.ipa.example",
+ "status": "enabled",
+ },
+ ]
+
+ assert reg.has_role(roles) is False
--
2.49.0

View File

@ -16,8 +16,8 @@
%bcond_without tests
Name: %{prefix}-healthcheck
Version: 0.16
Release: 9%{?dist}
Version: 0.19
Release: 1%{?dist}
Summary: Health check tool for %{productname}
BuildArch: noarch
License: GPLv3
@ -26,17 +26,6 @@ Source0: https://github.com/freeipa/freeipa-healthcheck/archive/%{version
Source1: ipahealthcheck.conf
Patch0001: 0001-Remove-ipaclustercheck.patch
Patch0002: 0002-Don-t-fail-if-a-service-name-cannot-be-looked-up-in-.patch
Patch0003: 0003-Temporarily-disable-the-ipa-ods-exporter-service-sta.patch
Patch0004: 0004-Skip-DogtagCertsConfigCheck-for-PKI-versions-11.5.0.patch
Patch0005: 0005-test-Handle-PKI-11.5.0-not-storing-certs-in-CS.cfg.patch
Patch0006: 0006-Fixes-log-file-permissions-as-per-CIS-benchmark.patch
Patch0007: 0007-Fix-some-file-mode-format-issues.patch
Patch0008: 0008-Allow-WARNING-in-the-files-test.patch
Patch0009: 0009-Check-user-provided-certificates-for-expiration.patch
Patch0010: 0010-Warn-if-krbLastSuccessfulAuth-replication-is-enabled.patch
Patch0011: 0011-Warn-about-unexpected-umask.patch
Patch0012: 0012-Don-t-rely-on-order-in-trust-agent-controller-role-c.patch
Requires: %{name}-core = %{version}-%{release}
Requires: %{prefix}-server
@ -166,6 +155,13 @@ PYTHONPATH=src PATH=$PATH:$RPM_BUILD_ROOT/usr/bin pytest-3 tests/test_*
%changelog
* Tue Sep 23 2025 Rob Crittenden <rcritten@redhat.com> - 0.19-1
- Update to 0.19 release
- Add a message if not a trust agent/controller (RHEL-116896)
- Check that expected NSS token matches the current FIPS state (RHEL-116897)
- Add /etc/pki/tls/certs/ directory to file checker (RHEL-116898)
- Check that allowed_uids in the SSSD config is valid (RHEL-79092)
* Mon Jun 30 2025 Rob Crittenden <rcritten@redhat.com> - 0.16-9
- Don't rely on order in trust roles (RHEL-99531)

View File

@ -1 +1 @@
SHA512 (0.16.tar.gz) = d1be0159773e0dd663a9c6dfea586fdba01a88ac1ab0e6a13459fc563b4ef814ac3c2597ad7a0bd1ef2132eb54fe9020f46726465d7cfaf3c5a124894296f2eb
SHA512 (0.19.tar.gz) = 7f40e9451c4207f4bbb02644ba8abd14eed3b818227f6ad5c5957487a4a401f294f2f8d57a663f06d84b6926864baec0494d88ae02542aa15eef08ff001da734