19

My new server instances are configured to login on root via ssh with password. I want my Ansible playbook to reconfigure it to use keys instead and disable root login with password on first run, so I need something like this:

  • try to login with key
  • if can't login with key:

    • login with password
    • add key to authorized_keys
    • disable root login with password
    • optionally reconnect using key
  • do other tasks

How can I accomplish that?

EDIT: To be clear, I'm not asking how to add key or disable root, that's just for context. I'm asking how to fallback to password if it couldn't authenticate with key. With --ask-pass or ansible_ssh_pass set, Ansible won't even try to use public key authentication

3
  • Good question, what have you tried so far?
    – dawud
    Nov 4, 2013 at 18:08
  • 1
    I took the ssh configuration to a separate playbook and I run it with -k option, then I run the main playbook without -k (using key from agent). I was hoping it can be wrapped in a single playbook though...
    – petr0
    Nov 4, 2013 at 18:26
  • @petr0 This is working for me when the ask-pass password is for a different user than the key (i.e. when you switch remote user after disabling passworded ssh). Not sure if that helps.
    – DylanYoung
    Dec 13, 2018 at 20:27

4 Answers 4

6

You could try the PreferredAuthentications option, setting it to publickey,password. The default includes these in this order, along with other options, so ansible is presumably setting this. Adding it via -o or the client ssh_config may prevent this.

You may be able to use a wrapper script. For example, with this in key_or_password.sh and a pass.sh that gives the password, running bash key_or_password.sh root@host will try a publickey followed by a non-interactive password login.

export DISPLAY=dummy:0
export SSH_ASKPASS=$PWD/pass.sh
exec setsid ssh -v -o 'PreferredAuthentications publickey,password' "$@"

The log indicates which method succeeded with, e.g.

debug1: Authentication succeeded (publickey).
0
9

Here's what I do if ansible_user is different for the 'first run' of the playbook (for example if you only have a root user and you're going to set up a new user with an SSH key):

  • Store the password in ansible_pass as you would if you were using password logins (remember to use Vault) this should be the password of the user you're using for the 'first run' of the playbook.
  • Set ansible_user to the username of the user you wish to use after the first run when you have users set up correctly on the server.
  • Set a variable of ansible_user_first_run to the user you're going to use for the 'first run' of the playbook, for example root.
  • Use a local command to attempt to connect to the server with the correct SSH key, using ignore_errors and changed_when: False
  • If that fails, update ansible_user to the value of ansible_user_first_run

Here's the code:

---
- name: Check if connection is possible
  command: ssh -o User={{ ansible_user }} -o ConnectTimeout=10 -o PreferredAuthentications=publickey -o PubkeyAuthentication=yes {{ inventory_hostname }} echo "Worked"
  register: result
  connection: local
  ignore_errors: yes
  changed_when: False
- name: If no connection, change user_name
  connection: local
  set_fact:
    ansible_user: "{{ ansible_user_first_run }}"
  when: result|failed

Note: It's worth setting up transport = ssh as paramiko can unexpectedly fail to login to the server in some configurations (e.g. when the server is set up not to accept passwords, and you're trying first with a key then a password... weird!) Also ssh transport is faster, so it's worth it anyway.

Further note: If you're using this method, you need to specify gather_facts: false in your playbook definition file so that the setup / fact gathering tasks aren't run automatically before you get to the stage of testing passwords. If you need any of the ansible facts, you will need to explicitly call setup in your role before accessing any of the data normally available in such places as ansible_devices, etc. One good way of doing this is to call setup with a when clause that checks to see if the fact you are using is empty or not before you call it in your role.

2
  • Your solution is good but you must add a gather_facts: no to stop ansible trying to connect when launching the playbook :)
    – k4cy
    Sep 16, 2019 at 14:13
  • You are correct. I have gather_facts set to off for all my playbooks so that I have to call 'setup' explicitly. I forgot to mention this in the answer, I will update it now.
    – Tom Bull
    Sep 18, 2019 at 9:02
5

You can use --ask-pass when running ansible-playbook.

For other tasks you asked, it is achievable by various means, eg, copy module.
Disabling root login also can be done eg. by templating sshd_conf or inserting line in conf file.

1
  • 1
    --ask-pass (which is the same as -k) is what I'm doing and I wrote about it in the comment above
    – petr0
    May 17, 2014 at 10:35
1

I've recently implemented a variant of the answer given above by Tom Bull which works for multiple hosts, sequentially checking for access and prompting for credentials if needed, then running tasks to finish setup (e.g. adding authorised keys, or setting up a user for Ansible):

playbook.yaml:

# Checking access to hosts must run sequentially
- hosts: all
  gather_facts: no
  serial: 1
  tasks:
    - include_tasks: check-access.yaml

# Subsequent tasks can run in parallel as usual 
- hosts: all
  tasks:
    - include_tasks: setup-tasks.yaml

check-access.yaml:

- name: check ansible user exists
  command: ssh -o User={{ ansible_user }} -o ConnectTimeout=10 -o PreferredAuthentications=publickey -o PubkeyAuthentication=yes {{ ansible_host }} echo "Worked"
  register: remote
  connection: local
  ignore_errors: yes
  become: false
  changed_when: False

- name: collect credentials
  block:
    - name: collect username
      pause:
        prompt: "Username for {{ inventory_hostname }} ({{ ansible_host }})"
        echo: yes
      register: username_result

    - name: collect password
      pause:
        prompt: "Password for {{ username_result.user_input }}"
        echo: no
      register: password_result

    - name: update ansible credentials
      set_fact:
        ansible_user: "{{ username_result.user_input }}"
        ansible_password: "{{ password_result.user_input }}"
        ansible_become_password: "{{ password_result.user_input }}"
  when: remote.failed

setup-tasks.yaml will then be run on all hosts, and can include whatever tasks are needed to ensure that the initial access check will succeed when playbook.yaml is next run.

Whilst keeping the check-access.yaml tasks in a separate file does make the main playbook easier to read, it may be possible to bring them into the main playbook - however I have not tried this so cannot be certain that they will run in the correct order.

Note that these tasks cannot be placed into a role, as prompt only works within tasks

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .