Skip to content

Commit

Permalink
Merge branch 'develop' into fix_cisco_apic
Browse files Browse the repository at this point in the history
  • Loading branch information
yone2ks authored Nov 21, 2024
2 parents b22e6f0 + 0697340 commit 6ec3589
Show file tree
Hide file tree
Showing 7 changed files with 343 additions and 23 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/main_testing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
shell: bash
strategy:
matrix:
python-version: [ '3.8', '3.9', '3.10', '3.11', "3.12", "3.13.0-beta.2" ]
python-version: [ '3.9', '3.10', '3.11', "3.12", "3.13" ]
platform: [ubuntu-24.04, windows-2022]

runs-on: ${{ matrix.platform }}
Expand Down Expand Up @@ -96,7 +96,7 @@ jobs:
shell: bash
strategy:
matrix:
python-version: [ '3.8', '3.9', '3.10', '3.11' ]
python-version: [ '3.9', '3.10', '3.11' ]
platform: [macos-13]

runs-on: ${{ matrix.platform }}
Expand Down
148 changes: 148 additions & 0 deletions ENCRYPTION_HANDLING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Netmiko Encryption Handling

This document describes the encryption mechanisms available in Netmiko for handling sensitive data in configuration files. These mechanisms are generally intended for use with `~/.netmiko.yml` and Netmiko Tools.

## Overview

Netmiko provides built-in encryption capabilities to secure sensitive data (like passwords) in your Netmiko Tools YAML configuration files. The encryption system is flexible and supports multiple encryption types.

## Configuration

### Basic Setup

Encryption is configured in the `~/.netmiko.yml` file using the `__meta__` field:

```yaml
__meta__:
encryption: true
encryption_type: fernet # or aes128
```
The two supported encryption types are:
- `fernet` (recommended)
- `aes128`

### Encryption Key

The encryption key is read from the environment variable `NETMIKO_TOOLS_KEY`. This should be a secure, randomly-generated key appropriate for the chosen encryption type.

```bash
# Example of setting the encryption key
export NETMIKO_TOOLS_KEY="your-secure-key-here"
```

## Using Encryption

### Encrypted Values in YAML

When encryption is enabled, Netmiko looks for fields that start with `__encrypt__`. For example:

```yaml
arista1:
device_type: arista_eos
host: arista1.domain.com
username: pyclass
password: >
__encrypt__ifcs7SWOUER4m1K3ZEZYlw==:Z0FBQUFBQm5CQ9lrdV9BVS0xOWxYelF1Yml
zV3hBcnF4am1SWjRYNnVSRGdBb1FPVmJ2Q2EzX1RjTWxYMVVMdlBZSXVqYWVqUVNASXNRO
FBpR1MxRTkxN2J0NWxVeZNKT0E9PQ==
```

### Encryption Functions

#### Encrypting Values

To encrypt a value, use the `encrypt_value` function:

```python
def encrypt_value(value: str, key: bytes, encryption_type: str) -> str:
"""
Encrypt a value using the specified encryption type.
Args:
value: The string to encrypt
key: Encryption key as bytes
encryption_type: Either 'fernet' or 'aes128'
Returns:
Encrypted string with '__encrypt__' prefix
"""
```

#### Decrypting Values

To decrypt a value, use the `decrypt_value` function:

```python
def decrypt_value(encrypted_value: str, key: bytes, encryption_type: str) -> str:
"""
Decrypt a value using the specified encryption type.
Args:
encrypted_value: The encrypted string (including '__encrypt__' prefix)
key: Encryption key as bytes
encryption_type: Either 'fernet' or 'aes128'
Returns:
Decrypted string
"""
```

#### Getting the Encryption Key

To retrieve the encryption key from the environment:

```python
def get_encryption_key() -> bytes:
"""
Retrieve the encryption key from NETMIKO_TOOLS_KEY environment variable.
Returns:
Encryption key as bytes
"""
```

## Example Usage

Here's a complete example of how to use encryption in your code:

```python
from netmiko.encryption_handling import encrypt_value, get_encryption_key
from netmiko.encryption_handling import decrypt_value
# Get the encryption key from environment
key = get_encryption_key()
# Encrypt a password
password = "my_secure_password"
encrypted_password = encrypt_value(password, key, "fernet")
# The encrypted password can now be stored in your YAML file
# It will automatically be decrypted when Netmiko Tools reads the
# file (assuming you have properly set the '__meta__' fields
```

Alternatively, you can decrypt the value by calling

```python
clear_value = decrypt_value(encrypted_value, key, encryption_type="fernet)
```

Or you can create a simple function to decrypt all of the fields in the YAML
file dynamically (by looking for any fields that start with `__encrypt__`).

Netmiko's 'encryption_handling.py' implements this using the 'decrypt_config'
function, but this function is a specific to Netmiko Tools' .netmiko.yml format
(i.e. it will need modified if you want to use it in a more generic context).

## Implementation Notes

1. Encryption is processed transparently when Netmiko Tools reads the YAML file
2. Only fields prefixed with `__encrypt__` are processed for decryption
3. The encryption type is determined by the `__meta__` section

