Skip to content

Commit 0d3cec2

Browse files
Afonne-CIDpriteau
authored andcommitted
Fix info leakage in Netmiko connection errors
Sanitize error messages generated by Netmiko on the event of a connection failure and redact sensitive information before it's propagated. Currenly, when a connection to a network device fails, sensitive information such as IP address, username, etc, are logged alongise the error message. Closes-Bug: #2100566 Change-Id: I124d44f161e3116ec2588617ed98d559f6fe3b1f
1 parent 5098525 commit 0d3cec2

File tree

10 files changed

+85
-49
lines changed

10 files changed

+85
-49
lines changed

networking_generic_switch/devices/netmiko_devices/__init__.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import tenacity
2727
from tooz import coordination
2828

29+
from networking_generic_switch._i18n import _
2930
from networking_generic_switch import batching
3031
from networking_generic_switch import devices
3132
from networking_generic_switch.devices import utils as device_utils
@@ -153,10 +154,13 @@ def __init__(self, device_cfg, *args, **kwargs):
153154
self.batch_cmds = None
154155
if self._batch_requests():
155156
if not CONF.ngs_coordination.backend_url:
156-
raise exc.GenericSwitchNetmikoConfigError(
157-
config=device_utils.sanitise_config(self.config),
158-
error="ngs_batch_requests is true but [ngs_coordination] "
159-
"backend_url is not provided")
157+
error = ("ngs_batch_requests is true but [ngs_coordination] "
158+
"backend_url is not provided")
159+
LOG.error(
160+
_("%(device)s, error: %(error)s"),
161+
{'device': device_utils.sanitise_config(self.config),
162+
'error': error})
163+
raise exc.GenericSwitchNetmikoConfigError()
160164
# NOTE: we skip the lock if we are batching requests
161165
self.locker = None
162166
switch_name = self.lock_kwargs['locks_prefix']
@@ -221,13 +225,19 @@ def _create_connection():
221225
try:
222226
net_connect = _create_connection()
223227
except tenacity.RetryError as e:
224-
LOG.error("Reached maximum SSH connection attempts, not retrying")
225-
raise exc.GenericSwitchNetmikoConnectError(
226-
config=device_utils.sanitise_config(self.config), error=e)
228+
LOG.error(
229+
_("Reached maximum SSH connection attempts, not retrying "
230+
"for device: %(device)s, error: %(error)s"), {
231+
'device': device_utils.sanitise_config(self.config),
232+
'error': e})
233+
raise exc.GenericSwitchNetmikoConnectError()
227234
except Exception as e:
228-
LOG.error("Unexpected exception during SSH connection")
229-
raise exc.GenericSwitchNetmikoConnectError(
230-
config=device_utils.sanitise_config(self.config), error=e)
235+
LOG.error(
236+
_("Unexpected exception during SSH connection "
237+
"to device: %(device)s, error: %(error)s"), {
238+
'device': device_utils.sanitise_config(self.config),
239+
'error': e})
240+
raise exc.GenericSwitchNetmikoConnectError()
231241

232242
# Now yield the connection to the caller.
233243
with net_connect:
@@ -257,8 +267,12 @@ def _send_commands_to_device(self, cmd_set):
257267
# module.
258268
raise
259269
except Exception as e:
260-
raise exc.GenericSwitchNetmikoConnectError(
261-
config=device_utils.sanitise_config(self.config), error=e)
270+
LOG.error(
271+
_("Error sending commands to device: %(device)s, "
272+
"error: %(error)s"), {
273+
'device': device_utils.sanitise_config(self.config),
274+
'error': e})
275+
raise exc.GenericSwitchNetmikoConnectError()
262276

263277
LOG.debug(output)
264278
return output
@@ -488,12 +502,14 @@ def check_output(self, output, operation):
488502

489503
for pattern in self.ERROR_MSG_PATTERNS:
490504
if pattern.search(output):
491-
msg = ("Found invalid configuration in device response. "
492-
"Operation: %(operation)s. Output: %(output)s" %
493-
{'operation': operation, 'output': output})
494-
raise exc.GenericSwitchNetmikoConfigError(
495-
config=device_utils.sanitise_config(self.config),
496-
error=msg)
505+
LOG.error(
506+
_("Found invalid configuration in device response. "
507+
"Operation: %(operation)s. Output: %(output)s. "
508+
"Device: %(device)s"), {
509+
'operation': operation, 'output': output,
510+
'device': device_utils.sanitise_config(self.config)
511+
})
512+
raise exc.GenericSwitchNetmikoConfigError()
497513

