napalm-automation-community / napalm-ros

MikroTik RouterOS NAPALM driver
101 stars 39 forks source link

How to connect using SSL/TLS with no certificate? #119

Closed ggiesen closed 11 months ago

ggiesen commented 1 year ago

Description of Issue/Question

I'm trying to connect to a Mikrotik RBM33G from Salt using the napalm proxy minion, using TLS with no certificate. Looking through the code, unless I'm misreading (and admit that's fully possible as I don't follow it completely), the only way I could find to explicitly enable TLS is to set netbox_default_ssl_params = True.

I have my proxy minion config as such:

proxy:
  proxytype: napalm
  driver: ros
  host: 192.0.2.1
  username: admin
  passwd: Iecee3quoh1Aimoh
  multiprocessing: False
  optional_args:
    netbox_default_ssl_params: True

When I try to issue a command, I get:

Traceback (most recent call last):
  File "/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/napalm.py", line 336, in get_device
    network_device.get("DRIVER").open()
  File "/opt/saltstack/salt/extras-3.10/napalm_ros/ros.py", line 475, in open
    raise ConnectionException(f"Could not connect to {self.hostname}:{self.port} - [{exc!r}]")
napalm.base.exceptions.ConnectionException: Could not connect to 192.0.2.1:8729 - [SSLError(1, '[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1007)')]

According to the librouteros docs, it looks like:

ctx.check_hostname = False
ctx.set_ciphers('ADH:@SECLEVEL=0')

needs to be passed to the context, but I don't think there's any way to do that since I can't create the context from Salt. Am I wrong on this, and if so, is there any way to specifically add support certificate-less SSL via optional_args?

Setup

napalm-ros version

(Paste verbatim output from pip freeze | grep napalm-ros between quotes below)

napalm-ros==1.2.4

ROS version

(Paste verbatim output from /system package print between quotes below)

Columns: NAME, VERSION
# NAME      VERSION
0 routeros  7.10 

librouteros version

(Paste verbatim output from pip freeze | grep librouteros between quotes below)

librouteros==3.2.1

python version

(paste output of python --version)

Python 3.10.11

Steps to Reproduce the Issue

Error Traceback

(Paste the complete traceback of the exception between quotes below)

Traceback (most recent call last):
  File "/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/napalm.py", line 336, in get_device
    network_device.get("DRIVER").open()
  File "/opt/saltstack/salt/extras-3.10/napalm_ros/ros.py", line 475, in open
    raise ConnectionException(f"Could not connect to {self.hostname}:{self.port} - [{exc!r}]")
napalm.base.exceptions.ConnectionException: Could not connect to 192.0.2.1:8729 - [SSLError(1, '[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1007)')]
ggiesen commented 1 year ago

I have managed to get a little further by replacing this:

https://github.com/napalm-automation-community/napalm-ros/blob/ffc6973e1c5f7c635f785f3d5952edd194b7a46e/napalm_ros/ros.py#L65-L75

With this:

        if self.optional_args.get('transport', 'http') == 'https' or self.optional_args.get('netbox_default_ssl_params', False):
            ctx = ssl.create_default_context()
            try:
                IPAddress(self.hostname)
                # IPAdresses cannot check hostname
                ctx.check_hostname = False
            except AddrFormatError:
                # if hostname is not IP, we use check_hostname variable
                ctx.check_hostname = self.optional_args.get('check_hostname', True)

            if not self.optional_args.get('use_ssl_cert', True):
                # Cannot use check_hostname with no certificate
                ctx.check_hostname = False
                # Disable certificate checking
                ctx.set_ciphers('ADH:@SECLEVEL=0')

            self.optional_args['ssl_wrapper'] = ctx.wrap_socket

and modifying my proxy config to this:

proxy:
  proxytype: napalm
  driver: ros
  host: 192.0.2.1
  username: admin
  passwd: Iecee3quoh1Aimoh
  multiprocessing: False
  optional_args:
    transport: https
    use_ssl_cert: False

And I see it log in successfully:

[DEBUG   ] dummy proxy __virtual__() called...
[DEBUG   ] The functions from module 'napalm' are being loaded by dir() on the loaded module
[INFO    ] nxos proxy __virtual__() called...
[DEBUG   ] rest_sample proxy __virtual__() called...
[INFO    ] ssh_sample proxy __virtual__() called...
[DEBUG   ] Setting up NAPALM connection
[DEBUG   ] <--- '/login'
[DEBUG   ] <--- '=name=admin'
[DEBUG   ] <--- '=password=Iecee3quoh1Aimoh'
[DEBUG   ] <--- EOS
[DEBUG   ] ---> '!done'
[DEBUG   ] ---> EOS
[DEBUG   ] Grains refresh requested. Refreshing grains.
[DEBUG   ] Reading configuration from /etc/salt/proxy
[DEBUG   ] Including configuration from '/etc/salt/proxy.d/mikrominion/_schedule.conf'
[DEBUG   ] Reading configuration from /etc/salt/proxy.d/mikrominion/_schedule.conf
[DEBUG   ] Including configuration from '/etc/salt/proxy.d/mikrominion/mikrominion.conf'
[DEBUG   ] Reading configuration from /etc/salt/proxy.d/mikrominion/mikrominion.conf

But then fails with an error of TypeError: cannot pickle 'SSLContext' object:

