Bootstrap FreeKB - Ansible - Append or Remove SSH servers keys to known hosts file
Ansible - Append or Remove SSH servers keys to known hosts file

Updated:   |  Ansible articles

If you are not familiar with modules, check out Ansible - Getting Started with Modules.

known_hosts can be used to append an SSH servers SSH key to a clients known hosts file. In this example, an SSH key is appended to /home/john.doe/.ssh/known_hosts2.

---
- hosts: all
  tasks:
  - name: append server1.example.com SSH key to John Doe's known_hosts file
    ansible.builtin.known_hosts:
      path: /home/john.doe/.ssh/known_hosts2
      name: server1.example.com <- must be an exact match of the key DNS name or IP
      key: "server1.example.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNpFc+HimGAZWJcAgRx6P8ycxh2JRHBeaTfVzu/HncxP0nR
    delegate_to: localhost
...

 

known_hosts can also be used to remove one or more SSH key from a known_hosts file.

---
- hosts: all
  tasks:
  - name: remove any SSH name containing name server1 from John Doe's known_hosts file
    ansible.builtin.known_hosts:
      path: /home/john.doe/.ssh/known_hosts2
      name: server1 <- must be an exact match of the key DNS name or IP
      state: absent
    delegate_to: localhost
...

 


Example scenario

Let's say you have a playbook that contains the following.

---
- hosts: all
  tasks:
  - ansible.builtin.stat:
      path: /tmp/example.txt
...

 

Here is one way to run this playbook against server2.example.com. This will make an SSH connection from to server2.example.com.

[john.doe@server1.example.com]# ansible-playbook foo.yml -i server2.example.com,

 

In this example, if the SSH key of server2.example.com is not listed in the /etc/ssh/ssh_known_hosts or /home/username/.ssh/known_hosts file on the control node (server1.example.com), a prompt will appear stating The authenticity of host 'hostname (ip address)' can't be established.

The authenticity of host 'server2.example.com (10.14.157.95)' can't be established
DSA key fingerprint is BB37 83F2 5E3A 7A4C 6C84  F047 D97B DD4E 38BB 2082
Are you sure you want to continue connecting (yes/no)?

 

Typing yes and pressing enter will append the SSH key of server2.example.com to the known_hosts file on the control node (server1.example.com). However, what if you are connecting to 100 target systems? Do you really want to type yes and press enter 100 times? Or, what if you have some sort of silent process that runs the playbooks. It would just hang indefinitely.

 


Solution

Here is how I handle this. I start my playbooks with a section that is run against localhost.

gather_facts must be set to false because the gathering of facts makes an SSH connection.

I then invoke a role as a pre_task to take care of the known hosts.

---
- hosts: localhost
  gather_facts: false
  serial: 1 # see https://github.com/ansible/ansible-modules-extras/issues/2489
  pre_tasks:
  - name: include_role 'knownHosts'
    ansible.builtin.include_role:
      name: knownHosts

- hosts: all
  tasks:
  - ansible.builtin.stat:
      path: /tmp/example.txt
...

 


Known Hosts Role

  • I use the file module to create the users known_hosts file if it does not exist.
  • lineinfile is used to remove any lines in the users known_hosts file that contain the hostname of the target server.
  • command or shell are used to invoke the ssh-keyscan command to get the SSH key.
  • debug is used to display the SSH key
  • known_hosts is used to append the SSH key to the users known_hosts file
- name: set_fact known_hosts_list
  set_fact:
    known_hosts_list: ['known_hosts', 'known_hosts2']

- name: create the known_hosts and known_hosts2 files if they do not exist
  ansible.builtin.file:
    path: "{{ lookup('env', 'HOME') }}/.ssh/{{ item }}"
    owner: "{{ lookup('env', 'USER') }}"
    mode: "0644"
    state: touch
  delegate_to: localhost
  with_items: "{{ known_hosts_list }}"
  run_once: true

- name: use nslookup to check for 'NXDOMAIN' or 'No answer'
  ansible.builtin.shell: nslookup {{ inventory_hostname_short }}
  register: answer
  delegate_to: localhost
  failed_when: answer.rc not in [ 0, 1 ]

- name: print warning if 'NXDOMAIN' or 'No answer' is returned
  ansible.builtin.debug:
    msg: the 'nslookup {{ inventory_hostname_short }}' command returned 'NXDOMAIN' or 'No answer' - the subsequent tasks will be skipped
  when: answer.rc == 1 or answer.stdout is search 'No answer'

- block:

  - name: use nslookup to get the IP address of the target server
    ansible.builtin.shell: "nslookup {{ inventory_hostname_short }} | grep ^Address | tail -1 | sed 's|Address: 
||'"
    register: nslookup
    delegate_to: localhost

  - name: set fact IP address
    ansible.builtin.set_fact:
      ipaddress: "{{ nslookup.stdout }}"

  - name: Remove SSH keys from known_hosts / known_hosts2
    ansible.builtin.known_hosts:
      path: "{{ lookup('env', 'HOME') }}/.ssh/{{ item.key }}"
      name: "{{ item.value }}"
      state: absent
    delegate_to: localhost
    with_items:
    - { key: 'known_hosts',  value: "{{ inventory_hostname | upper }}" }
    - { key: 'known_hosts',  value: "{{ inventory_hostname | lower }}" }
    - { key: 'known_hosts2', value: "{{ inventory_hostname | upper }}" }
    - { key: 'known_hosts2', value: "{{ inventory_hostname | lower }}" }
    - { key: 'known_hosts',  value: "{{ inventory_hostname_short | upper }}" }
    - { key: 'known_hosts',  value: "{{ inventory_hostname_short | lower }}" }
    - { key: 'known_hosts2', value: "{{ inventory_hostname_short | upper }}" }
    - { key: 'known_hosts2', value: "{{ inventory_hostname_short | lower }}" }
    - { key: 'known_hosts',  value: "{{ ipaddress }}" }
    - { key: 'known_hosts2', value: "{{ ipaddress }}" }

  - name: ssh-keyscan for SSH servers SSH key
    connection: local
    ansible.builtin.shell: ssh-keyscan {{ inventory_hostname }},{{ ipaddress }} 2>/dev/null | head -1
    register: keyscan_key

  - name: display the contents of the 'keyscan_key' variable
    ansible.builtin.debug: 
      var: keyscan_key.stdout

  - name: create the tmplist list
    ansible.builtin.set_fact:
      tmplist : "{{ ssh_key.stdout.split() }}"

  - name: create the sshkey_for_known_hosts2 variable
    ansible.builtin.set_fact:
      sshkey_for_known_hosts2: "{{ inventory_hostname_short | upper }},{{ ipaddress }} {{ tmplist [1] }} {{ tmplist [2] }}"

  - name: append SSH key to known_hosts / known_hosts2
    ansible.builtin.known_hosts:
      path: ""{{ lookup('env', 'HOME') }}/.ssh/{{ item.path }}"
      name: "{{ ipaddress }}"
      key: "{{ item.key }}"
    delegate_to: localhost
    with_items:
    - { path: 'known_hosts',  key: "{{ ssh_key.stdout }}" }
    - { path: 'known_hosts2', key: "{{ sshkey_for_known_hosts2 }}" }

  when: answer.rc == 0 and answer.stdout is not search 'No answer'

 




Did you find this article helpful?

If so, consider buying me a coffee over at Buy Me A Coffee



Comments


Add a Comment


Please enter 7895c5 in the box below so that we can be sure you are a human.