- Netomate
- Posts
- Ansible Mastery Part 4 :Let’s Talk SSH
Ansible Mastery Part 4 :Let’s Talk SSH
Walkthrough: SSH into Cisco Devices with Ansible Playbooks

Welcome back to our Ansible Mastery Journey. If you are new to here , don’t miss to check previous posts before diving into this post.
In this post, we will have a deep dive into:
Why SSH matters for Ansible
How to set up key-based ssh access(no password required)
Real setup providing steps to push your public key to Cisco routers
Tesing your setup and tshoot with playbooks.
🚀 Why SSH Matters for Ansible
Lets talk about SSH , honestly its one thing which we expect to work while working with ansible.
At its core ,SSH is basically secure tunnel into remote machines. Its how we get in device without physically being there. It used port 22 by default but you can t change it without any issue.
Ansible is agentless, which means it doesn’t need to install anything on the remote machine to work its magic. It just depend upon on SSH to get in, do its job.
So if your SSH setup is broken or misconfigured? Good luck running playbooks. That’s why getting SSH right is half the battle when setting up Ansible.
⚙️ Setting Up SSH for Ansible
Here’s how I usually go about it:
1. Install OpenSSH
Make sure OpenSSH is installed—both on your control machine (where you run Ansible) and the target machines (the ones you want to manage).
To check if it's installed:
[anurudh@localhost ~]$ ssh -V
OpenSSH_8.7p1, OpenSSL 3.2.2 4 Jun 2024
[anurudh@localhost ~]$
[anurudh@localhost ~]$
[anurudh@localhost ~]$
if not , it can be installed like this:
[anurudh@localhost ~]$ sudo yum install openssh-server
Last metadata expiration check: 1:02:08 ago on Sat 03 May 2025 12:33:40 PM IST.
Package openssh-server-8.7p1-45.el9.x86_64 is already installed.
Dependencies resolved.
Nothing to do.
Complete!
[anurudh@localhost ~]$
2. Set Up Key-Based Auth
This is important. You don’t want to be typing passwords every time Ansible connects, right?
So generate a key pair on your control node:
[anurudh@localhost ~]$ ssh-keygen
Now if you check your .ssh
directory:
anurudh@localhost ~]$ ls ~/.ssh/
id_rsa id_rsa.pub known_hosts
[anurudh@localhost ~]$
That id_rsa
is your private key. Keep it safe. Never share it.
And id_rsa.pub
? That’s the one you give to devices so they let you in.
This way, Ansible can log in without needing a password every time.
3. Test the Connection
Before going all-in with Ansible, test the SSH connection manually:
[anurudh@localhost ~]$ ssh [email protected]
If it logs in without asking for a password—you’re done
Lets walk through each step discussed above with real example
🔧 Real-World Example: Pushing Public Key to a Cisco Device
Let’s go step-by-step on setting up key-based SSH access with a Cisco router.
Lets first check ssh directory in master node
[anurudh@localhost ~]$ ls ~/.ssh/
known_hosts
[anurudh@localhost ~]$
Okay, no key yet—let’s generate one:
[anurudh@localhost ~]$ ssh-keygen
After that directory should look like this
[anurudh@localhost ~]$ ls ~/.ssh/
id_rsa id_rsa.pub known_hosts
Here we have got bot private ( id_rsa )
and public key (id_rsa.pub )
generated. Private key should not leave this machine . We will copy public key to network devices to enable SSH key based authentication.
and now we'll take a look into the public key.
[anurudh@localhost ~]$ cat ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDiHucZaT0Q6dYc5LD5tYZBfiioPY6WrsryPLV4K7ypw+bS92fWnPorNi5EUvJY4xJ46uCFvIj14JNGBHiUfDDbmX2ZHqsIGe536fh1V7TmkQl/5n1Aq6tikzjui3tBgh5r7O5A3EF6HoU/FsFPGEWPDMLJP0uv2KIVcD5VJDr9SdR62x2b/+6ZhKV6R6v8seEk31El4oFyGIRh8G9e21avqhl9ikoXor9SEaQx5WEVvO3vjHH+JWgyq2QfR3bqC4tXM+BRgpHiJ9PirL/unneZMAQD/+relOVWymY2uMrtSZFvSoWWxE10CabAn2Lc2Fs7MQM6gRFWo4BaPJCCUMwvcv3g7nmIAVY86BDYnzcSCCkR6xzrY4WB1OPfY9bDunDC3ZUWwMHzl/yB8pagl84g83/r/FfGxABntnM7UMBC2rNHUYJYGSOp0GDTO5iJ2YabBoIy99tFEqzoXrqwHsuFxS/oLtNC26eFFuEZ9eAYX1WbV2x9xWipnz6khJo6hw8= [email protected]
[anurudh@localhost ~]$
So we need to copy and paste this content into Cisco device.
But hold up—Cisco CLI only accepts 256 characters per line. So we need to fold that key before pasting:
[anurudh@localhost ~]$ fold -b -w 64 ~/.ssh/id_rsa.pub
Now let’s jump into the Cisco device:
[anurudh@localhost network_automation_ansible]$ ssh [email protected]([email protected]) Password:
R1#
Inside the router
anurudh@localhost network_automation_ansible]$ ssh [email protected] ([email protected])
Password:
R1#config t Enter configuration commands, one per line. End with CNTL/Z. R1(config)#username ansible_admin
R1(config)#ip ssh pu
R1(config)#ip ssh pubkey-chain
R1(conf-ssh-pubkey)#username ansible_admin
R1(conf-ssh-pubkey-user)#key-? key-hash key-string
R1(conf-ssh-pubkey-user)#key-str
R1(conf-ssh-pubkey-user)#key-string
R1(conf-ssh-pubkey-data)#
we will paste public key copied into cisco terminal
R1(conf-ssh-pubkey-user)#key-string
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDiHucZaT0Q6dYc5LD5tYZBfiio
PY6WrsryPLV4K7ypw+bS92fWnPorNi5EUvJY4xJ46uCFvIj14JNGBHiUfDDbmX2Z
HqsIGe536fh1V7TmkQl/5n1Aq6tikzjui3tBgh5r7O5A3EF6HoU/FsFPGEWPDMLJ
P0uv2KIVcD5VJDr9SdR62x2b/+6ZhKV6R6v8seEk31El4oFyGIRh8G9e21avqhl9
ikoXor9SEaQx5WEVvO3vjHH+JWgyq2QfR3bqC4tXM+BRgpHiJ9PirL/unneZMAQD
/+relOVWymY2uMrtSZFvSoWWxE10CabAn2Lc2Fs7MQM6gRFWo4BaPJCCUMwvcv3g
7nmIAVY86BDYnzcSCCkR6xzrY4WB1OPfY9bDunDC3ZUWwMHzl/yB8pagl84g83/r
/FfGxABntnM7UMBC2rNHUYJYGSOp0GDTO5iJ2YabBoIy99tFEqzoXrqwHsuFxS/o
LtNC26eFFuEZ9eAYX1WbV2x9xWipnz6khJo6hw8= [email protected]
domain
Press enter and exit
R1(conf-ssh-pubkey-data)#exit
R1(conf-ssh-pubkey-user)#
R1(conf-ssh-pubkey-user)#exit
R1(conf-ssh-pubkey)#
Now test it again from your control node:
[anurudh@localhost ~]$ ssh [email protected]
R1#
if you’re in without a password prompt—Great we have done it .
If not, and you're being asked for a password, try updating your SSH config:
[anurudh@localhost ~]$ sudo vi /etc/ssh/ssh_config
add:
PubkeyacceptedKeyTypes +ssh-rsa
Lets run below playbook now
Here’s a simple Ansible playbook to check inventory details:
[anurudh@localhost network_automation_ansible]$ cat 03_cisco_inventory_check
---
- name: Cisco inventory check
hosts: ios_devices
#
gather_facts: false
tasks:
- name: print Ansible config location
ansible.builtin.debug:
msg:
- "{{ ansible_config_file }}"
- "{{ inventory_file }}"
- "{{ ansible_facts }}"
- "{{ ansible_user }}"
[anurudh@localhost network_automation_ansible]$
run it
[anurudh@localhost network_automation_ansible]$ ansible-playbook 03_cisco_inventory_check
You should see output like:
[anurudh@localhost network_automation_ansible]$ ansible-playbook 03_cisco_inventory_check
PLAY [Cisco inventory check] *************************************************************************************************************************************************************
TASK [Gathering Facts] *******************************************************************************************************************************************************************
ok: [R2]
ok: [R1]
TASK [print Ansible config location] *****************************************************************************************************************************************************
ok: [R1] => {
"msg": [
"/home/anurudh/network_automation_ansible/ansible.cfg",
"/home/anurudh/network_automation_ansible/hosts.ini",
{
"net_api": "cliconf",
"net_gather_network_resources": [],
"net_gather_subset": [
"default"
],
"net_hostname": "R1", .....},
"ansible_admin"
]
}
ok: [R2] => {
"msg": [
"/home/anurudh/network_automation_ansible/ansible.cfg",
"/home/anurudh/network_automation_ansible/hosts.ini",
{
"net_api": "cliconf",
"net_gather_network_resources": [],
"net_gather_subset": [
"default"
],
"net_hostname": "R2", ....},
"cisco"
]
}
PLAY RECAP *******************************************************************************************************************************************************************************
R1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
R2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
[anurudh@localhost network_automation_ansible]$
So R1 is using the ansible_admin
user we set up for key-based access. R2 is still using the default cisco
user from the group vars.
Thanks for reading Automation! This post is public so feel free to share it.
Lets check our inventory file
[anurudh@localhost network_automation_ansible]$ cat hosts.ini
[routers]
R1 ansible_host=1.1.1.1 ansible_user=ansible_admin
R2 ansible_host=2.2.2.2
[ios_devices:children]
routers
[ios_devices:vars]
ansible_user=cisco
ansible_password=cisco
ansible_connection=network_cli
ansible_network_os=ios
[anurudh@localhost network_automation_ansible]$
R1 has its own user and SSH key setup. R2 falls back to the default group settings. Simple, flexible, and super clean.
let change private key file location to something different , if we get error after playbook , it means that ssh key is working perfectecly
[anurudh@localhost network_automation_ansible]$ cat hosts.ini
[routers]
R1 ansible_host=1.1.1.1 ansible_user=ansible_admin ansible_ssh_private_key_file=/home/anurudh/.ssh/id_rsa_2
R2 ansible_host=2.2.2.2
[ios_devices:children]
routers
[ios_devices:vars]
ansible_user=cisco
ansible_password=cisco
ansible_connection=network_cli
ansible_network_os=ios
[anurudh@localhost network_automation_ansible]$
configured wrong file name —> /home/anurudh/.ssh/id_rsa_2
lets run same playbook again
[anurudh@localhost network_automation_ansible]$ ansible-playbook 03_cisco_inventory_check
You should see output like:
[anurudh@localhost network_automation_ansible]$ ansible-playbook 03_cisco_inventory_check
PLAY [Cisco inventory check] *************************************************************************************************************************************************************
TASK [Gathering Facts] *******************************************************************************************************************************************************************
fatal: [R1]: FAILED! => {"ansible_facts": {}, "changed": false, "failed_modules": {"ansible.legacy.ios_facts": {"failed": true, "invocation": {"module_args": {"available_network_resources": false, "gather_network_resources": null, "gather_subset": ["min"]}}, "msg": "[Errno 2] No such file or directory: '/home/anurudh/.ssh/id_rsa_2'"}}, "msg": "The following modules failed to execute: ansible.legacy.ios_facts\n"}
ok: [R2]
TASK [print Ansible config location] *****************************************************************************************************************************************************
ok: [R2] => {
"msg": [
"/home/anurudh/network_automation_ansible/ansible.cfg",
"/home/anurudh/network_automation_ansible/hosts.ini",
{
"net_api": "cliconf",
"net_gather_network_resources": [],
"net_gather_subset": [
"default"
],
"net_hostname": "R2" ....},
"cisco"
]
}
PLAY RECAP *******************************************************************************************************************************************************************************
R1 : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
R2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
[anurudh@localhost network_automation_ansible]$
R1 is failing as it not having correct ssh key path. so we are good and It means R1 is taking SSH authentication .
Lets modify inventory file and correct it
[anurudh@localhost network_automation_ansible]$ cat hosts.ini
[routers]
R1 ansible_host=1.1.1.1 ansible_user=ansible_admin ansible_ssh_private_key_file=/home/anurudh/.ssh/id_rsa
R2 ansible_host=2.2.2.2
[ios_devices:children]
routers
[ios_devices:vars]
ansible_user=cisco
ansible_password=cisco
ansible_connection=network_cli
ansible_network_os=ios
[anurudh@localhost network_automation_ansible]$
lets run same playbook again
[anurudh@localhost network_automation_ansible]$ ansible-playbook 03_cisco_inventory_check
You should see output like:
[anurudh@localhost network_automation_ansible]$ ansible-playbook 03_cisco_inventory_check
PLAY [Cisco inventory check] *************************************************************************************************************************************************************
TASK [Gathering Facts] *******************************************************************************************************************************************************************
ok: [R1]
ok: [R2]
TASK [print Ansible config location] *****************************************************************************************************************************************************
ok: [R2] => {
"msg": [
"/home/anurudh/network_automation_ansible/ansible.cfg",
"/home/anurudh/network_automation_ansible/hosts.ini",
{
"net_api": "cliconf",
"net_gather_network_resources": [],
"net_gather_subset": [
"default"
],
"net_hostname": "R2".....},
},
"cisco"
]
}
ok: [R1] => {
"msg": [
"/home/anurudh/network_automation_ansible/ansible.cfg",
"/home/anurudh/network_automation_ansible/hosts.ini",
{
"net_api": "cliconf",
"net_gather_network_resources": [],
"net_gather_subset": [
"default"
],
"net_hostname": "R1"......},
"ansible_admin"
]
}
PLAY RECAP *******************************************************************************************************************************************************************************
R1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
R2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
We have done it !!!!
Action Step for You Today
Just one thing: make sure to go through the concepts and follow along with the tasks. Try to complete them within two days after the post is published.
I’ll guide you, one simple post at a time.
We will deep dive more into Ansible in upcoming posts. Don’t miss to DM or ping me with your queries and comments.
Smiles :)
Anurudh
Reply