Skip to content

Commit

Permalink
add ssh key authentication support, closes #81
Browse files Browse the repository at this point in the history
  • Loading branch information
thatmattlove committed Oct 11, 2020
1 parent 6b188e4 commit fbb42e7
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 20 deletions.
5 changes: 4 additions & 1 deletion docs/docs/adding-devices.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
4 changes: 3 additions & 1 deletion hyperglass/compat/_sshtunnel.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
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
Expand Down Expand Up @@ -759,7 +761,7 @@ def __init__(
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
Expand Down
32 changes: 22 additions & 10 deletions hyperglass/execution/drivers/ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,29 @@ def setup_proxy(self) -> Callable:

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(
Expand Down
21 changes: 18 additions & 3 deletions hyperglass/execution/drivers/ssh_netmiko.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,35 @@ async def collect(self, host: str = None, port: int = None) -> Iterable:

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 = ()

Expand Down
16 changes: 15 additions & 1 deletion hyperglass/execution/drivers/ssh_scrapli.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,28 @@ async def collect(self, host: str = None, port: int = None) -> Iterable:
"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,
"ssh_known_hosts_file": False,
"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}")

Expand Down
36 changes: 32 additions & 4 deletions hyperglass/models/config/credential.py
Original file line number Diff line number Diff line change
@@ -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"

0 comments on commit fbb42e7

Please sign in to comment.