Skip to content

Commit b7f26d4

Browse files
authored
[cli-sessions] Add support for cli-sessions feature (#99)
Add support for cli-sessions feature What I did: Added handlers for new SSH_SERVER and SERIAL_CONSOLE attributes. Why I did it: Give ability to: configure limit for active login sessions. configure ssh-server / serial console autologout timeout configure sysrq-capabilities (enable / disable) How I did it Add new service that responsible for serial configuration; Update existing flows for extended ssh-server configurations in hostcfgd; Add YANG model to support new configuration. How I verified it: Configure, reconfigure, and reset all new parameters, check if applicapple parameters was updated in the system. Added unittests.
1 parent d2170c9 commit b7f26d4

File tree

10 files changed

+171
-13
lines changed

10 files changed

+171
-13
lines changed

data/templates/limits.conf.j2

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,8 @@
6666
# ftp - chroot /ftp
6767
# @student - maxlogins 4
6868

69+
{% if max_sessions and max_sessions | int > 0 -%}
70+
* - maxsyslogins {{ max_sessions }}
71+
{% endif -%}
72+
6973
# End of file

scripts/hostcfgd

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,14 @@ LINUX_DEFAULT_PASS_MAX_DAYS = 99999
5454
LINUX_DEFAULT_PASS_WARN_AGE = 7
5555

5656
# Ssh min-max values
57-
SSH_MIN_VALUES={"authentication_retries": 3, "login_timeout": 1, "ports": 1}
58-
SSH_MAX_VALUES={"authentication_retries": 100, "login_timeout": 600, "ports": 65535}
59-
SSH_CONFIG_NAMES={"authentication_retries": "MaxAuthTries" , "login_timeout": "LoginGraceTime"}
57+
SSH_MIN_VALUES={"authentication_retries": 3, "login_timeout": 1, "ports": 1,
58+
"inactivity_timeout": 0, "max_sessions": 0}
59+
SSH_MAX_VALUES={"authentication_retries": 100, "login_timeout": 600,
60+
"ports": 65535, "inactivity_timeout": 35000,
61+
"max_sessions": 100}
62+
SSH_CONFIG_NAMES={"authentication_retries": "MaxAuthTries",
63+
"login_timeout": "LoginGraceTime", "ports": "Port",
64+
"inactivity_timeout": "ClientAliveInterval"}
6065

6166
ACCOUNT_NAME = 0 # index of account name
6267
AGE_DICT = { 'MAX_DAYS': {'REGEX_DAYS': r'^PASS_MAX_DAYS[ \t]*(?P<max_days>-?\d*)', 'DAYS': 'max_days', 'CHAGE_FLAG': '-M '},
@@ -1101,9 +1106,15 @@ class SshServer(object):
11011106
syslog.syslog(syslog.LOG_ERR, "Ssh {} {} out of range".format(key, value))
11021107
elif key in SSH_CONFIG_NAMES:
11031108
# search replace configuration - if not in config file - append
1109+
if key == "inactivity_timeout":
1110+
# translate min to sec.
1111+
value = int(value) * 60
11041112
kv_str = "{} {}".format(SSH_CONFIG_NAMES[key], str(value)) # name +' '+ value format
11051113
modify_single_file_inplace(SSH_CONFG_TMP,['-E', "/^#?" + SSH_CONFIG_NAMES[key]+"/{h;s/.*/"+
11061114
kv_str + "/};${x;/^$/{s//" + kv_str + "/;H};x}"])
1115+
elif key in ['max_sessions']:
1116+
# Ignore, these parameters handled in other modules
1117+
continue
11071118
else:
11081119
syslog.syslog(syslog.LOG_ERR, "Failed to update sshd config file - wrong key {}".format(key))
11091120

@@ -1318,16 +1329,31 @@ class PamLimitsCfg(object):
13181329
self.config_db = config_db
13191330
self.hwsku = ""
13201331
self.type = ""
1332+
self.max_sessions = None
13211333

13221334
# Load config from ConfigDb and render config file/
13231335
def update_config_file(self):
13241336
device_metadata = self.config_db.get_table('DEVICE_METADATA')
1325-
if "localhost" not in device_metadata:
1337+
ssh_server_policies = {}
1338+
try:
1339+
ssh_server_policies = self.config_db.get_table('SSH_SERVER')
1340+
except KeyError:
1341+
"""Dont throw except in case we don`t have SSH_SERVER config."""
1342+
pass
1343+
1344+
if "localhost" not in device_metadata and "POLICIES" not in ssh_server_policies:
13261345
return
13271346

13281347
self.read_localhost_config(device_metadata["localhost"])
1348+
self.read_max_sessions_config(ssh_server_policies.get("POLICIES", None))
13291349
self.render_conf_file()
13301350

1351+
# Read max_sessions config
1352+
def read_max_sessions_config(self, ssh_server_policies):
1353+
if ssh_server_policies is not None:
1354+
max_sess_cfg = ssh_server_policies.get('max_sessions', 0)
1355+
self.max_sessions = max_sess_cfg if max_sess_cfg != 0 else None
1356+
13311357
# Read localhost config
13321358
def read_localhost_config(self, localhost):
13331359
if "hwsku" in localhost:
@@ -1344,7 +1370,6 @@ class PamLimitsCfg(object):
13441370
def render_conf_file(self):
13451371
env = jinja2.Environment(loader=jinja2.FileSystemLoader('/'), trim_blocks=True)
13461372
env.filters['sub'] = sub
1347-
13481373
try:
13491374
template_file = os.path.abspath(PAM_LIMITS_CONF_TEMPLATE)
13501375
template = env.get_template(template_file)
@@ -1358,7 +1383,8 @@ class PamLimitsCfg(object):
13581383
template = env.get_template(template_file)
13591384
limits_conf = template.render(
13601385
hwsku=self.hwsku,
1361-
type=self.type)
1386+
type=self.type,
1387+
max_sessions=self.max_sessions)
13621388
with open(LIMITS_CONF, 'w') as f:
13631389
f.write(limits_conf)
13641390
except Exception as e:
@@ -1690,6 +1716,39 @@ class FipsCfg(object):
16901716
syslog.syslog(syslog.LOG_INFO, f'FipsCfg: update the FIPS enforce option {self.enforce}.')
16911717
loader.set_fips(image, self.enforce)
16921718

1719+
1720+
class SerialConsoleCfg:
1721+
1722+
def __init__(self):
1723+
self.cache = {}
1724+
1725+
def load(self, cli_sessions_conf):
1726+
self.cache = cli_sessions_conf or {}
1727+
syslog.syslog(syslog.LOG_INFO,
1728+
f'SerialConsoleCfg: Initial config: {self.cache}')
1729+
1730+
def update_serial_console_cfg(self, key, data):
1731+
'''
1732+
Apply config flow:
1733+
inactivity_timeout | set here AND in ssh_config flow | serial-config.service restarted.
1734+
max_sessions | set by PamLimitsCfg | serial-config.service DOESNT restarted.
1735+
sysrq_capabilities | set here | serial-config.service restarted.
1736+
'''
1737+
1738+
if self.cache.get(key, {}) != data:
1739+
''' Config changed, need to restart the serial-config.service '''
1740+
syslog.syslog(syslog.LOG_INFO, f'Set serial-config parameter {key} value: {data}')
1741+
try:
1742+
run_cmd(['sudo', 'service', 'serial-config', 'restart'],
1743+
True, True)
1744+
except Exception:
1745+
syslog.syslog(syslog.LOG_ERR, f'Failed to update {key} serial-config.service config')
1746+
return
1747+
self.cache.update({key: data})
1748+
1749+
return
1750+
1751+
16931752
class HostConfigDaemon:
16941753
def __init__(self):
16951754
self.state_db_conn = DBConnector(STATE_DB, 0)
@@ -1741,6 +1800,9 @@ class HostConfigDaemon:
17411800
# Initialize FipsCfg
17421801
self.fipscfg = FipsCfg(self.state_db_conn)
17431802

1803+
# Initialize SerialConsoleCfg
1804+
self.serialconscfg = SerialConsoleCfg()
1805+
17441806
def load(self, init_data):
17451807
aaa = init_data['AAA']
17461808
tacacs_global = init_data['TACPLUS']
@@ -1763,6 +1825,7 @@ class HostConfigDaemon:
17631825
ntp_global = init_data.get(swsscommon.CFG_NTP_GLOBAL_TABLE_NAME)
17641826
ntp_servers = init_data.get(swsscommon.CFG_NTP_SERVER_TABLE_NAME)
17651827
ntp_keys = init_data.get(swsscommon.CFG_NTP_KEY_TABLE_NAME)
1828+
serial_console = init_data.get('SERIAL_CONSOLE', {})
17661829

17671830
self.aaacfg.load(aaa, tacacs_global, tacacs_server, radius_global, radius_server, ldap_global, ldap_server)
17681831
self.iptables.load(lpbk_table)
@@ -1771,11 +1834,12 @@ class HostConfigDaemon:
17711834
self.sshscfg.load(ssh_server)
17721835
self.devmetacfg.load(dev_meta)
17731836
self.mgmtifacecfg.load(mgmt_ifc, mgmt_vrf)
1774-
17751837
self.rsyslogcfg.load(syslog_cfg, syslog_srv)
17761838
self.dnscfg.load(dns)
17771839
self.fipscfg.load(fips_cfg)
17781840
self.ntpcfg.load(ntp_global, ntp_servers, ntp_keys)
1841+
self.serialconscfg.load(serial_console)
1842+
self.pamLimitsCfg.update_config_file()
17791843

17801844
# Update AAA with the hostname
17811845
self.aaacfg.hostname_update(self.devmetacfg.hostname)
@@ -1797,6 +1861,8 @@ class HostConfigDaemon:
17971861

17981862
def ssh_handler(self, key, op, data):
17991863
self.sshscfg.policies_update(key, data)
1864+
self.pamLimitsCfg.update_config_file()
1865+
18001866
syslog.syslog(syslog.LOG_INFO, 'SSH Update: key: {}, op: {}, data: {}'.format(key, op, data))
18011867

18021868
def tacacs_server_handler(self, key, op, data):
@@ -1922,6 +1988,10 @@ class HostConfigDaemon:
19221988
data = self.config_db.get_table("FIPS")
19231989
self.fipscfg.fips_handler(data)
19241990

1991+
def serial_console_config_handler(self, key, op, data):
1992+
syslog.syslog(syslog.LOG_INFO, 'SERIAL_CONSOLE table handler...')
1993+
self.serialconscfg.update_serial_console_cfg(key, data)
1994+
19251995
def wait_till_system_init_done(self):
19261996
# No need to print the output in the log file so using the "--quiet"
19271997
# flag
@@ -1951,6 +2021,8 @@ class HostConfigDaemon:
19512021
self.config_db.subscribe('LDAP_SERVER', make_callback(self.ldap_server_handler))
19522022
self.config_db.subscribe('PASSW_HARDENING', make_callback(self.passwh_handler))
19532023
self.config_db.subscribe('SSH_SERVER', make_callback(self.ssh_handler))
2024+
# Handle SERIAL_CONSOLE
2025+
self.config_db.subscribe('SERIAL_CONSOLE', make_callback(self.serial_console_config_handler))
19542026
# Handle IPTables configuration
19552027
self.config_db.subscribe('LOOPBACK_INTERFACE', make_callback(self.lpbk_handler))
19562028
# Handle updates to src intf changes in radius

tests/hostcfgd/hostcfgd_test.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,48 @@ def test_loopback_update(self):
121121
])
122122