498514
def add_subports_on_trunk(self, binding_profile, port_id, subports):
499515
"""Allow subports on trunk

networking_generic_switch/devices/netmiko_devices/juniper.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from oslo_log import log as logging
1616
import tenacity
1717

18+
from networking_generic_switch._i18n import _
1819
from networking_generic_switch.devices import netmiko_devices
1920
from networking_generic_switch.devices import utils as device_utils
2021
from networking_generic_switch import exceptions as exc
@@ -172,20 +173,26 @@ def commit():
172173
try:
173174
commit()
174175
except DBLocked as e:
175-
msg = ("Reached timeout waiting for switch configuration DB lock. "
176-
"Configuration might not be committed. Error: %s" % str(e))
176+
msg = _("Reached timeout waiting for switch configuration "
177+
"DB lock. Configuration might not be committed. "
178+
"Device: %(device)s, error: %(error)s") % {
179+
'device': device_utils.sanitise_config(self.config),
180+
'error': e}
177181
LOG.error(msg)
178-
raise exc.GenericSwitchNetmikoConfigError(
179-
config=device_utils.sanitise_config(self.config), error=msg)
182+
raise exc.GenericSwitchNetmikoConfigError()
180183
except (WarningStmtNotExist, WarningStmtExists) as e:
181-
msg = ("Reached timeout while attempting to apply configuration. "
182-
"This is likely to be caused by multiple sessions "
183-
"configuring the device concurrently. Error: %s" % str(e))
184+
msg = _("Reached timeout while attempting to apply "
185+
"configuration. This is likely to be caused by multiple "
186+
"sessions configuring the device concurrently. "
187+
"Device: %(device)s, error: %(error)s") % {
188+
'device': device_utils.sanitise_config(self.config),
189+
'error': e}
184190
LOG.error(msg)
185-
raise exc.GenericSwitchNetmikoConfigError(
186-
config=device_utils.sanitise_config(self.config), error=msg)
191+
raise exc.GenericSwitchNetmikoConfigError()
187192
except ValueError as e:
188-
msg = "Failed to commit configuration: %s" % e
193+
msg = _("Failed to commit configuration: Device: %(device)s, "
194+
"error: %(error)s") % {
195+
'device': device_utils.sanitise_config(self.config),
196+
'error': e}
189197
LOG.error(msg)
190-
raise exc.GenericSwitchNetmikoConfigError(
191-
config=device_utils.sanitise_config(self.config), error=msg)
198+
raise exc.GenericSwitchNetmikoConfigError()

networking_generic_switch/devices/netmiko_devices/nokia.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from oslo_log import log as logging
1616

17+
from networking_generic_switch._i18n import _
1718
from networking_generic_switch.devices import netmiko_devices
1819
from networking_generic_switch.devices import utils as device_utils
1920
from networking_generic_switch import exceptions as exc
@@ -81,9 +82,10 @@ def send_commands_to_device(self, cmd_set):
8182
# module.
8283
raise
8384
except Exception as e:
84-
raise exc.GenericSwitchNetmikoConnectError(
85-
config=device_utils.sanitise_config(self.config), error=e
86-
)
85+
LOG.error(_("Device: %(device)s, error: %(error)s"), {
86+
'device': device_utils.sanitise_config(self.config),
87+
'error': e})
88+
raise exc.GenericSwitchNetmikoConnectError()
8789

8890
LOG.debug(output)
8991
return output

networking_generic_switch/devices/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ def sanitise_config(config):
4646
:param config: a configuration dict to sanitise.
4747
:returns: a copy of the configuration, with sensitive fields removed.
4848
"""
49-
sanitised_fields = {"password"}
49+
sanitised_fields = {"password", "ip", "device_type", "username",
50+
"session_log"}
5051
return {
5152
key: "******" if key in sanitised_fields else value
5253
for key, value in config.items()

networking_generic_switch/exceptions.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@ class GenericSwitchNetmikoNotSupported(GenericSwitchException):
4444

4545

4646
class GenericSwitchNetmikoConnectError(GenericSwitchException):
47-
message = _("Netmiko connection error: %(config)s, error: %(error)s")
47+
message = _("Failed to connect to Netmiko switch. "
48+
"Please contact your administrator.")
4849

4950

5051
class GenericSwitchNetmikoConfigError(GenericSwitchException):
51-
message = _("Netmiko configuration error: %(config)s, error: %(error)s")
52+
message = _("Netmiko switch configuration operation failed. "
53+
"Please contact your administrator.")
5254

5355

5456
class GenericSwitchBatchError(GenericSwitchException):

networking_generic_switch/tests/unit/devices/test_utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ def test_get_switch_device_fallback_to_switch_info(self):
6060
ngs_mac_address='11:22:33:44:55:77'))
6161

