From 7927cf51e04177d73f6cb836338f7540c1f629c5 Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Wed, 15 Jul 2026 16:21:31 +0530 Subject: [PATCH] mgr/dashboard: set MOTD fails on FIPS enabled systems MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Root cause: On FIPS-compliant systems, the OpenSSL provider rejects hashlib.md5() calls that lack the usedforsecurity=False flag, because MD5 is not an approved algorithm for security-sensitive use. This causes the MOTD set command — and the /api/motd create endpoint — to raise a ValueError and crash whenever a new MOTD is saved. Fix: Pass usedforsecurity=False to hashlib.md5(). This tells the FIPS provider that MD5 is being used only as a non-cryptographic checksum (to detect whether a user has already dismissed a specific MOTD message), which is a legitimate use-case that FIPS allows. Fixes: https://tracker.ceph.com/issues/78233 Signed-off-by: Aashish Sharma --- src/pybind/mgr/dashboard/plugins/motd.py | 5 +- src/pybind/mgr/dashboard/tests/test_motd.py | 108 ++++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 src/pybind/mgr/dashboard/tests/test_motd.py diff --git a/src/pybind/mgr/dashboard/plugins/motd.py b/src/pybind/mgr/dashboard/plugins/motd.py index c148c9bcca7..299bfbf5e06 100644 --- a/src/pybind/mgr/dashboard/plugins/motd.py +++ b/src/pybind/mgr/dashboard/plugins/motd.py @@ -59,9 +59,12 @@ class Motd(SP): expires = datetime_to_str(datetime_now() + delta) else: expires = '' + + # pylint: disable=unexpected-keyword-arg + md5 = hashlib.md5(message.encode(), usedforsecurity=False).hexdigest() value: str = json.dumps({ 'message': message, - 'md5': hashlib.md5(message.encode()).hexdigest(), + 'md5': md5, 'severity': severity.value, 'expires': expires }) diff --git a/src/pybind/mgr/dashboard/tests/test_motd.py b/src/pybind/mgr/dashboard/tests/test_motd.py new file mode 100644 index 00000000000..71fbd164cd9 --- /dev/null +++ b/src/pybind/mgr/dashboard/tests/test_motd.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# pylint: disable=protected-access + +import hashlib +import json +import unittest + +from ..plugins.motd import Motd, MotdSeverity +from . import KVStoreMockMixin # pylint: disable=no-name-in-module + + +class MotdTest(unittest.TestCase, KVStoreMockMixin): + """Unit tests for Motd._set_motd and the helpers it depends on.""" + + def setUp(self): + self.mock_kv_store() + self.plugin = Motd() + + # ------------------------------------------------------------------ + # _normalize_severity + # ------------------------------------------------------------------ + + def test_normalize_severity_from_enum(self): + result = self.plugin._normalize_severity(MotdSeverity.INFO) + self.assertEqual(result, MotdSeverity.INFO) + + def test_normalize_severity_from_valid_string(self): + for value, expected in ( + ('info', MotdSeverity.INFO), + ('warning', MotdSeverity.WARNING), + ('danger', MotdSeverity.DANGER), + ): + with self.subTest(value=value): + self.assertEqual(self.plugin._normalize_severity(value), expected) + + def test_normalize_severity_invalid_string_returns_none(self): + self.assertIsNone(self.plugin._normalize_severity('critical')) + + # ------------------------------------------------------------------ + # _set_motd — error paths + # ------------------------------------------------------------------ + + def test_set_motd_invalid_severity_returns_error(self): + rc, out, err = self.plugin._set_motd('critical', '0', 'hello') + self.assertEqual(rc, 1) + self.assertEqual(out, '') + self.assertIn('severity', err) + + def test_set_motd_invalid_expires_returns_error(self): + rc, out, err = self.plugin._set_motd('info', 'bad-expires', 'hello') + self.assertEqual(rc, 1) + self.assertEqual(out, '') + self.assertIn('expires', err) + + # ------------------------------------------------------------------ + # _set_motd — happy path: no expiry + # ------------------------------------------------------------------ + + def test_set_motd_no_expiry_returns_success(self): + rc, out, err = self.plugin._set_motd('info', '0', 'hello world') + self.assertEqual(rc, 0) + self.assertNotEqual(out, '') + self.assertEqual(err, '') + + def test_set_motd_no_expiry_stores_expected_fields(self): + self.plugin._set_motd('warning', '0', 'test message') + raw = self.plugin.get_option(self.plugin.NAME) + self.assertIsNotNone(raw) + data = json.loads(raw) + self.assertEqual(data['message'], 'test message') + self.assertEqual(data['severity'], 'warning') + self.assertEqual(data['expires'], '') # '0' → no expiry + + def test_set_motd_stores_correct_md5(self): + message = 'checksum test' + self.plugin._set_motd('danger', '0', message) + data = json.loads(self.plugin.get_option(self.plugin.NAME)) + expected_md5 = hashlib.md5( # pylint: disable=unexpected-keyword-arg + message.encode(), usedforsecurity=False).hexdigest() + self.assertEqual(data['md5'], expected_md5) + + # ------------------------------------------------------------------ + # _set_motd — happy path: with expiry + # ------------------------------------------------------------------ + + def test_set_motd_with_expiry_stores_iso_timestamp(self): + self.plugin._set_motd('info', '2h', 'expiring message') + data = json.loads(self.plugin.get_option(self.plugin.NAME)) + # expires must be a non-empty ISO 8601 string when a duration is given + self.assertTrue(data['expires'], 'expected a non-empty expires value') + + def test_set_motd_accepts_enum_severity(self): + rc, _, err = self.plugin._set_motd(MotdSeverity.DANGER, '0', 'enum severity') + self.assertEqual(rc, 0) + self.assertEqual(err, '') + data = json.loads(self.plugin.get_option(self.plugin.NAME)) + self.assertEqual(data['severity'], 'danger') + + # ------------------------------------------------------------------ + # Overwrite: second call replaces the stored value + # ------------------------------------------------------------------ + + def test_set_motd_overwrite(self): + self.plugin._set_motd('info', '0', 'first') + self.plugin._set_motd('warning', '0', 'second') + data = json.loads(self.plugin.get_option(self.plugin.NAME)) + self.assertEqual(data['message'], 'second') + self.assertEqual(data['severity'], 'warning') -- 2.47.3