123123

124+
class TestSerialConsoleCfgd(TestCase):
125+
"""
126+
Test hostcfd daemon - SerialConsoleCfg
127+
"""
128+
def setUp(self):
129+
MockConfigDb.CONFIG_DB['SERIAL_CONSOLE'] = {
130+
'POLICIES': {'inactivity-timeout': '15', 'sysrq-capabilities': 'disabled'}
131+
}
132+
133+
def tearDown(self):
134+
MockConfigDb.CONFIG_DB = {}
135+
136+
def test_serial_console_update_cfg(self):
137+
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
138+
popen_mock = mock.Mock()
139+
attrs = {'communicate.return_value': ('output', 'error')}
140+
popen_mock.configure_mock(**attrs)
141+
mocked_subprocess.Popen.return_value = popen_mock
142+
143+
serialcfg = hostcfgd.SerialConsoleCfg()
144+
serialcfg.update_serial_console_cfg(
145+
'POLICIES', MockConfigDb.CONFIG_DB['SERIAL_CONSOLE']['POLICIES'])
146+
mocked_subprocess.check_call.assert_has_calls([
147+
call(['sudo', 'service', 'serial-config', 'restart']),
148+
])
149+
150+
def test_serial_console_is_caching_config(self):
151+
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
152+
popen_mock = mock.Mock()
153+
attrs = {'communicate.return_value': ('output', 'error')}
154+
popen_mock.configure_mock(**attrs)
155+
mocked_subprocess.Popen.return_value = popen_mock
156+
157+
serialcfg = hostcfgd.SerialConsoleCfg()
158+
serialcfg.cache['POLICIES'] = MockConfigDb.CONFIG_DB['SERIAL_CONSOLE']['POLICIES']
159+
160+
serialcfg.update_serial_console_cfg(
161+
'POLICIES', MockConfigDb.CONFIG_DB['SERIAL_CONSOLE']['POLICIES'])
162+
163+
mocked_subprocess.check_call.assert_not_called()
164+
165+
124166
class TestHostcfgdDaemon(TestCase):
125167

