1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

add ssh key authentication support, closes #81

This commit is contained in:
checktheroads
2020-10-11 14:39:17 -07:00
parent 6b188e446c
commit fbb42e7c5b
6 changed files with 94 additions and 20 deletions

View File

@@ -79,7 +79,10 @@ For HTTP devices (i.e. devices using [hyperglass-agent](https://github.com/check
| Parameter | Type | Description |
| :-------------- | :----- | :----------------------------------------------------------- |
| <R/> `username` | String | Username |
| <R/> `password` | String | Password <MiniNote>Passwords will never be logged</MiniNote> |
| `password` | String | Password <MiniNote>Passwords will never be logged</MiniNote> |
| `key` | Path | Path to SSH Private Key |
To use SSH key authentication, simply specify the path to the SSH private key with `key:`. If the key is encrypted, set the private key's password to the with the `password:` field, and hyperglass will use it to decrypt the SSH key.
### `ssl`

View File

@@ -53,6 +53,8 @@ from hyperglass.configuration import params
if params.debug:
logging.getLogger("paramiko").setLevel(logging.DEBUG)
log.bind(logger_name="paramiko")
TUNNEL_TIMEOUT = 1.0 #: Timeout (seconds) for tunnel connection
_DAEMON = False #: Use daemon threads in connections
_CONNECTION_COUNTER = 1
@@ -759,7 +761,7 @@ class SSHTunnelForwarder:
host_pkey_directories=None, # look for keys in ~/.ssh
gateway_timeout=None,
*args,
**kwargs # for backwards compatibility
**kwargs, # for backwards compatibility
):
self.logger = logger or log
self.ssh_host_key = ssh_host_key

View File

@@ -23,17 +23,29 @@ class SSHConnection(Connection):
def opener():
"""Set up an SSH tunnel according to a device's configuration."""
tunnel_kwargs = {
"ssh_username": proxy.credential.username,
"remote_bind_address": (self.device._target, self.device.port),
"local_bind_address": ("localhost", 0),
"skip_tunnel_checkup": False,
"gateway_timeout": params.request_timeout - 2,
}
if proxy.credential._method == "password":
# Use password auth if no key is defined.
tunnel_kwargs[
"ssh_password"
] = proxy.credential.password.get_secret_value()
else:
# Otherwise, use key auth.
tunnel_kwargs["ssh_pkey"] = proxy.credential.key.as_posix()
if proxy.credential._method == "encrypted_key":
# If the key is encrypted, use the password field as the
# private key password.
tunnel_kwargs[
"ssh_private_key_password"
] = proxy.credential.password.get_secret_value()
try:
return open_tunnel(
proxy._target,
proxy.port,
ssh_username=proxy.credential.username,
ssh_password=proxy.credential.password.get_secret_value(),
remote_bind_address=(self.device._target, self.device.port),
local_bind_address=("localhost", 0),
skip_tunnel_checkup=False,
gateway_timeout=params.request_timeout - 2,
)
return open_tunnel(proxy._target, proxy.port, **tunnel_kwargs)
except BaseSSHTunnelForwarderError as scrape_proxy_error:
log.error(

View File

@@ -60,20 +60,35 @@ class NetmikoConnection(SSHConnection):
send_args = netmiko_nos_send_args.get(self.device.nos, {})
netmiko_args = {
driver_kwargs = {
"host": host or self.device._target,
"port": port or self.device.port,
"device_type": self.device.nos,
"username": self.device.credential.username,
"password": self.device.credential.password.get_secret_value(),
"global_delay_factor": params.netmiko_delay_factor,
"timeout": math.floor(params.request_timeout * 1.25),
"session_timeout": math.ceil(params.request_timeout - 1),
**global_args,
}
if self.device.credential._method == "password":
# Use password auth if no key is defined.
driver_kwargs[
"password"
] = self.device.credential.password.get_secret_value()
else:
# Otherwise, use key auth.
driver_kwargs["use_keys"] = True
driver_kwargs["key_file"] = self.device.credential.key
if self.device.credential._method == "encrypted_key":
# If the key is encrypted, use the password field as the
# private key password.
driver_kwargs[
"passphrase"
] = self.device.credential.password.get_secret_value()
try:
nm_connect_direct = ConnectHandler(**netmiko_args)
nm_connect_direct = ConnectHandler(**driver_kwargs)
responses = ()

View File

@@ -77,7 +77,6 @@ class ScrapliConnection(SSHConnection):
"host": host or self.device._target,
"port": port or self.device.port,
"auth_username": self.device.credential.username,
"auth_password": self.device.credential.password.get_secret_value(),
"timeout_transport": math.floor(params.request_timeout * 1.25),
"transport": "asyncssh",
"auth_strict_key": False,
@@ -85,6 +84,21 @@ class ScrapliConnection(SSHConnection):
"ssh_config_file": False,
}
if self.device.credential._method == "password":
# Use password auth if no key is defined.
driver_kwargs[
"auth_password"
] = self.device.credential.password.get_secret_value()
else:
# Otherwise, use key auth.
driver_kwargs["auth_private_key"] = self.device.credential.key.as_posix()
if self.device.credential._method == "encrypted_key":
# If the key is encrypted, use the password field as the
# private key password.
driver_kwargs[
"auth_private_key_passphrase"
] = self.device.credential.password.get_secret_value()
driver = driver(**driver_kwargs)
driver.logger = log.bind(logger_name=f"scrapli.driver-{driver._host}")

View File

@@ -1,14 +1,42 @@
"""Validate credential configuration variables."""
# Standard Library
from typing import Optional
# Third Party
from pydantic import SecretStr, StrictStr
from pydantic import FilePath, SecretStr, StrictStr, constr, root_validator
# Local
from ..main import HyperglassModel
from ..main import HyperglassModelExtra
Methods = constr(regex=r"(password|unencrypted_key|encrypted_key)")
class Credential(HyperglassModel):
class Credential(HyperglassModelExtra):
"""Model for per-credential config in devices.yaml."""
username: StrictStr
password: SecretStr
password: Optional[SecretStr]
key: Optional[FilePath]
@root_validator
def validate_credential(cls, values):
"""Ensure either a password or an SSH key is set."""
if values["key"] is None and values["password"] is None:
raise ValueError(
"Either a password or an SSH key must be specified for user '{}'".format(
values["username"]
)
)
return values
def __init__(self, **kwargs):
"""Set private attribute _method based on validated model."""
super().__init__(**kwargs)
self._method = None
if self.password is not None and self.key is not None:
self._method = "encrypted_key"
elif self.password is None:
self._method = "unencrypted_key"
elif self.key is None:
self._method = "password"