Traceback (most recent call last):
  File "/bin/salt-call", line 11, in <module>
    sys.exit(salt_call())
  File "/opt/saltstack/salt/lib/python3.10/site-packages/salt/scripts.py", line 444, in salt_call
    client.run()
  File "/opt/saltstack/salt/lib/python3.10/site-packages/salt/cli/call.py", line 40, in run
    caller = salt.cli.caller.Caller.factory(self.config)
  File "/opt/saltstack/salt/lib/python3.10/site-packages/salt/cli/caller.py", line 42, in factory
    return ZeroMQCaller(opts, **kwargs)
  File "/opt/saltstack/salt/lib/python3.10/site-packages/salt/cli/caller.py", line 303, in __init__
    super().__init__(opts)
  File "/opt/saltstack/salt/lib/python3.10/site-packages/salt/cli/caller.py", line 61, in __init__
    self.minion = salt.minion.SProxyMinion(opts)
  File "/opt/saltstack/salt/lib/python3.10/site-packages/salt/minion.py", line 929, in __init__
    self.gen_modules(initial_load=True, context=context)
  File "/opt/saltstack/salt/lib/python3.10/site-packages/salt/minion.py", line 3983, in gen_modules
    self.opts["grains"] = salt.loader.grains(self.opts, proxy=self.proxy)
  File "/opt/saltstack/salt/lib/python3.10/site-packages/salt/loader/__init__.py", line 1106, in grains
    funcs = grain_funcs(
  File "/opt/saltstack/salt/lib/python3.10/site-packages/salt/loader/__init__.py", line 967, in grain_funcs
    _utils = utils(opts, proxy=proxy)
  File "/opt/saltstack/salt/lib/python3.10/site-packages/salt/loader/__init__.py", line 534, in utils
    return LazyLoader(
  File "/opt/saltstack/salt/lib/python3.10/site-packages/salt/loader/lazy.py", line 249, in __init__
    opts = copy.deepcopy(opts)
  File "/opt/saltstack/salt/lib/python3.10/copy.py", line 146, in deepcopy
    y = copier(x, memo)
  File "/opt/saltstack/salt/lib/python3.10/copy.py", line 231, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
  File "/opt/saltstack/salt/lib/python3.10/copy.py", line 146, in deepcopy
    y = copier(x, memo)
  File "/opt/saltstack/salt/lib/python3.10/copy.py", line 231, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
  File "/opt/saltstack/salt/lib/python3.10/copy.py", line 146, in deepcopy
    y = copier(x, memo)
  File "/opt/saltstack/salt/lib/python3.10/copy.py", line 231, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
  File "/opt/saltstack/salt/lib/python3.10/copy.py", line 146, in deepcopy
    y = copier(x, memo)
  File "/opt/saltstack/salt/lib/python3.10/copy.py", line 238, in _deepcopy_method
    return type(x)(x.__func__, deepcopy(x.__self__, memo))
  File "/opt/saltstack/salt/lib/python3.10/copy.py", line 161, in deepcopy
    rv = reductor(4)
TypeError: cannot pickle 'SSLContext' object

I realize the traceback is on the Salt side, but given that other drivers work fine, I'm curious if a) I'm doing something wrong or b) there is something nonstandard about this driver/other issues with this driver?

ggiesen commented 1 year ago

With some help from @ktbyers on Slack, I believe I've solved the issue. I replaced:

https://github.com/napalm-automation-community/napalm-ros/blob/ffc6973e1c5f7c635f785f3d5952edd194b7a46e/napalm_ros/ros.py#L64-L78

with the following:

        self.ssl_wrapper = librouteros.DEFAULTS['ssl_wrapper']

        if self.optional_args.get('transport', 'http') == 'https' or self.optional_args.get('netbox_default_ssl_params', False):
            ctx = ssl.create_default_context()
            try:
                IPAddress(self.hostname)
                # IPAdresses cannot check hostname
                ctx.check_hostname = False
            except AddrFormatError:
                # if hostname is not IP, we use check_hostname variable
                ctx.check_hostname = self.optional_args.get('check_hostname', True)

            if not self.optional_args.get('use_ssl_cert', True):
                # Cannot use check_hostname with no certificate
                ctx.check_hostname = False
                # Disable certificate checking
                ctx.set_ciphers('ADH:@SECLEVEL=0')

            self.optional_args['transport'] = 'https'
            self.ssl_wrapper = ctx.wrap_socket
        else:
            self.optional_args['transport'] = 'http'

        self.port = self.optional_args.get('port', 8729 if self.optional_args['transport'] == 'https' else 8728)

I don't see a reason the SSLContext needs to be passed through optional_args via ssl_wapper (resulting in the error when it's trying to be serialized by pickle), but perhaps @luqasz or @sHorst can advise?

luqasz commented 1 year ago

Docs state it clearly why ssl context is used. HTTP / HTTPS is not supported by librouteros nor napalm-ros driver.

ggiesen commented 1 year ago

I apologize if I was unclear. The question is not why SSLContext is used, but why the object needs to be passed through optional_args instead of just having the driver create it (like it does when the netbox_default_ssl_params option is used).

Is there any reason it needs to be able to be passed through optional_args (rendering the driver unusable with Salt)? If I submit a PR similar to the code above, would it be accepted?

luqasz commented 1 year ago

I've added ssl support into librouteros some time ago. Later I've added it to napalm due to requests / PRs from netbox users. Then problems started since some users want X, some Y and others Z. I didn't refactor it in order not to break what is already running. That being said I want to refactor all ssl code in napalm ros driver to give reasonable defaults and options.

I am open to discussion / PRs.

washcroft commented 1 year ago

Related to https://github.com/napalm-automation-community/napalm-ros/pull/103

stale[bot] commented 11 months ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.