Why libssh
Paramiko is on a long-term deprecation path in the Ansible ecosystem. ansible-pylibssh (libssh) will be the supported SSH backend for network automation going forward, offering better security, maintenance, and feature development. Paramiko has been marked deprecated in ansible.netcommon v8.5.0 and will be removed in a release after 2028-02-01. All network collections should migrate to libssh.
The ssh_type=libssh setting applies to the ansible.netcommon.network_cli connection plugin, which is the primary SSH-based connection used by Ansible network collections.
Prerequisites
Install ansible-pylibssh
pip install ansible-pylibssh>=1.4.0
Verify installation
pip list | grep -i pylibssh
python3 -c "import pylibsshext; print(pylibsshext.__version__)"
Required collection versions
| Collection | Minimum Version | Notes |
|---|---|---|
| ansible.netcommon | >= 6.0.0 | Full libssh support; v8.5.0+ recommended |
| ansible-pylibssh | >= 1.4.0 | Required by netcommon v8.5.0+ |
How ssh_type Works
The ssh_type option controls which Python SSH library the ansible.netcommon.network_cli connection plugin uses to connect to network devices. There are three choices:
| Value | Behavior |
|---|---|
| auto | Default. Uses libssh if ansible-pylibssh is installed, falls back to paramiko |
| libssh | Explicitly use libssh. Fails if ansible-pylibssh is not installed |
| paramiko | Use paramiko (deprecated, removal after 2028-02-01) |
Ways to Set ssh_type=libssh
The following methods configure ssh_type=libssh for the ansible.netcommon.network_cli connection plugin. Pick the one that fits your deployment.
1. Environment variable (global)
export ANSIBLE_NETWORK_CLI_SSH_TYPE=libssh
2. ansible.cfg (project-level)
[persistent_connection]
ssh_type = libssh
3. Inventory variable (per-host or per-group)
[all:vars]
ansible_network_cli_ssh_type=libssh
4. Playbook variable (per-play or per-task)
vars:
ansible_network_cli_ssh_type: libssh
Collection Examples
Cisco IOS (cisco.ios)
Inventory (inventory_ios.ini)
[ios_devices]
ios-router ansible_host=192.168.1.10
[ios_devices:vars]
ansible_connection=ansible.netcommon.network_cli
ansible_network_os=cisco.ios.ios
ansible_user=admin
ansible_password=secret
ansible_network_cli_ssh_type=libssh
Playbook: Gather facts and push config
---
- name: Cisco IOS - libssh examples
hosts: ios_devices
gather_facts: false
tasks:
- name: Gather IOS facts
cisco.ios.ios_facts:
gather_subset: all
- name: Show version
cisco.ios.ios_command:
commands:
- show version
- show ip interface brief
register: output
- name: Display output
ansible.builtin.debug:
var: output.stdout_lines
- name: Configure logging
cisco.ios.ios_config:
lines:
- logging buffered 8192
- logging console informational
save_when: modified
- name: Configure interfaces using resource module
cisco.ios.ios_l3_interfaces:
config:
- name: Loopback100
ipv4:
- address: 10.100.0.1/32
state: merged
Run it
ansible-playbook -i inventory_ios.ini ios_libssh_examples.yaml -vvv
File Transfer with libssh (net_put / net_get)
File transfer operations use the SSH transport directly. When using libssh, the underlying SFTP/SCP implementation comes from ansible-pylibssh.
Upload a file to a device
---
- name: File transfer with libssh
hosts: ios_devices
gather_facts: false
vars:
ansible_network_cli_ssh_type: libssh
tasks:
- name: Upload file to device (SCP)
ansible.netcommon.net_put:
src: ./configs/running_backup.cfg
dest: flash:running_backup.cfg
protocol: scp
- name: Upload file to device (SFTP)
ansible.netcommon.net_put:
src: ./configs/startup_template.cfg
dest: flash:startup_template.cfg
protocol: sftp
Download a file from a device
- name: Download file from device
ansible.netcommon.net_get:
src: running-config
dest: ./backups/{{ inventory_hostname }}_backup.cfg
protocol: scp
Important: File transfer behavior can differ by device, OS image, and protocol (SCP vs SFTP). Test on your platforms after switching from paramiko. Retest whenever you upgrade ansible.netcommon, ansible-pylibssh, or your network collections.
Using ansible.netcommon Modules Directly with libssh
These modules work across all network OSes when ssh_type=libssh is set on the ansible.netcommon.network_cli connection.
---
- name: Generic netcommon modules with libssh
hosts: all
gather_facts: false
vars:
ansible_network_cli_ssh_type: libssh
tasks:
- name: Run arbitrary CLI command
ansible.netcommon.cli_command:
command: show version
register: version_output
- name: Display version
ansible.builtin.debug:
var: version_output.stdout
- name: Push config lines
ansible.netcommon.cli_config:
config: |
interface Loopback99
description Configured via libssh
- name: Network reachability test
ansible.netcommon.net_ping:
dest: 8.8.8.8
count: 3
register: ping_result
ignore_errors: true
Migrating Variables: Paramiko / SSH to libssh
When switching from paramiko (or OpenSSH-based connections) to libssh, several connection variables must be updated. libssh uses its own namespace (ansible_libssh_*) and does not inherit paramiko or OpenSSH variables. If you skip this step, your existing authentication, host key, and proxy settings will be silently ignored — connections may fail or fall back to unexpected defaults with no warning.
Variables that MUST change
These variables are not shared across SSH backends. If you used them with paramiko or OpenSSH, you must replace them with the libssh equivalents.
| Setting | Paramiko / OpenSSH variable | libssh variable | What changed | How to verify |
|---|---|---|---|---|
| Private key file | ansible_ssh_private_key_file or ansible_paramiko_private_key_file | ansible_libssh_private_key_file | libssh does not read the old variable at all. If only the old variable is set, libssh ignores your key and falls back to password auth or fails. | Run a playbook with -vvvv and confirm the log shows Loading private key from <your_key_path>. If you see password authentication instead, the key variable is not being picked up. |
| Host key checking | ansible_host_key_checking or ansible_paramiko_host_key_auto_add | ansible_libssh_host_key_checking | Paramiko’s host_key_auto_add=true silently accepted and saved unknown host keys to known hosts. libssh’s host_key_checking=false skips verification entirely but does not save keys. In production, set to true and pre-populate your known hosts file. | Connect to a device not in ~/.ssh/known_hosts. With host_key_checking=true, the connection should fail. With host_key_checking=false, it should succeed (lab only). |
| Proxy / jump host | ansible_ssh_common_args (e.g., -o ProxyCommand=“ssh -W %h:%p bastion”) | ansible_libssh_proxy_command | This is the most common migration pitfall. ansible_ssh_common_args is OpenSSH-specific and is completely ignored by libssh. Also, drop the -o ProxyCommand= wrapper — pass the command string directly. | Connect to a device behind a bastion host. With -vvvv, confirm the log shows the proxy command being invoked. If the connection goes direct (bypassing the bastion), the old variable is still in use. |
| SSH config file | ~/.ssh/config (auto-loaded by OpenSSH) | ansible_libssh_config_file | OpenSSH automatically reads ~/.ssh/config for host aliases, identity files, and proxy settings. libssh does not auto-load it — you must point to it explicitly. | If you rely on ~/.ssh/config, set ansible_libssh_config_file=~/.ssh/config and verify the connection picks up settings defined there (e.g., a host alias or identity file). |
| Auto-discover keys | ansible_paramiko_look_for_keys | No equivalent | Paramiko could automatically search ~/.ssh/ for usable private keys. libssh does not do this. You must specify your key explicitly. | Unset ansible_libssh_private_key_file and confirm that libssh does not magically find your key. Then set it explicitly and confirm authentication succeeds. |
Variables that stay the same (no change needed)
These variables are shared across all SSH backends (paramiko, libssh, OpenSSH). They work as-is after migration.
| Setting | Variable | Notes |
|---|---|---|
| SSH password | ansible_password or ansible_ssh_pass | Used for password-based authentication. Works the same with libssh. |
| SSH user | ansible_user | The remote username. Shared across all connection types. |
| Connection timeout | ansible_connect_timeout | How long to wait for the initial SSH connection. No change needed. |
| Command timeout | ansible_command_timeout | How long to wait for a command to complete on the device. No change needed. |
| Become / enable | ansible_become, ansible_become_method, ansible_become_pass | Privilege escalation is handled at the connection plugin level, not the SSH library. No change needed. |
Side-by-side example: inventory before and after
Before (paramiko):
[all:vars]
ansible_connection=ansible.netcommon.network_cli
ansible_network_os=cisco.ios.ios
ansible_network_cli_ssh_type=paramiko
# Authentication
ansible_user=admin
ansible_ssh_private_key_file=~/.ssh/id_rsa
# Host key handling
ansible_paramiko_host_key_auto_add=true
# Proxy / bastion
ansible_ssh_common_args=-o ProxyCommand="ssh -W %h:%p bastion.example.com"
# Timeouts (these stay the same)
ansible_connect_timeout=30
ansible_command_timeout=60
After (libssh):
[all:vars]
ansible_connection=ansible.netcommon.network_cli
ansible_network_os=cisco.ios.ios
ansible_network_cli_ssh_type=libssh
# Authentication — variable name changed
ansible_user=admin
ansible_libssh_private_key_file=~/.ssh/id_rsa
#Use true in production (requires pre-populated known hosts); set false only in lab/test
ansible_libssh_host_key_checking=true
# Proxy / bastion — variable name changed, syntax simplified (no -o ProxyCommand= wrapper)
ansible_libssh_proxy_command=ssh -W %h:%p bastion.example.com
# Timeouts (unchanged)
ansible_connect_timeout=30
ansible_command_timeout=60
Migration checklist
Use this checklist to verify you have covered all variable changes:
- Search your inventory files, group_vars/, and host_vars/ for ansible_ssh_private_key_file and ansible_paramiko_private_key_file — replace with ansible_libssh_private_key_file
- Search for ansible_paramiko_host_key_auto_add and ansible_host_key_checking — replace with ansible_libssh_host_key_checking
- Search for ansible_ssh_common_args containing ProxyCommand — replace with ansible_libssh_proxy_command (drop the -o ProxyCommand= prefix)
- Search for ansible_paramiko_look_for_keys — remove it and ensure keys are set explicitly via ansible_libssh_private_key_file
- If you rely on ~/.ssh/config — add ansible_libssh_config_file=~/.ssh/config
- Run a test playbook with -vvvv against each platform and verify in the logs that libssh is the active backend and all authentication/proxy settings are applied correctly
What Else You Should Verify
Before declaring your migration to libssh complete, work through these checks for each platform and workflow you support.
1. Environment and dependencies
- ansible-pylibssh is installed and meets the minimum version: pip show ansible-pylibssh (>= 1.4.0)
- ansible.netcommon collection meets the minimum version: ansible-galaxy collection list ansible.netcommon (>= 6.0.0, 8.5.0+ recommended)
- Python can import the library without error: python3 -c “import pylibsshext; print(pylibsshext.version)”
2. Connection and authentication
- SSH connectivity works with libssh to each target device (password auth)
- SSH connectivity works with libssh using key-based authentication (ansible_libssh_private_key_file)
- Host key verification behaves as expected (accept known hosts, reject unknown in production)
- If using a proxy/jump host: ansible_libssh_proxy_command connects through the bastion successfully
- Connection timeouts are reasonable for your environment (ansible_command_timeout, ansible_connect_timeout)
3. Core operations per platform
For each platform you use (e.g., Cisco IOS):
- *_facts module returns facts successfully (e.g., cisco.ios.ios_facts)
- *_command module executes show commands and returns expected output
- *_config module pushes configuration changes and they persist on the device
- Resource modules (*_interfaces, *_vlans, *_acls, etc.) apply config with state: merged and report correct changed status
- Resource modules with state: replaced and state: deleted behave correctly
- Idempotency: running the same playbook twice results in changed: false on the second run
4. File transfer (if applicable)
- net_put successfully uploads files to the device over SCP
- net_put successfully uploads files to the device over SFTP
- net_get successfully downloads files from the device
- File contents match after transfer (no corruption, no truncation)
- Test with both small files (< 1 KB) and larger files (> 1 MB) if your workflows require it
5. Behavioral differences from paramiko
- Verify that error messages and failure modes are still actionable in your automation (libssh may surface different error text than paramiko)
- If your playbooks rely on ansible_ssh_common_args, switch to ansible_libssh_proxy_command – ansible_ssh_common_args does not apply to libssh
- If you set ansible_paramiko_* variables anywhere, confirm they are replaced with the equivalent ansible_libssh_* or ansible_network_cli_* variables
- Confirm that long-running commands (large config pushes, file copies) complete without unexpected timeouts
Debugging and Troubleshooting
Enable verbose output
ansible-playbook -i inventory.ini playbook.yaml -vvvv
Enable libssh debug logging
export ANSIBLE_LOG_PATH=./ansible_libssh_debug.log
export ANSIBLE_DEBUG=1
At verbosity -vvvv (level 4+), libssh automatically switches to DEBUG-level logging.
Verify which SSH backend is active
Add this task to confirm libssh is being used:
- name: Verify SSH backend
ansible.builtin.debug:
msg: "SSH type configured: {{ ansible_network_cli_ssh_type | default('auto') }}"
Common issues and fixes
| Issue | Fix |
|---|---|
| ModuleNotFoundError: pylibsshext | Install: pip install ansible-pylibssh>=1.4.0 |
| Timeout errors on connection | Increase ansible_command_timeout in inventory or ansible.cfg |
| File transfer fails with net_put | Try switching protocol from scp to sftp or vice versa |
| Host key verification failure | Set ansible_libssh_host_key_checking=false (lab only) |
| Proxy command not working | Use ansible_libssh_proxy_command (not ansible_ssh_common_args) |
Summary
| Connection Type | Variable to Set | Value | Notes |
|---|---|---|---|
| network_cli | ansible_network_cli_ssh_type | libssh | Works with all vendor collections |
| netconf | ansible_netconf_libssh | true | Needs ncclient>=0.7.0 |
| httpapi | N/A | N/A | REST-based, does not use SSH |
| grpc | N/A | N/A | gRPC-based, does not use SSH |
Support
If you encounter any issues while using ssh_type=libssh with the ansible.netcommon.network_cli connection plugin, you can open a GitHub issue on the relevant repository:
- ansible.netcommon (connection plugins, net_put/net_get, cli_command, cli_config): [ Issues · ansible-collections/ansible.netcommon · GitHub ]
- ansible-pylibssh (SSH library itself, connection failures, key exchange, SFTP/SCP): Issues · ansible/pylibssh · GitHub