126168
def setUp(self):

tests/hostcfgd/sample_output/SSH_SERVER/sshd_config.old

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,4 @@ Subsystem sftp /usr/lib/openssh/sftp-server
118118
# PermitTTY no
119119
# ForceCommand cvs server
120120
PermitRootLogin yes
121-
ClientAliveInterval 120
121+
ClientAliveInterval 900

tests/hostcfgd/sample_output/SSH_SERVER_default_values/sshd_config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,4 @@ Subsystem sftp /usr/lib/openssh/sftp-server
118118
# PermitTTY no
119119
# ForceCommand cvs server
120120
PermitRootLogin yes
121-
ClientAliveInterval 120
121+
ClientAliveInterval 900

tests/hostcfgd/sample_output/SSH_SERVER_modify_all/sshd_config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,4 @@ Subsystem sftp /usr/lib/openssh/sftp-server
119119
# PermitTTY no
120120
# ForceCommand cvs server
121121
PermitRootLogin yes
122-
ClientAliveInterval 120
122+
ClientAliveInterval 900

tests/hostcfgd/sample_output/SSH_SERVER_modify_authentication_retries/sshd_config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,4 @@ Subsystem sftp /usr/lib/openssh/sftp-server
118118
# PermitTTY no
119119
# ForceCommand cvs server
120120
PermitRootLogin yes
121-
ClientAliveInterval 120
121+
ClientAliveInterval 900

