diff --git a/data/templates/limits.conf.j2 b/data/templates/limits.conf.j2 index 70ac9d8c..c5fe74d0 100755 --- a/data/templates/limits.conf.j2 +++ b/data/templates/limits.conf.j2 @@ -66,4 +66,8 @@ # ftp - chroot /ftp # @student - maxlogins 4 +{% if max_sessions and max_sessions | int > 0 -%} +* - maxsyslogins {{ max_sessions }} +{% endif -%} + # End of file diff --git a/scripts/hostcfgd b/scripts/hostcfgd index 12f2fd1b..f588e65d 100644 --- a/scripts/hostcfgd +++ b/scripts/hostcfgd @@ -40,9 +40,14 @@ LINUX_DEFAULT_PASS_MAX_DAYS = 99999 LINUX_DEFAULT_PASS_WARN_AGE = 7 # Ssh min-max values -SSH_MIN_VALUES={"authentication_retries": 3, "login_timeout": 1, "ports": 1} -SSH_MAX_VALUES={"authentication_retries": 100, "login_timeout": 600, "ports": 65535} -SSH_CONFIG_NAMES={"authentication_retries": "MaxAuthTries" , "login_timeout": "LoginGraceTime"} +SSH_MIN_VALUES={"authentication_retries": 3, "login_timeout": 1, "ports": 1, + "inactivity_timeout": 0, "max_sessions": 0} +SSH_MAX_VALUES={"authentication_retries": 100, "login_timeout": 600, + "ports": 65535, "inactivity_timeout": 35000, + "max_sessions": 100} +SSH_CONFIG_NAMES={"authentication_retries": "MaxAuthTries", + "login_timeout": "LoginGraceTime", "ports": "Port", + "inactivity_timeout": "ClientAliveInterval"} ACCOUNT_NAME = 0 # index of account name AGE_DICT = { 'MAX_DAYS': {'REGEX_DAYS': r'^PASS_MAX_DAYS[ \t]*(?P\d*)', 'DAYS': 'max_days', 'CHAGE_FLAG': '-M '}, @@ -911,9 +916,15 @@ class SshServer(object): syslog.syslog(syslog.LOG_ERR, "Ssh {} {} out of range".format(key, value)) elif key in SSH_CONFIG_NAMES: # search replace configuration - if not in config file - append + if key == "inactivity_timeout": + # translate min to sec. + value = int(value) * 60 kv_str = "{} {}".format(SSH_CONFIG_NAMES[key], str(value)) # name +' '+ value format modify_single_file_inplace(SSH_CONFG_TMP,['-E', "/^#?" + SSH_CONFIG_NAMES[key]+"/{h;s/.*/"+ kv_str + "/};${x;/^$/{s//" + kv_str + "/;H};x}"]) + elif key in ['max_sessions']: + # Ignore, these parameters handled in other modules + continue else: syslog.syslog(syslog.LOG_ERR, "Failed to update sshd config file - wrong key {}".format(key)) @@ -1128,16 +1139,31 @@ class PamLimitsCfg(object): self.config_db = config_db self.hwsku = "" self.type = "" + self.max_sessions = None # Load config from ConfigDb and render config file/ def update_config_file(self): device_metadata = self.config_db.get_table('DEVICE_METADATA') - if "localhost" not in device_metadata: + ssh_server_policies = {} + try: + ssh_server_policies = self.config_db.get_table('SSH_SERVER') + except KeyError: + """Dont throw except in case we don`t have SSH_SERVER config.""" + pass + + if "localhost" not in device_metadata and "POLICIES" not in ssh_server_policies: return self.read_localhost_config(device_metadata["localhost"]) + self.read_max_sessions_config(ssh_server_policies.get("POLICIES", None)) self.render_conf_file() + # Read max_sessions config + def read_max_sessions_config(self, ssh_server_policies): + if ssh_server_policies is not None: + max_sess_cfg = ssh_server_policies.get('max_sessions', 0) + self.max_sessions = max_sess_cfg if max_sess_cfg != 0 else None + # Read localhost config def read_localhost_config(self, localhost): if "hwsku" in localhost: @@ -1154,7 +1180,6 @@ class PamLimitsCfg(object): def render_conf_file(self): env = jinja2.Environment(loader=jinja2.FileSystemLoader('/'), trim_blocks=True) env.filters['sub'] = sub - try: template_file = os.path.abspath(PAM_LIMITS_CONF_TEMPLATE) template = env.get_template(template_file) @@ -1168,7 +1193,8 @@ class PamLimitsCfg(object): template = env.get_template(template_file) limits_conf = template.render( hwsku=self.hwsku, - type=self.type) + type=self.type, + max_sessions=self.max_sessions) with open(LIMITS_CONF, 'w') as f: f.write(limits_conf) except Exception as e: @@ -1500,6 +1526,44 @@ class FipsCfg(object): syslog.syslog(syslog.LOG_INFO, f'FipsCfg: update the FIPS enforce option {self.enforce}.') loader.set_fips(image, self.enforce) + +class SerialConsoleCfg: + + def __init__(self): + self.cache = {} + + def load(self, cli_sessions_conf): + self.cache = cli_sessions_conf or {} + syslog.syslog(syslog.LOG_INFO, + f'SerialConsoleCfg: Initial config: {self.cache}') + + def update_serial_console_cfg(self, key, data): + ''' + Apply config flow: + inactivity_timeout | set here AND in ssh_config flow | serial-config.service restarted. + max_sessions | set by PamLimitsCfg | serial-config.service DOESNT restarted. + sysrq_capabilities | set here | serial-config.service restarted. + ''' + + # max_sessions applied in limits.conf by PamLimitsCfg + if key in ['max_sessions']: + syslog.syslog(syslog.LOG_DEBUG, + f'SerialConsoleCfg: skip max_sessions in SerialConsoleCfg apply config handler') + return + + if self.cache.get(key, {}) != data: + ''' Config changed, need to restart the serial-config.service ''' + syslog.syslog(syslog.LOG_INFO, f'Set serial-config parameter {key} value: {data}') + try: + run_cmd('sudo service serial-config restart', True, True) + except Exception: + syslog.syslog(syslog.LOG_ERR, f'Failed to update {key} serial-config.service config') + return + self.cache.update({key: data}) + + return + + class HostConfigDaemon: def __init__(self): self.state_db_conn = DBConnector(STATE_DB, 0) @@ -1551,6 +1615,9 @@ class HostConfigDaemon: # Initialize FipsCfg self.fipscfg = FipsCfg(self.state_db_conn) + # Initialize SerialConsoleCfg + self.serialconscfg = SerialConsoleCfg() + def load(self, init_data): aaa = init_data['AAA'] tacacs_global = init_data['TACPLUS'] @@ -1571,6 +1638,7 @@ class HostConfigDaemon: ntp_global = init_data.get(swsscommon.CFG_NTP_GLOBAL_TABLE_NAME) ntp_servers = init_data.get(swsscommon.CFG_NTP_SERVER_TABLE_NAME) ntp_keys = init_data.get(swsscommon.CFG_NTP_KEY_TABLE_NAME) + serial_console = init_data.get('SERIAL_CONSOLE', {}) self.aaacfg.load(aaa, tacacs_global, tacacs_server, radius_global, radius_server) self.iptables.load(lpbk_table) @@ -1579,11 +1647,12 @@ class HostConfigDaemon: self.sshscfg.load(ssh_server) self.devmetacfg.load(dev_meta) self.mgmtifacecfg.load(mgmt_ifc, mgmt_vrf) - self.rsyslogcfg.load(syslog_cfg, syslog_srv) self.dnscfg.load(dns) self.fipscfg.load(fips_cfg) self.ntpcfg.load(ntp_global, ntp_servers, ntp_keys) + self.serialconscfg.load(serial_console) + self.pamLimitsCfg.update_config_file() # Update AAA with the hostname self.aaacfg.hostname_update(self.devmetacfg.hostname) @@ -1605,6 +1674,8 @@ class HostConfigDaemon: def ssh_handler(self, key, op, data): self.sshscfg.policies_update(key, data) + self.pamLimitsCfg.update_config_file() + syslog.syslog(syslog.LOG_INFO, 'SSH Update: key: {}, op: {}, data: {}'.format(key, op, data)) def tacacs_server_handler(self, key, op, data): @@ -1716,6 +1787,10 @@ class HostConfigDaemon: data = self.config_db.get_table("FIPS") self.fipscfg.fips_handler(data) + def serial_console_config_handler(self, key, op, data): + syslog.syslog(syslog.LOG_INFO, 'SERIAL_CONSOLE table handler...') + self.serialconscfg.update_serial_console_cfg(key, data) + def wait_till_system_init_done(self): # No need to print the output in the log file so using the "--quiet" # flag @@ -1742,6 +1817,8 @@ class HostConfigDaemon: self.config_db.subscribe('RADIUS_SERVER', make_callback(self.radius_server_handler)) self.config_db.subscribe('PASSW_HARDENING', make_callback(self.passwh_handler)) self.config_db.subscribe('SSH_SERVER', make_callback(self.ssh_handler)) + # Handle SERIAL_CONSOLE + self.config_db.subscribe('SERIAL_CONSOLE', make_callback(self.serial_console_config_handler)) # Handle IPTables configuration self.config_db.subscribe('LOOPBACK_INTERFACE', make_callback(self.lpbk_handler)) # Handle updates to src intf changes in radius diff --git a/tests/hostcfgd/hostcfgd_test.py b/tests/hostcfgd/hostcfgd_test.py index 2eaef658..fc500aea 100644 --- a/tests/hostcfgd/hostcfgd_test.py +++ b/tests/hostcfgd/hostcfgd_test.py @@ -121,6 +121,48 @@ def test_loopback_update(self): ]) +class TestSerialConsoleCfgd(TestCase): + """ + Test hostcfd daemon - SerialConsoleCfg + """ + def setUp(self): + MockConfigDb.CONFIG_DB['SERIAL_CONSOLE'] = { + 'POLICIES': {'inactivity-timeout': '15', 'sysrq-capabilities': 'disabled'} + } + + def tearDown(self): + MockConfigDb.CONFIG_DB = {} + + def test_serial_console_update_cfg(self): + with mock.patch('hostcfgd.subprocess') as mocked_subprocess: + popen_mock = mock.Mock() + attrs = {'communicate.return_value': ('output', 'error')} + popen_mock.configure_mock(**attrs) + mocked_subprocess.Popen.return_value = popen_mock + + serialcfg = hostcfgd.SerialConsoleCfg() + serialcfg.update_serial_console_cfg( + 'POLICIES', MockConfigDb.CONFIG_DB['SERIAL_CONSOLE']['POLICIES']) + mocked_subprocess.check_call.assert_has_calls([ + call('sudo service serial-config restart'), + ]) + + def test_serial_console_is_caching_config(self): + with mock.patch('hostcfgd.subprocess') as mocked_subprocess: + popen_mock = mock.Mock() + attrs = {'communicate.return_value': ('output', 'error')} + popen_mock.configure_mock(**attrs) + mocked_subprocess.Popen.return_value = popen_mock + + serialcfg = hostcfgd.SerialConsoleCfg() + serialcfg.cache['POLICIES'] = MockConfigDb.CONFIG_DB['SERIAL_CONSOLE']['POLICIES'] + + serialcfg.update_serial_console_cfg( + 'POLICIES', MockConfigDb.CONFIG_DB['SERIAL_CONSOLE']['POLICIES']) + + mocked_subprocess.check_call.assert_not_called() + + class TestHostcfgdDaemon(TestCase): def setUp(self): diff --git a/tests/hostcfgd/sample_output/SSH_SERVER/sshd_config.old b/tests/hostcfgd/sample_output/SSH_SERVER/sshd_config.old index adbb09a8..af536e44 100644 --- a/tests/hostcfgd/sample_output/SSH_SERVER/sshd_config.old +++ b/tests/hostcfgd/sample_output/SSH_SERVER/sshd_config.old @@ -118,4 +118,4 @@ Subsystem sftp /usr/lib/openssh/sftp-server # PermitTTY no # ForceCommand cvs server PermitRootLogin yes -ClientAliveInterval 120 +ClientAliveInterval 900 diff --git a/tests/hostcfgd/sample_output/SSH_SERVER_default_values/sshd_config b/tests/hostcfgd/sample_output/SSH_SERVER_default_values/sshd_config index 20e09bf1..c96b8a06 100644 --- a/tests/hostcfgd/sample_output/SSH_SERVER_default_values/sshd_config +++ b/tests/hostcfgd/sample_output/SSH_SERVER_default_values/sshd_config @@ -118,4 +118,4 @@ Subsystem sftp /usr/lib/openssh/sftp-server # PermitTTY no # ForceCommand cvs server PermitRootLogin yes -ClientAliveInterval 120 +ClientAliveInterval 900 diff --git a/tests/hostcfgd/sample_output/SSH_SERVER_modify_all/sshd_config b/tests/hostcfgd/sample_output/SSH_SERVER_modify_all/sshd_config index ac74ed14..ee7dcfec 100644 --- a/tests/hostcfgd/sample_output/SSH_SERVER_modify_all/sshd_config +++ b/tests/hostcfgd/sample_output/SSH_SERVER_modify_all/sshd_config @@ -119,4 +119,4 @@ Subsystem sftp /usr/lib/openssh/sftp-server # PermitTTY no # ForceCommand cvs server PermitRootLogin yes -ClientAliveInterval 120 +ClientAliveInterval 900 diff --git a/tests/hostcfgd/sample_output/SSH_SERVER_modify_authentication_retries/sshd_config b/tests/hostcfgd/sample_output/SSH_SERVER_modify_authentication_retries/sshd_config index 23bdb7dd..78cd5946 100644 --- a/tests/hostcfgd/sample_output/SSH_SERVER_modify_authentication_retries/sshd_config +++ b/tests/hostcfgd/sample_output/SSH_SERVER_modify_authentication_retries/sshd_config @@ -118,4 +118,4 @@ Subsystem sftp /usr/lib/openssh/sftp-server # PermitTTY no # ForceCommand cvs server PermitRootLogin yes -ClientAliveInterval 120 +ClientAliveInterval 900 diff --git a/tests/hostcfgd/sample_output/SSH_SERVER_modify_login_timeout/sshd_config b/tests/hostcfgd/sample_output/SSH_SERVER_modify_login_timeout/sshd_config index 12097b16..2b8ad503 100644 --- a/tests/hostcfgd/sample_output/SSH_SERVER_modify_login_timeout/sshd_config +++ b/tests/hostcfgd/sample_output/SSH_SERVER_modify_login_timeout/sshd_config @@ -118,4 +118,4 @@ Subsystem sftp /usr/lib/openssh/sftp-server # PermitTTY no # ForceCommand cvs server PermitRootLogin yes -ClientAliveInterval 120 +ClientAliveInterval 900 diff --git a/tests/hostcfgd/sample_output/SSH_SERVER_modify_ports/sshd_config b/tests/hostcfgd/sample_output/SSH_SERVER_modify_ports/sshd_config index 6d0811ad..bc73a7d7 100644 --- a/tests/hostcfgd/sample_output/SSH_SERVER_modify_ports/sshd_config +++ b/tests/hostcfgd/sample_output/SSH_SERVER_modify_ports/sshd_config @@ -120,4 +120,4 @@ Subsystem sftp /usr/lib/openssh/sftp-server # PermitTTY no # ForceCommand cvs server PermitRootLogin yes -ClientAliveInterval 120 +ClientAliveInterval 900 diff --git a/tests/hostcfgd/test_ssh_server_vectors.py b/tests/hostcfgd/test_ssh_server_vectors.py index 401c3e84..8a14d1fd 100644 --- a/tests/hostcfgd/test_ssh_server_vectors.py +++ b/tests/hostcfgd/test_ssh_server_vectors.py @@ -11,6 +11,8 @@ "authentication_retries": "6", "login_timeout": "120", "ports": "22", + "inactivity_timeout": "15", + "max_sessions": "0", } }, "DEVICE_METADATA": { @@ -35,6 +37,12 @@ "num_dumps": "3", "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" } + }, + "SERIAL_CONSOLE": { + "POLICIES":{ + "inactivity_timeout": "15", + "sysrq_capabilities": "disabled" + } } }, "modify_authentication_retries":{ @@ -43,6 +51,8 @@ "authentication_retries": "12", "login_timeout": "120", "ports": "22", + "inactivity_timeout": "15", + "max_sessions": "0", } }, "DEVICE_METADATA": { @@ -67,6 +77,12 @@ "num_dumps": "3", "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" } + }, + "SERIAL_CONSOLE": { + "POLICIES":{ + "inactivity_timeout": "15", + "sysrq_capabilities": "disabled" + } } }, "modify_login_timeout":{ @@ -75,6 +91,8 @@ "authentication_retries": "6", "login_timeout": "60", "ports": "22", + "inactivity_timeout": "15", + "max_sessions": "0", } }, "DEVICE_METADATA": { @@ -99,6 +117,12 @@ "num_dumps": "3", "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" } + }, + "SERIAL_CONSOLE": { + "POLICIES":{ + "inactivity_timeout": "15", + "sysrq_capabilities": "disabled" + } } }, "modify_ports":{ @@ -107,6 +131,8 @@ "authentication_retries": "6", "login_timeout": "120", "ports": "22,23,24", + "inactivity_timeout": "15", + "max_sessions": "0", } }, "DEVICE_METADATA": { @@ -132,6 +158,12 @@ "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" } }, + "SERIAL_CONSOLE": { + "POLICIES":{ + "inactivity_timeout": "15", + "sysrq_capabilities": "disabled" + } + } }, "modify_all":{ "SSH_SERVER": { @@ -139,6 +171,8 @@ "authentication_retries": "16", "login_timeout": "140", "ports": "22,222", + "inactivity_timeout": "15", + "max_sessions": "0", } }, "DEVICE_METADATA": { @@ -163,6 +197,12 @@ "num_dumps": "3", "memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M" } + }, + "SERIAL_CONSOLE": { + "POLICIES":{ + "inactivity_timeout": "15", + "sysrq_capabilities": "disabled" + } } } }