5

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?

Tal
  • 295
  • 1
  • 4
  • 11
  • 3
    I solved this by moving the task to change the password to the end in the ansible playbook. – Gerald Schneider Apr 23 '18 at 16:31
  • I have tried this - handlers run after all tasks, and will fail if you move this to the end. I wonder if I can just move it to a completely new play at the end... Do a play's handlers run at the end of that play, or at the end of the last play? Lets see... – Tal Apr 23 '18 at 17:04
  • 3
    You can use `meta: flush_handlers` to force all pending handlers to be run. You can use `meta: reset_connection` to restart the connection. If you do combine that with using set_fact to update the variables that store the ssh password, then you should be able to continue executing tasks with the updated credentials. – Zoredache Apr 23 '18 at 19:57
  • @Zoredache wow - that sounds great. Another simple solution. Thanks! – Tal Apr 23 '18 at 22:17

2 Answers2

2

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: {}
Zoredache
  • 128,755
  • 40
  • 271
  • 413
  • Several interesting things being used here that I haven't come across before, like blocks and tags. Looking them up, they seem easy enough to understand, and they sound very useful. The idea itself of how to do one of several types of authentications is very interesting as well, and I might end up using over my solution. Thanks! – Tal Apr 23 '18 at 22:14
1

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?

Tal
  • 295
  • 1
  • 4
  • 11