I'm trying to write an Ansible playbook to bootstrap my servers. By default on Linode I can only login as root with a password, so my playbook logs in as root, creates a non-root user with an SSH key, and disables root and password SSH.

This is a problem because I now can't run that playbook again since root login is disabled! I would like the playbook to be idempotent and not have to add and remove hosts after bootstrapping them.


I like to do it this way:

- hosts: all
  remote_user: root
  gather_facts: no
    - name: Check ansible user
      command: ssh -q -o BatchMode=yes -o ConnectTimeout=3 ansible@{{ inventory_hostname }} "echo OK"
      changed_when: false
      failed_when: false
      register: check_ansible_user
    - block:
      - name: Create Ansible user
          name: ansible
          comment: "Ansible user"
          password: $6$u3GdHI6FzXL01U9q$LENkJYHcA/NbnXAoJ1jzj.n3a7X6W35rj2TU1kSx4cDtgOEV9S6UboZ4BQ414UDjVvpaQhTt8sXVtkPvOuNt.0
          shell: /bin/bash
      - name: Add authorized key
          user: ansible
          key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
          exclusive: yes
      - name: Allow sudo for ansible
          content: ansible ALL=(ALL) ALL
          dest: /etc/sudoers.d/ansible
          mode: 0600
      when: check_ansible_user | failed

I try to connect to the remote host with my ansible user. If this is impossible (on the first run), I connect as root and create the ansible user along with its authorized_keys file and sudo rights.

On subsequent runs, connecting as ansible user works, so the block of tasks can be skipped.

Once the remote host is bootstrapped, I can go on with the ansible user and become:

- hosts: all
  remote_user: ansible
  become: yes
    - ...
  • Are you manually changing the remote_user in your playbook after that first run? That isn't idempotent. I hope I'm missing something. – Deefour Sep 16 '18 at 2:30
  • 1
    You do, I don't change anything manually. The two codesamples represent two different plays (maybe it helps to imagine them as being called bootstrap.yml and site.yml, where site.yml includes bootstrap.yml before anything else). If the first task of bootstrap.yml fails, all other tasks of this play are skipped and site.yml takes over. – Michael Trojanek Sep 16 '18 at 15:25
  • copy-pasted the code snippet, but ansible skips that block of tasks: "skip_reason": "Conditional result was False". Running the play with -vvv shows the ssh call returns "msg": "non-zero return code", "rc": 255, – Rafa Sep 30 '19 at 9:55
  • 1
    I fixed changing the when condition: when: not "OK" in check_ansible_user.stdout – Rafa Sep 30 '19 at 13:27

I would do the following:

  • create a role (something like 'base') where you (amongst other things), create a suitable user (and sudo rules) for ansible to use
  • create or adapt your role for SSH, to manage sshd_config (I would tend to recommend you manage the entire file, using a template, but that is up to you), and disable root logins
  • make your SSH role depend on the base role, e.g. using meta.

For the first role (the base one), I tend to use something like:

 name: base | local ansible user | create user
    name: "{{ local_ansible_user }}"
    group: "{{ local_ansible_group }}"
    home: "/home/{{ local_ansible_user }}"
    state: present
    generate_ssh_key: "{{ local_ansible_generate_key }}"
    ssh_key_bits: 4096
    ssh_key_type: rsa
    - ansible
    - local_user

- name: base | local ansible user | provision authorised keys
    user: "{{ local_ansible_user }}"
    state: present
    key: "{{ item }}"
  with_items: "{{ local_ansible_authorised_keys }}"
    - ansible
    - authorised_keys

For the SSH config, I would use:

- name: openssh | server | create configuration
    src: sshd_config.j2
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: "0640"
    validate: "/usr/sbin/sshd -tf %s"
    - openssh | server | restart
    - ssh
    - openssh

Ansible's role dependencies are documented here.

You could also just use ordering within your playbook to do this.

I have some ansible stuff on github (from which the above is taken), if you want to see it in context


If you create your servers on Linode with the linode module you could register the return value of the linode task and include the the bootstrap tasks with an condition checking the outout of the linode task. That should be idempotent. Try something like this:

- linode:
    api_key: 'longStringFromLinodeApi'
    name: linode-test1
    plan: 1
    datacenter: 2
    distribution: 99
    password: 'superSecureRootPassword'
    private_ip: yes
    ssh_pub_key: 'ssh-rsa qwerty'
    swap: 768
    wait: yes
    wait_timeout: 600
    state: present
  register: linode_node

- include: bootstrap.yml
  when: linode_node.changed

bootstrap.yml would than contain all the tasks necessary to disable ssh root login and so on.


Maybe you could just modify the ansible_ssh_user in the inventory after you have bootstrapped the host?


other1.example.com ansible_connection=ssh ansible_ssh_user=root # new host
other2.example.com ansible_connection=ssh ansible_ssh_user=user # bootstrapped host

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.