How to Configure Ansible SSH Fallback from Public Key to Password Authentication


2 views

When provisioning new servers that initially only allow password authentication, we need a way for Ansible to:

  • First attempt public key authentication
  • Gracefully fallback to password auth when keys are rejected
  • Then configure the server to enforce key-based auth

Here's the complete workflow we'll implement:


1. Initial connection attempt with SSH keys
2. If key auth fails → Prompt for password
3. Push public key to server
4. Disable password auth in sshd_config
5. Reconnect to verify key-based auth works
6. Proceed with remaining tasks

We'll use Ansible's ansible_become system with conditional tasks:


# ansible.cfg
[defaults]
host_key_checking = False
ssh_args = -o PreferredAuthentications=publickey,keyboard-interactive,password

---
- hosts: new_servers
  gather_facts: false
  vars_prompt:
    - name: ansible_password
      prompt: "Enter SSH password (will only be used if key auth fails)"
      private: yes

  tasks:
    - name: Attempt initial connection with SSH keys
      ping:
      ignore_errors: yes
      register: key_auth_result
      changed_when: false

    - block:
        - name: Add SSH key when key auth fails
          ansible.posix.authorized_key:
            user: root
            state: present
            key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
            exclusive: yes

        - name: Disable password authentication
          lineinfile:
            path: /etc/ssh/sshd_config
            regexp: '^PasswordAuthentication'
            line: 'PasswordAuthentication no'
            validate: '/usr/sbin/sshd -t -f %s'
          notify: restart sshd

        - name: Verify key-based authentication
          ping:
      when: key_auth_result is failed

    - meta: flush_handlers

    - name: Proceed with server configuration
      # Your regular tasks here
      debug:
        msg: "Key-based auth established, continuing with provisioning"

For complex environments, consider this pattern:


- name: Multi-stage auth setup
  hosts: all
  strategy: free
  tasks:
    - name: First connection attempt (key only)
      command: true
      delegate_to: localhost
      run_once: true
      vars:
        ansible_ssh_common_args: '-o PreferredAuthentications=publickey'

    - name: Second attempt (password fallback)
      command: true
      delegate_to: localhost
      run_once: true
      when: ansible_ssh_host_key_validation == 'failed'
      vars:
        ansible_ssh_common_args: '-o PreferredAuthentications=password'
        ansible_ssh_pass: "{{ lookup('env', 'ANSIBLE_SSH_PASS') }}"
  • Always use vault for password storage in production
  • Implement proper host key verification after initial setup
  • Consider using temporary SSH certificates instead of static keys

Common issues and solutions:


# Verbose SSH debugging
export ANSIBLE_SSH_ARGS="-vvv"

# Force password auth test
ansible -m ping all --ask-pass -e ansible_ssh_common_args=""

When provisioning new servers, we often face a chicken-and-egg problem: we want to configure SSH key authentication and disable password login, but initially the server only accepts password authentication. Ansible's default behavior doesn't handle this transition gracefully.

By default, Ansible will:

  • Try public key authentication first
  • If that fails (and no password is provided), it gives up
  • If a password is provided (via --ask-pass or ansible_ssh_pass), it skips key authentication entirely

We'll create a playbook that:

  1. Attempts key-based authentication
  2. Falls back to password authentication if needed
  3. Configures the server properly
  4. Optionally reconnects using the key

Here's a complete playbook example:


- name: Bootstrap server with SSH key
  hosts: new_servers
  gather_facts: no
  vars:
    ansible_ssh_common_args: '-o PreferredAuthentications=publickey,keyboard-interactive -o PubkeyAuthentication=yes'
    temp_password: "{{ vault_temp_password }}"
  
  tasks:
    - name: Try connection with key (silent failure)
      ping:
      ignore_errors: yes
      register: key_auth_works
      changed_when: false
      tags: always

    - block:
        - name: Set temporary password auth
          set_fact:
            ansible_ssh_pass: "{{ temp_password }}"
            ansible_ssh_common_args: '-o PreferredAuthentications=keyboard-interactive -o PubkeyAuthentication=no'
          when: not key_auth_works|success
          tags: always

        - name: Ensure .ssh directory exists
          file:
            path: /root/.ssh
            state: directory
            mode: '0700'
          when: not key_auth_works|success

        - name: Add authorized key
          authorized_key:
            user: root
            key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
          when: not key_auth_works|success

        - name: Disable password authentication
          lineinfile:
            path: /etc/ssh/sshd_config
            regexp: '^PasswordAuthentication'
            line: 'PasswordAuthentication no'
            state: present
          notify: restart sshd
          when: not key_auth_works|success

        - name: Disable root login
          lineinfile:
            path: /etc/ssh/sshd_config
            regexp: '^PermitRootLogin'
            line: 'PermitRootLogin prohibit-password'
            state: present
          notify: restart sshd
          when: not key_auth_works|success
      when: not key_auth_works|success

  handlers:
    - name: restart sshd
      service:
        name: sshd
        state: restarted
  • PreferredAuthentications: Controls the authentication methods tried
  • ignore_errors: Allows the playbook to continue after failed key auth
  • Conditional execution: Tasks only run when key auth fails
  • Variable override: Dynamically changes connection method

For more complex scenarios, you might use delegation:


- name: First contact via password
  hosts: new_servers
  vars:
    ansible_ssh_pass: "{{ temp_password }}"
  tasks:
    - name: Setup SSH key
      authorized_key:
        user: root
        key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"

- name: Continue with key auth
  hosts: new_servers
  tasks:
    - name: Secure SSH config
      # ... other tasks here

Remember to:

  • Use Ansible Vault for storing temporary passwords
  • Limit password access time window
  • Consider using temporary access tokens instead of root passwords
  • Always test in staging first