tests/hostcfgd/sample_output/SSH_SERVER_modify_login_timeout/sshd_config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,4 @@ Subsystem sftp /usr/lib/openssh/sftp-server
118118
# PermitTTY no
119119
# ForceCommand cvs server
120120
PermitRootLogin yes
121-
ClientAliveInterval 120
121+
ClientAliveInterval 900

tests/hostcfgd/sample_output/SSH_SERVER_modify_ports/sshd_config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,4 @@ Subsystem sftp /usr/lib/openssh/sftp-server
120120
# PermitTTY no
121121
# ForceCommand cvs server
122122
PermitRootLogin yes
123-
ClientAliveInterval 120
123+
ClientAliveInterval 900

tests/hostcfgd/test_ssh_server_vectors.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
"authentication_retries": "6",
1212
"login_timeout": "120",
1313
"ports": "22",
14+
"inactivity_timeout": "15",
15+
"max_sessions": "0",
1416
}
1517
},
1618
"DEVICE_METADATA": {
@@ -35,6 +37,12 @@
3537
"num_dumps": "3",
3638
"memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M"
3739
}
40+
},
41+
"SERIAL_CONSOLE": {
42+
"POLICIES":{
43+
"inactivity_timeout": "15",
44+
"sysrq_capabilities": "disabled"
45+
}
3846
}
3947
},
4048
"modify_authentication_retries":{
@@ -43,6 +51,8 @@
4351
"authentication_retries": "12",
4452
"login_timeout": "120",
4553
"ports": "22",
54+
"inactivity_timeout": "15",
55+
"max_sessions": "0",
4656
}
4757
},
4858
"DEVICE_METADATA": {
@@ -67,6 +77,12 @@
6777
"num_dumps": "3",
6878
"memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M"
6979
}
80+
},
81+
"SERIAL_CONSOLE": {
82+
"POLICIES":{
83+
"inactivity_timeout": "15",
84+
"sysrq_capabilities": "disabled"
85+
}
7086
}
7187
},
7288
"modify_login_timeout":{
@@ -75,6 +91,8 @@
7591
"authentication_retries": "6",
7692
"login_timeout": "60",
7793
"ports": "22",
94+
"inactivity_timeout": "15",
95+
"max_sessions": "0",
7896
}
7997
},
8098
"DEVICE_METADATA": {
@@ -99,6 +117,12 @@
99117
"num_dumps": "3",
100118
"memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M"
101119
}
120+
},
121+
"SERIAL_CONSOLE": {
122+
"POLICIES":{
123+
"inactivity_timeout": "15",
124+
"sysrq_capabilities": "disabled"
125+
}
102126
}
103127
},
104128
"modify_ports":{
@@ -107,6 +131,8 @@
107131
"authentication_retries": "6",
108132
"login_timeout": "120",
109133
"ports": "22,23,24",
134+
"inactivity_timeout": "15",
135+
"max_sessions": "0",
110136
}
111137
},
112138
"DEVICE_METADATA": {
@@ -132,13 +158,21 @@
132158
"memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M"
133159
}
134160
},
161+
"SERIAL_CONSOLE": {
162+
"POLICIES":{
163+
"inactivity_timeout": "15",
164+
"sysrq_capabilities": "disabled"
165+
}
166+
}
135167
},
136168
"modify_all":{
137169
"SSH_SERVER": {
138170
"POLICIES":{
139171
"authentication_retries": "16",
140172
"login_timeout": "140",
141173
"ports": "22,222",
174+
"inactivity_timeout": "15",
175+
"max_sessions": "0",
142176
}
143177
},
144178
"DEVICE_METADATA": {
@@ -163,6 +197,12 @@
163197
"num_dumps": "3",
164198
"memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M"
165199
}
200+
},
201+
"SERIAL_CONSOLE": {
202+
"POLICIES":{
203+
"inactivity_timeout": "15",
204+
"sysrq_capabilities": "disabled"
205+
}
166206
}
167207
}
168208
}

0 commit comments

Comments
 (0)