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

ParameterDefaultDescription
user(required)Username
key(required)SSH public key or URL
statepresentpresent or absent
exclusivefalseRemove all other keys
path~/.ssh/authorized_keysKey file path
key_options—SSH key options (e.g., command=, no-pty)
manage_dirtrueCreate .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" }

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.