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:
- Attempts key-based authentication
- Falls back to password authentication if needed
- Configures the server properly
- 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