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] 2afa67e873/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/user.py (L240)
[2] 2afa67e873/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 <heitorvieira.matsui@windriver.com>
This commit is contained in:
Heitor Matsui 2023-04-18 19:07:15 -03:00
parent 2afa67e873
commit b887d9961e
3 changed files with 76 additions and 4 deletions

View File

@ -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

View File

@ -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):

View File

@ -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