Introduction
SSH is Ansible's default connection method — managing SSH keys and hardening SSH configuration is fundamental. Ansible automates key distribution, sshd configuration, and security hardening across your entire fleet.
Manage Authorized Keys
# Add SSH key for user
- name: Add SSH key
ansible.builtin.authorized_key:
user: deploy
key: "{{ lookup('file', 'files/deploy_key.pub') }}"
state: present
# Add key from URL
- name: Add key from GitHub
ansible.builtin.authorized_key:
user: deploy
key: https://github.com/username.keys
state: present
# Multiple keys
- name: Add team SSH keys
ansible.builtin.authorized_key:
user: deploy
key: "{{ item }}"
loop:
- "ssh-ed25519 AAAA... alice@laptop"
- "ssh-ed25519 BBBB... bob@laptop"
# Exclusive mode (remove all other keys)
- name: Set exclusive keys
ansible.builtin.authorized_key:
user: deploy
key: "{{ lookup('file', 'files/deploy_key.pub') }}"
exclusive: true
# Remove a key
- name: Remove old key
ansible.builtin.authorized_key:
user: oldemployee
key: "ssh-ed25519 XXXX... oldemployee@laptop"
state: absent
authorized_key Parameters
| Parameter | Default | Description |
|---|---|---|
user | (required) | Username |
key | (required) | SSH public key or URL |
state | present | present or absent |
exclusive | false | Remove all other keys |
path | ~/.ssh/authorized_keys | Key file path |
key_options | — | SSH key options (e.g., command=, no-pty) |
manage_dir | true | Create .ssh directory |
comment | — | Key comment |
Key Options (Restrict Access)
# Restrict key to specific command
- name: Backup-only key
ansible.builtin.authorized_key:
user: backup
key: "ssh-ed25519 AAAA... backup-server"
key_options: 'command="/opt/backup.sh",no-port-forwarding,no-X11-forwarding,no-pty'
# Restrict by source IP
- name: IP-restricted key
ansible.builtin.authorized_key:
user: deploy
key: "ssh-ed25519 AAAA... ci-server"
key_options: 'from="10.0.0.50,10.0.0.51"'
Generate SSH Keys
# Generate key pair on remote host
- name: Generate SSH key for user
ansible.builtin.user:
name: deploy
generate_ssh_key: true
ssh_key_type: ed25519
ssh_key_comment: "deploy@{{ inventory_hostname }}"
# Generate key with openssh_keypair module
- name: Generate SSH keypair
community.crypto.openssh_keypair:
path: /home/deploy/.ssh/id_ed25519
type: ed25519
owner: deploy
group: deploy
register: keypair
- debug:
msg: "Public key: {{ keypair.public_key }}"
Harden sshd Configuration
- name: Harden SSH configuration
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
validate: "sshd -t -f %s"
loop:
# Disable root login
- { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
# Disable password authentication
- { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
# Disable empty passwords
- { regexp: '^#?PermitEmptyPasswords', line: 'PermitEmptyPasswords no' }
# Use SSH protocol 2 only
- { regexp: '^#?Protocol', line: 'Protocol 2' }
# Disable X11 forwarding
- { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' }
# Set max auth tries
- { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 3' }
# Idle timeout
- { regexp: '^#?ClientAliveInterval', line: 'ClientAliveInterval 300' }
- { regexp: '^#?ClientAliveCountMax', line: 'ClientAliveCountMax 2' }
# Limit users
# - { regexp: '^#?AllowUsers', line: 'AllowUsers deploy admin' }
notify: Restart sshd
handlers:
- name: Restart sshd
ansible.builtin.service:
name: sshd
state: restarted
Template-Based sshd Config
- name: Deploy sshd config
ansible.builtin.template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
mode: "0600"
validate: "sshd -t -f %s"
notify: Restart sshd
Complete SSH Hardening Playbook
---
- name: Harden SSH access
hosts: all
become: true
vars:
ssh_allowed_users:
- deploy
- admin
ssh_port: 22
tasks:
- name: Set authorized keys for deploy
ansible.builtin.authorized_key:
user: deploy
key: "{{ lookup('file', 'files/' + item + '.pub') }}"
loop: "{{ ssh_allowed_users }}"
- name: Harden sshd config
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
validate: "sshd -t -f %s"
loop:
- { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
- { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
- { regexp: '^#?PermitEmptyPasswords', line: 'PermitEmptyPasswords no' }
- { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' }
- { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 3' }
- { regexp: '^#?ClientAliveInterval', line: 'ClientAliveInterval 300' }
notify: Restart sshd
- name: Ensure .ssh directory permissions
ansible.builtin.file:
path: "/home/{{ item }}/.ssh"
state: directory
owner: "{{ item }}"
mode: "0700"
loop: "{{ ssh_allowed_users }}"
handlers:
- name: Restart sshd
ansible.builtin.service:
name: sshd
state: restarted
SSH Key Rotation
# Rotate keys: add new, remove old
- name: Add new key
ansible.builtin.authorized_key:
user: deploy
key: "{{ lookup('file', 'files/deploy_key_2026.pub') }}"
- name: Remove old key
ansible.builtin.authorized_key:
user: deploy
key: "{{ lookup('file', 'files/deploy_key_2025.pub') }}"
state: absent
Troubleshooting
Locked Out After Disabling Passwords
Always ensure key-based access works BEFORE disabling passwords:
# Test SSH key access first
- name: Verify key access
ansible.builtin.ping:
# THEN disable passwords
- name: Disable password auth
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PasswordAuthentication'
line: 'PasswordAuthentication no'
Permission Errors
# SSH requires strict permissions
- file: { path: ~/.ssh, mode: "0700" }
- file: { path: ~/.ssh/authorized_keys, mode: "0600" }
- file: { path: ~/.ssh/id_ed25519, mode: "0600" }
Related Articles
Conclusion
SSH key management is the foundation of secure Ansible automation. Use authorized_key to distribute keys, exclusive: true for strict control, and key_options to restrict key capabilities. Harden sshd: disable root login, disable password auth, limit auth tries. Always validate sshd config with sshd -t before restarting. Rotate keys regularly. Test key access before disabling password authentication — one wrong move and you're locked out.