6262
def test_sanitise_config(self):
63-
config = {'username': 'fake-user', 'password': 'fake-password'}
63+
config = {'username': 'fake-user', 'password': 'fake-password',
64+
'ip': '123.456.789', 'session_log': '/some/path/here',
65+
"device_type": "my_device"}
6466
result = device_utils.sanitise_config(config)
65-
expected = {'username': 'fake-user', 'password': '******'}
67+
expected = {k: '******' for k in config}
6668
self.assertEqual(expected, result)

networking_generic_switch/tests/unit/netmiko/test_dell.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -565,9 +565,8 @@ def test_check_output(self):
565565
self.switch.check_output('fake output', 'fake op')
566566

567567
def _test_check_output_error(self, output):
568-
msg = ("Found invalid configuration in device response. Operation: "
569-
"fake op. Output: %s" % output)
570-
self.assertRaisesRegex(exc.GenericSwitchNetmikoConfigError, msg,
568+
self.assertRaisesRegex(exc.GenericSwitchNetmikoConfigError,
569+
"switch configuration operation failed",
571570
self.switch.check_output, output, 'fake op')
572571

573572
def test_check_output_incomplete_command(self):

networking_generic_switch/tests/unit/netmiko/test_juniper.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def test_save_configuration_db_locked_timeout(self, m_stop, m_wait):
177177
"Commit failed with the following errors:\n\n{0}".format(output))
178178

179179
self.assertRaisesRegex(exc.GenericSwitchNetmikoConfigError,
180-
"Reached timeout waiting for",
180+
"switch configuration operation failed",
181181
self.switch.save_configuration,
182182
mock_connection)
183183
self.assertGreater(mock_connection.commit.call_count, 1)
@@ -227,7 +227,7 @@ def test_save_configuration_warn_already_exists_timeout(
227227
"Commit failed with the following errors:\n\n{0}".format(output))
228228

229229
self.assertRaisesRegex(exc.GenericSwitchNetmikoConfigError,
230-
"Reached timeout while attempting",
230+
"switch configuration operation failed",
231231
self.switch.save_configuration,
232232
mock_connection)
233233
self.assertGreater(mock_connection.commit.call_count, 1)
@@ -277,7 +277,7 @@ def test_save_configuration_warn_does_not_exist_timeout(
277277
"Commit failed with the following errors:\n\n{0}".format(output))
278278

279279
self.assertRaisesRegex(exc.GenericSwitchNetmikoConfigError,
280-
"Reached timeout while attempting",
280+
"switch configuration operation failed",
281281
self.switch.save_configuration,
282282
mock_connection)
283283
self.assertGreater(mock_connection.commit.call_count, 1)
@@ -299,7 +299,7 @@ def test_save_configuration_error(self):
299299
"Commit failed with the following errors:\n\n{0}".format(output))
300300

301301
self.assertRaisesRegex(exc.GenericSwitchNetmikoConfigError,
302-
"Failed to commit configuration",
302+
"switch configuration operation failed",
303303
self.switch.save_configuration,
304304
mock_connection)
305305
mock_connection.commit.assert_called_once_with()

networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def test_batch(self):
5454

5555
def test_batch_missing_backend_url(self):
5656
self.assertRaisesRegex(
57-
Exception, "backend_url",
57+
Exception, "switch configuration operation failed",
5858
self._make_switch_device, {'ngs_batch_requests': True})
5959

6060
@mock.patch('networking_generic_switch.devices.netmiko_devices.'
@@ -445,7 +445,6 @@ def test_check_output_error(self):
445445
fake switch command
446446
fake error message
447447
"""
448-
msg = ("Found invalid configuration in device response. Operation: "
449-
"fake op. Output: %s" % output)
450-
self.assertRaisesRegex(exc.GenericSwitchNetmikoConfigError, msg,
448+
self.assertRaisesRegex(exc.GenericSwitchNetmikoConfigError,
449+
"switch configuration operation failed",
451450
self.switch.check_output, output, 'fake op')
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
fixes:
3+
- |
4+
Fixes a security vulnerability where sensitive information (IP addresses,
5+
usernames, hostname and file paths) were leaked in error messages when a
6+
connection to a network device fails. Error messages are now properly
7+
sanitized to redact these sensitive details while preserving meaningful
8+
information for troubleshooting.

0 commit comments

Comments
 (0)