Scenario / Questions

I have a Linux distribution that is freshly installed. It has a regular user with a default password, and a locked down root account (no password on root – can’t be SSHed as directly). I want to be able to run an ansible playbook to configure this server, and lock down this default account by changing its password, without doing extra out-of-band steps.

Most of the setup that my playbook does needs root, so it is configured to ssh in as the default user, become root, and run all the commands. That works fine. The problem is changing passwords.

If I try to have ansible change the password of the user using the user module, it succeeds, but all tasks after that fail. Even if I do the change as the last step in my playbook, all the handlers that are supposed to run at the end fail.

There are 2 suggestions I have come across online to similar questions:

  • Use RSA keys
  • Use an initial play to somehow test for, and change the password

Using RSA keys requires you to setup RSA ahead of time, and is therefore not in-band.

I’ve been trying to use an initial play to somehow do this, but so far, the second the initial play fails to login with the default password, ansible exits with an error, and ignores other tasks, and the next play.

There’s also a way to do this using ssh-pass, as demonstrated here, which should work, but that seems more of a hack, and ssh-pass isn’t always easily available, like on MacOS machines running ansible.

A similar topic was discussed here over a decade ago, but it looks like Dave got around this by setting Linux to display an “Account Disabled” message when you try to login as the default user, and changing the user instead, which also sounds like it should work, but is different than simply changing the default user’s password.

Is there anything that can be done by ansible to change the password of the user it is running as, without disconnecting?

Find below all possible solutions or suggestions for the above questions..

Suggestion: 1

Is there a way to make Ansible try the default password, and keep running the playbook if that fails?

Here is an example, it doesn’t exactly match what you mentioned, but might be a starting point.

---
# SYNOPSIS
# try authentication using keys, if that fails, fall back to default credentials

- import_playbook: ../bootstrap.yml

- hosts: linux_systems
  gather_facts: no
  become: yes
  any_errors_fatal: true
  vars: 
    ansible_user_first_run: vagrant
    ansible_pass_first_run: vagrant
  tasks:

  - block:
    - name: Check if connection is possible using keys
      command: ssh -F {{project_dir}}/.ssh/ansible_ssh_config -o User={{ ansible_user }} -o ConnectTimeout=10 -o PreferredAuthentications=publickey -o PubkeyAuthentication=yes {{ ansible_host }} /bin/true
      register: result
      connection: local
      ignore_errors: yes
      changed_when: False

    - name: If using user_first_run
      connection: local
      set_fact:
        using_first_run: true
      when: result is failed

    - name: If no connection, change ansible_user
      connection: local
      set_fact:
        ansible_user: "{{ ansible_user_first_run }}"
      when: result is failed

    - name: If no connection, change ansible_ssh_pass
      connection: local
      set_fact:
        ansible_ssh_pass: "{{ ansible_pass_first_run }}"
      when: result is failed

    - name: If no connection, change ansible_become_pass
      connection: local
      set_fact:
        ansible_become_pass: "{{ ansible_pass_first_run }}"
      when: result is failed

    # since any_errors_fatal this should fail the play
    # if we still cannot reach all hosts.
    - name: Check if connection is possible
      raw: /bin/true
      changed_when: False

    tags:
    - always

  - name: Perform a ansible ping
    ping: {}

Suggestion: 2

Wow – I’m dumb. I can’t believe how simple the solution ended up being.

My playbook looks something like this now:

---
- hosts: RaspberryPis
  become: True
  become_user: root

  tasks:
    - name: Do stuff
      notify:
        - restart some service

  handlers:
    - name: restart some service
      systemd:
        name: some_service
        state: restarted

- hosts: RaspberryPis
  become: True
  become_user: root

  tasks:

    - name: Set pi password
      user:
        name: pi
        password: "{{ 'test'|password_hash('sha512', 'mysalt') }}"

I run it with ansible-playbook --ask-pass. The first run, I give it the default password. The next run, I give it the new password. Basically, it’s always the password the device currently has.

Of course for security reasons you’d want the new password to be a variable coming from an ansible vault, or something like that, but that’s the idea, and it seems to work.

Having it in a separate play at the end ensures that the handlers from your main play run before the password is changed. This way, changing the user’s password really is the last step.

I can’t believe I never thought of this before.

This is as simple as I could make it. Is there a way to make Ansible try the default password, and keep running the playbook if that fails? That way, I could have both the default and the new password in my vault, and would only need to supply the vault password – not both the current user password, AND the vault password?