## Security Considerations

1. Store the `NETMIKO_TOOLS_KEY` securely and never commit it to version control
2. Fernet encryption is recommended over AES128 as it includes additional security features
3. Encrypted values in YAML files should still be treated as sensitive data
19 changes: 11 additions & 8 deletions netmiko/base_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,15 +472,18 @@ def __init__(
self.system_host_keys = system_host_keys
self.alt_host_keys = alt_host_keys
self.alt_key_file = alt_key_file
self.disabled_algorithms = disabled_algorithms

if disabled_algorithms:
self.disabled_algorithms = disabled_algorithms
else:
self.disabled_algorithms = (
{"pubkeys": ["rsa-sha2-256", "rsa-sha2-512"]}
if disable_sha2_fix
else {}
)
if disable_sha2_fix:
sha2_pubkeys = ["rsa-sha2-256", "rsa-sha2-512"]
if self.disabled_algorithms is None:
self.disabled_algorithms = {"pubkeys": sha2_pubkeys}
else:
# Merge sha2_pubkeys into pubkeys and prevent duplicates
current_pubkeys = self.disabled_algorithms.get("pubkeys", [])
self.disabled_algorithms["pubkeys"] = list(
set(current_pubkeys + sha2_pubkeys)
)

# For SSH proxy support
self.ssh_config_file = ssh_config_file
Expand Down
30 changes: 19 additions & 11 deletions netmiko/fortinet/fortinet_ssh.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import paramiko
import re
from typing import Optional
from typing import Optional, Any

from netmiko.no_config import NoConfig
from netmiko.no_enable import NoEnable
Expand All @@ -9,16 +9,24 @@

class FortinetSSH(NoConfig, NoEnable, CiscoSSHConnection):
prompt_pattern = r"[#$]"

def _modify_connection_params(self) -> None:
"""Modify connection parameters prior to SSH connection."""
paramiko_transport = getattr(paramiko, "Transport")
paramiko_transport._preferred_kex = (
"diffie-hellman-group14-sha1",
"diffie-hellman-group-exchange-sha1",
"diffie-hellman-group-exchange-sha256",
"diffie-hellman-group1-sha1",
)
preferred_kex = {
"diffie-hellman-group14-sha1",
"diffie-hellman-group-exchange-sha1",
"diffie-hellman-group-exchange-sha256",
"diffie-hellman-group1-sha1",
}

def __init__(self, *args: Any, **kwargs: Any) -> None:
disabled_algorithms = kwargs.get("disabled_algorithms")
# Set this as long as no "kex" settings being passed via disabled_algorithms
if disabled_algorithms is None or not disabled_algorithms.get("kex"):
paramiko_transport = getattr(paramiko, "Transport")
paramiko_cur_kex = set(paramiko_transport._preferred_kex)
# Disable any kex not in allowed fortinet set
disabled_kex = list(paramiko_cur_kex - self.preferred_kex)
kwargs["disabled_algorithms"] = {"kex": disabled_kex}

super().__init__(*args, **kwargs)

def _try_session_preparation(self, force_data: bool = False) -> None:
super()._try_session_preparation(force_data=force_data)
Expand Down
26 changes: 26 additions & 0 deletions netmiko/nokia/nokia_srl.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,32 @@ def session_preparation(self) -> None:
self.disable_paging(command=command, cmd_verify=True, pattern=r"#")
self.set_base_prompt()

def strip_prompt(self, *args: Any, **kwargs: Any) -> str:
"""Strip the prompt and the additional context line"""
a_string = super().strip_prompt(*args, **kwargs)
return self._strip_context_items(a_string)

def _strip_context_items(self, a_string: str) -> str:
"""Strip NokiaSRL-specific output.
Nokia will put extra context in the 1st line of the prompt, such as:
--{ running }--[ ]--
--{ candidate private private-admin }--[ ]--
--{ candidate private private-admin }--[ ]--
This method removes those lines.
"""
strings_to_strip = [
r"--{.*\B",
]

response_list = a_string.split(self.RESPONSE_RETURN)
last_line = response_list[-1]
for pattern in strings_to_strip:
if re.search(pattern, last_line, flags=re.I):
return self.RESPONSE_RETURN.join(response_list[:-1])
return a_string

def set_base_prompt(
self,
pri_prompt_terminator: str = "#",
Expand Down
7 changes: 7 additions & 0 deletions netmiko/zyxel/zyxel_ssh.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re

from typing import Any, Sequence, Iterator, TextIO, Union
from netmiko.cisco_base_connection import CiscoSSHConnection
from netmiko.no_enable import NoEnable
Expand Down Expand Up @@ -28,3 +30,8 @@ def session_preparation(self) -> None:
super().session_preparation()
# Zyxel switches output ansi codes
self.ansi_escape_codes = True

def strip_ansi_escape_codes(self, string_buffer: str) -> str:
"""Replace '^J' code by next line"""
output = re.sub(r"^\^J", self.RETURN, string_buffer)
return super().strip_ansi_escape_codes(output)
Loading

0 comments on commit 6ec3589

Please sign in to comment.