From b887d9961ef08404fb60fa91c09cc59a4ff65dc9 Mon Sep 17 00:00:00 2001 From: Heitor Matsui Date: Tue, 18 Apr 2023 19:07:15 -0300 Subject: [PATCH] Audit local sysadmin user password periodically The platform as designed currently does not update the user configuration as an atomic operation, as it first update the database on [1] and then later run a manifest to configure the hosts on [2]. If the database is updated but the manifest apply fails, the hosts may remain with outdated user data locally. This commit adds an agent audit to periodically check if local user data is consistent with the database, and if it is not, then it calls the platform::users::runtime manifest to attempt to reconfigure the user correctly on the host. To enable this audit, this commit also adds a new RPC call to allow the agent to receive user config from the database, and because of this new RPC call, when upgrading from older loads, there will be an AttributeError exception in the active controller logs that is harmless and should be cleared when both controllers are upgraded, and the RPC call update_user_config is changed to allow running the runtime manifest only for a group of hosts. [1] https://opendev.org/starlingx/config/src/commit/2afa67e8730b62465b02041865934b4d42ded3f7/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/user.py#L240 [2] https://opendev.org/starlingx/config/src/commit/2afa67e8730b62465b02041865934b4d42ded3f7/sysinv/sysinv/sysinv/sysinv/conductor/manager.py#L8147 Test Plan: PASS: set log to debug and verify audit executing on the log PASS: update OS user sysadmin's password on active controller to trigger the sysinv host update call, force manifest apply failure on a host and verify that agent audit on the host detects the difference and attempts to reapply the manifest, then verify that the password is updated on the host Regression: PASS: update OS user sysadmin's password on active controller, verify manifest being reapplied on all hosts PASS: upgrade AIO-SX stx6 -> stx8 PASS: upgrade AIO-DX stx6 -> stx8 PASS: upgrade DC central cloud + orchestrated subcloud stx6 -> stx8 Closes-bug: 2017039 Change-Id: Iaa33dac08a8d246be366ee93ab0507fecddaeb4e Signed-off-by: Heitor Matsui --- sysinv/sysinv/sysinv/sysinv/agent/manager.py | 48 ++++++++++++++++++- .../sysinv/sysinv/sysinv/conductor/manager.py | 15 +++++- .../sysinv/sysinv/sysinv/conductor/rpcapi.py | 17 ++++++- 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/sysinv/sysinv/sysinv/sysinv/agent/manager.py b/sysinv/sysinv/sysinv/sysinv/agent/manager.py index 04b25ddcd4..95ccad6c81 100644 --- a/sysinv/sysinv/sysinv/sysinv/agent/manager.py +++ b/sysinv/sysinv/sysinv/sysinv/agent/manager.py @@ -93,7 +93,8 @@ agent_opts = [ audit_intervals_opts = [ cfg.IntOpt('default', default=60), cfg.IntOpt('inventory_audit', default=60), - cfg.IntOpt('lldp_audit', default=300) + cfg.IntOpt('lldp_audit', default=300), + cfg.IntOpt('security_audit', default=900), ] dpdk_opts = [ @@ -1313,6 +1314,51 @@ class AgentManager(service.PeriodicService): else: self._lldp_enable_and_report(icontext, rpcapi, self._ihost_uuid) + @periodic_task.periodic_task(spacing=CONF.agent_periodic_task_intervals.security_audit) + def _security_audit(self, context): + if not self._ihost_uuid: + return + + LOG.debug("Sysinv Agent Security Audit running.") + + # get sysadmin password locally + with open("/etc/shadow", "r") as f: + user_attrs = [] + lines = f.readlines() + for line in lines: + if "sysadmin" in line: + user_attrs = line.split(":") + break + if not user_attrs: + LOG.warn("No shadow entry found for 'sysadmin' user.") + return + + icontext = mycontext.get_admin_context() + rpcapi = conductor_rpcapi.ConductorAPI(topic=conductor_rpcapi.MANAGER_TOPIC) + + # get user information from the database + try: + iuser = rpcapi.get_iuser(icontext) + except RemoteError as e: + # ignore because active controller is not yet upgraded, + # so it's current load may not implement this RPC call + if "AttributeError" in str(e): + LOG.warn("Skip security audit. Upgrade in progress.") + else: + LOG.error("Failed to get user configuration via RPC.") + return + if not iuser.passwd_hash: + LOG.warn("No password configured for 'sysadmin' in the database.") + return + + # compare sysadmin password hash with the value retrieved from the + # database and trigger user config manifest reapply if values differ + if iuser.passwd_hash != user_attrs[1]: + LOG.info("Configuration mismatch for 'sysadmin' user, attempting to reconfigure...") + rpcapi.update_user_config(icontext, [self._ihost_uuid]) + else: + LOG.debug("No divergence found within 'sysadmin' user configuration.") + @utils.synchronized(LOCK_AGENT_ACTION, external=False) def agent_audit(self, context, host_uuid, force_updates, cinder_device=None): # perform inventory audit diff --git a/sysinv/sysinv/sysinv/sysinv/conductor/manager.py b/sysinv/sysinv/sysinv/sysinv/conductor/manager.py index 5516c6c52c..0e486a54a1 100644 --- a/sysinv/sysinv/sysinv/sysinv/conductor/manager.py +++ b/sysinv/sysinv/sysinv/sysinv/conductor/manager.py @@ -5797,6 +5797,16 @@ class ConductorManager(service.PeriodicService): system = self.dbapi.isystem_get_one() return system + def get_iuser(self, context): + """Return iuser object + + This method returns an iuser object + + :returns: iuser object, including all field + """ + user = self.dbapi.iuser_get_one() + return user + def get_ihost_by_macs(self, context, ihost_macs): """Finds ihost db entry based upon the mac list @@ -8144,7 +8154,7 @@ class ConductorManager(service.PeriodicService): cutils.touch( self._get_oam_runtime_apply_file(standby_controller=True)) - def update_user_config(self, context): + def update_user_config(self, context, hosts_uuid=None): """Update the user configuration""" LOG.info("update_user_config") @@ -8157,6 +8167,9 @@ class ConductorManager(service.PeriodicService): "personalities": personalities, "classes": ['platform::users::runtime'] } + if hosts_uuid: + config_dict.update({"hosts_uuid": hosts_uuid}) + self._config_apply_runtime_manifest(context, config_uuid, config_dict) def update_controller_rollback_flag(self, context): diff --git a/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py b/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py index 878581ebf3..55612816d2 100644 --- a/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py +++ b/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py @@ -190,6 +190,15 @@ class ConductorAPI(sysinv.openstack.common.rpc.proxy.RpcProxy): """ return self.call(context, self.make_msg('get_isystem',)) + def get_iuser(self, context): + """Return iuser object + + This method returns an iuser object + + :returns: iuser object, including all field + """ + return self.call(context, self.make_msg('get_iuser',)) + def get_ihost_by_macs(self, context, ihost_macs): """Finds ihost db entry based upon the mac list @@ -752,12 +761,16 @@ class ConductorAPI(sysinv.openstack.common.rpc.proxy.RpcProxy): """ return self.call(context, self.make_msg('update_oam_config')) - def update_user_config(self, context): + def update_user_config(self, context, hosts_uuid=None): """Synchronously, have the conductor update the user configuration. :param context: request context. + :param hosts_uuid: list of host_uuids to run user puppet manifest """ - return self.call(context, self.make_msg('update_user_config')) + return self.call( + context, + self.make_msg('update_user_config', hosts_uuid=hosts_uuid) + ) def update_controller_rollback_flag(self, context): """Synchronously, have a conductor update controller rollback flag