Sandbox99 Chronicles

Automating SSH Hardening with Ansible Playbooks

ssh hardening

Written by Jose Mendez

Hi, I’m Jose Mendez, the creator of sandbox99.cc. with a passion for technology and a hands-on approach to learning, I’ve spent more than fifteen years navigating the ever-evolving world of IT.

Published May 20, 2025 | Last updated on May 28, 2025 at 6:40AM

Reading Time: 4 minutes

Introduction

In a previous blog post, we walked through the essential steps of initial server setup using Ansible, laying the groundwork for secure and automated infrastructure management. Continuing that journey, this post focuses on securing SSH access—the most common entry point for remote server administration.

Traditionally, hardening SSH means logging into each server and manually editing the /etc/ssh/sshd_config file. While this works, it doesn’t scale well, and it’s easy to make errors or miss a step, especially when managing multiple servers. That’s where Ansible Playbooks shine.

With Ansible, we can define a secure SSH configuration in code—clear, repeatable, and version-controlled. This method not only reduces human error but also ensures that all your systems follow the same security baseline. Whether you’re enforcing key-based authentication, disabling root login, or limiting access to specific users, a playbook can handle it all in one go.

This approach also keeps your system configurations transparent and reversible. If anything goes wrong or you need to audit your setup, you can refer back to your playbook rather than sifting through config files manually.

In this post, we’ll create a simple yet effective ssh-hardening.yml playbook to automate these best practices.

Playbook – ssh_hardening.yml

  • Please pay close attention to the spacing in this YAML playbook file. Using an editor like VS Code with a YAML extension is highly recommended to help ensure correct formatting.
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
---
# OpenSSH Hardening Ansible Playbook
# This playbook implements security best practices for OpenSSH server
#
# WARNING: This playbook assumes you have already configured key-based authentication!
# Before implementing this playbook, ensure that:
# 1. Your public SSH key is already copied to the target server(s) in ~/.ssh/authorized_keys
# 2. You have verified that you can successfully log in using your SSH key
# 3. You have tested your key-based authentication prior to disabling password authentication
#
# Failure to set up proper key-based authentication before running this playbook
# could result in being locked out of your server(s).
- name: OpenSSH Server Hardening
hosts: all # Replace with the group or hostname
become: true
vars:
# Customize these variables as needed
ssh_port: 2222 # Change default SSH port
# ssh_allow_users: "admin maintenance" # Space-separated list of allowed users
# ssh_allow_groups: "sshusers wheel" # Space-separated list of allowed groups
ssh_client_alive_interval: 300 # Client alive interval in seconds
ssh_client_alive_count_max: 3 # Maximum client alive count
ssh_max_auth_tries: 3 # Maximum authentication attempts
ssh_max_sessions: 4 # Maximum sessions
ssh_login_grace_time: 30 # Login grace time in seconds
ssh_banner_path: "/etc/ssh/banner" # Path to SSH banner file
tasks:
- name: Ensure OpenSSH is up to date
package:
name: openssh-server
state: latest
notify: Restart sshd
- name: Create SSH banner file
copy:
dest: "{{ ssh_banner_path }}"
content: |
*******************************************************************
* AUTHORIZED ACCESS ONLY *
* This system is restricted to authorized users for legitimate *
* business purposes only. Unauthorized access is prohibited. *
*******************************************************************
mode: '0644'
- name: Configure SSH server
lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: present
validate: "sshd -t -f %s"
with_items:
# Disable root login
- regexp: '^#?PermitRootLogin'
line: 'PermitRootLogin no'
# Disable password authentication (already done in your setup, but ensuring it's explicitly set)
- regexp: '^#?PasswordAuthentication'
line: 'PasswordAuthentication no'
# Disable challenge-response authentication
- regexp: '^#?ChallengeResponseAuthentication'
line: 'ChallengeResponseAuthentication no'
# Disable empty passwords
- regexp: '^#?PermitEmptyPasswords'
line: 'PermitEmptyPasswords no'
# Use strong crypto algorithms
- regexp: '^#?Ciphers'
line: 'Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr'
- regexp: '^#?MACs'
line: 'MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256'
- regexp: '^#?KexAlgorithms'
line: 'KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256'
# Set SSH port
- regexp: '^#?Port'
line: 'Port {{ ssh_port }}'
# Set client alive interval
- regexp: '^#?ClientAliveInterval'
line: 'ClientAliveInterval {{ ssh_client_alive_interval }}'
# Set client alive count max
- regexp: '^#?ClientAliveCountMax'
line: 'ClientAliveCountMax {{ ssh_client_alive_count_max }}'
# Set login grace time
- regexp: '^#?LoginGraceTime'
line: 'LoginGraceTime {{ ssh_login_grace_time }}'
# Set max authentication tries
- regexp: '^#?MaxAuthTries'
line: 'MaxAuthTries {{ ssh_max_auth_tries }}'
# Set max sessions
- regexp: '^#?MaxSessions'
line: 'MaxSessions {{ ssh_max_sessions }}'
# # Set allowed users
# - regexp: '^#?AllowUsers'
# line: 'AllowUsers {{ ssh_allow_users }}'
# # Set allowed groups
# - regexp: '^#?AllowGroups'
# line: 'AllowGroups {{ ssh_allow_groups }}'
# Disable X11 forwarding
- regexp: '^#?X11Forwarding'
line: 'X11Forwarding no'
# Enable PrintMotd
- regexp: '^#?PrintMotd'
line: 'PrintMotd yes'
# Use custom banner
- regexp: '^#?Banner'
line: 'Banner {{ ssh_banner_path }}'
# Log more information
- regexp: '^#?LogLevel'
line: 'LogLevel VERBOSE'
# Disable TCP forwarding
- regexp: '^#?AllowTcpForwarding'
line: 'AllowTcpForwarding no'
# Disable agent forwarding
- regexp: '^#?AllowAgentForwarding'
line: 'AllowAgentForwarding no'
# Disable gateway ports
- regexp: '^#?GatewayPorts'
line: 'GatewayPorts no'
# Disable compression (can be exploited)
- regexp: '^#?Compression'
line: 'Compression no'
# Disable tunneling
- regexp: '^#?PermitTunnel'
line: 'PermitTunnel no'
# Use protocol 2 only (redundant on newer versions, but good practice)
- regexp: '^#?Protocol'
line: 'Protocol 2'
# Set idle timeout
- regexp: '^#?TCPKeepAlive'
line: 'TCPKeepAlive yes'
# Use strong host key algorithms
- regexp: '^#?HostKey'
line: 'HostKey /etc/ssh/ssh_host_ed25519_key'
- name: Ensure only strong SSH host keys are used
file:
path: "{{ item }}"
state: absent
with_items:
- /etc/ssh/ssh_host_dsa_key
- /etc/ssh/ssh_host_dsa_key.pub
notify: Restart sshd
- name: Set permissions on SSH directory
file:
path: /etc/ssh
state: directory
mode: '0700'
- name: Set permissions on SSH host keys
file:
path: "{{ item }}"
state: file
mode: '0600'
with_fileglob:
- "/etc/ssh/ssh_host_*_key"
- name: Set permissions on SSH host public keys
file:
path: "{{ item }}"
state: file
mode: '0644'
with_fileglob:
- "/etc/ssh/ssh_host_*_key.pub"
handlers:
- name: Restart sshd
service:
name: sshd
state: restarted
--- # OpenSSH Hardening Ansible Playbook # This playbook implements security best practices for OpenSSH server # # WARNING: This playbook assumes you have already configured key-based authentication! # Before implementing this playbook, ensure that: # 1. Your public SSH key is already copied to the target server(s) in ~/.ssh/authorized_keys # 2. You have verified that you can successfully log in using your SSH key # 3. You have tested your key-based authentication prior to disabling password authentication # # Failure to set up proper key-based authentication before running this playbook # could result in being locked out of your server(s). - name: OpenSSH Server Hardening hosts: all # Replace with the group or hostname become: true vars: # Customize these variables as needed ssh_port: 2222 # Change default SSH port # ssh_allow_users: "admin maintenance" # Space-separated list of allowed users # ssh_allow_groups: "sshusers wheel" # Space-separated list of allowed groups ssh_client_alive_interval: 300 # Client alive interval in seconds ssh_client_alive_count_max: 3 # Maximum client alive count ssh_max_auth_tries: 3 # Maximum authentication attempts ssh_max_sessions: 4 # Maximum sessions ssh_login_grace_time: 30 # Login grace time in seconds ssh_banner_path: "/etc/ssh/banner" # Path to SSH banner file tasks: - name: Ensure OpenSSH is up to date package: name: openssh-server state: latest notify: Restart sshd - name: Create SSH banner file copy: dest: "{{ ssh_banner_path }}" content: | ******************************************************************* * AUTHORIZED ACCESS ONLY * * This system is restricted to authorized users for legitimate * * business purposes only. Unauthorized access is prohibited. * ******************************************************************* mode: '0644' - name: Configure SSH server lineinfile: path: /etc/ssh/sshd_config regexp: "{{ item.regexp }}" line: "{{ item.line }}" state: present validate: "sshd -t -f %s" with_items: # Disable root login - regexp: '^#?PermitRootLogin' line: 'PermitRootLogin no' # Disable password authentication (already done in your setup, but ensuring it's explicitly set) - regexp: '^#?PasswordAuthentication' line: 'PasswordAuthentication no' # Disable challenge-response authentication - regexp: '^#?ChallengeResponseAuthentication' line: 'ChallengeResponseAuthentication no' # Disable empty passwords - regexp: '^#?PermitEmptyPasswords' line: 'PermitEmptyPasswords no' # Use strong crypto algorithms - regexp: '^#?Ciphers' line: 'Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr' - regexp: '^#?MACs' line: 'MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256' - regexp: '^#?KexAlgorithms' line: 'KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256' # Set SSH port - regexp: '^#?Port' line: 'Port {{ ssh_port }}' # Set client alive interval - regexp: '^#?ClientAliveInterval' line: 'ClientAliveInterval {{ ssh_client_alive_interval }}' # Set client alive count max - regexp: '^#?ClientAliveCountMax' line: 'ClientAliveCountMax {{ ssh_client_alive_count_max }}' # Set login grace time - regexp: '^#?LoginGraceTime' line: 'LoginGraceTime {{ ssh_login_grace_time }}' # Set max authentication tries - regexp: '^#?MaxAuthTries' line: 'MaxAuthTries {{ ssh_max_auth_tries }}' # Set max sessions - regexp: '^#?MaxSessions' line: 'MaxSessions {{ ssh_max_sessions }}' # # Set allowed users # - regexp: '^#?AllowUsers' # line: 'AllowUsers {{ ssh_allow_users }}' # # Set allowed groups # - regexp: '^#?AllowGroups' # line: 'AllowGroups {{ ssh_allow_groups }}' # Disable X11 forwarding - regexp: '^#?X11Forwarding' line: 'X11Forwarding no' # Enable PrintMotd - regexp: '^#?PrintMotd' line: 'PrintMotd yes' # Use custom banner - regexp: '^#?Banner' line: 'Banner {{ ssh_banner_path }}' # Log more information - regexp: '^#?LogLevel' line: 'LogLevel VERBOSE' # Disable TCP forwarding - regexp: '^#?AllowTcpForwarding' line: 'AllowTcpForwarding no' # Disable agent forwarding - regexp: '^#?AllowAgentForwarding' line: 'AllowAgentForwarding no' # Disable gateway ports - regexp: '^#?GatewayPorts' line: 'GatewayPorts no' # Disable compression (can be exploited) - regexp: '^#?Compression' line: 'Compression no' # Disable tunneling - regexp: '^#?PermitTunnel' line: 'PermitTunnel no' # Use protocol 2 only (redundant on newer versions, but good practice) - regexp: '^#?Protocol' line: 'Protocol 2' # Set idle timeout - regexp: '^#?TCPKeepAlive' line: 'TCPKeepAlive yes' # Use strong host key algorithms - regexp: '^#?HostKey' line: 'HostKey /etc/ssh/ssh_host_ed25519_key' - name: Ensure only strong SSH host keys are used file: path: "{{ item }}" state: absent with_items: - /etc/ssh/ssh_host_dsa_key - /etc/ssh/ssh_host_dsa_key.pub notify: Restart sshd - name: Set permissions on SSH directory file: path: /etc/ssh state: directory mode: '0700' - name: Set permissions on SSH host keys file: path: "{{ item }}" state: file mode: '0600' with_fileglob: - "/etc/ssh/ssh_host_*_key" - name: Set permissions on SSH host public keys file: path: "{{ item }}" state: file mode: '0644' with_fileglob: - "/etc/ssh/ssh_host_*_key.pub" handlers: - name: Restart sshd service: name: sshd state: restarted
---
# OpenSSH Hardening Ansible Playbook
# This playbook implements security best practices for OpenSSH server
#
# WARNING: This playbook assumes you have already configured key-based authentication!
# Before implementing this playbook, ensure that:
#   1. Your public SSH key is already copied to the target server(s) in ~/.ssh/authorized_keys
#   2. You have verified that you can successfully log in using your SSH key
#   3. You have tested your key-based authentication prior to disabling password authentication
# 
# Failure to set up proper key-based authentication before running this playbook 
# could result in being locked out of your server(s).

- name: OpenSSH Server Hardening
  hosts: all # Replace with the group or hostname
  become: true
  vars:
    # Customize these variables as needed
    ssh_port: 2222 # Change default SSH port
    # ssh_allow_users: "admin maintenance"  # Space-separated list of allowed users
    # ssh_allow_groups: "sshusers wheel"    # Space-separated list of allowed groups
    ssh_client_alive_interval: 300 # Client alive interval in seconds
    ssh_client_alive_count_max: 3 # Maximum client alive count
    ssh_max_auth_tries: 3 # Maximum authentication attempts
    ssh_max_sessions: 4 # Maximum sessions
    ssh_login_grace_time: 30 # Login grace time in seconds
    ssh_banner_path: "/etc/ssh/banner" # Path to SSH banner file

  tasks:
  - name: Ensure OpenSSH is up to date
    package:
      name: openssh-server
      state: latest
    notify: Restart sshd

  - name: Create SSH banner file
    copy:
      dest: "{{ ssh_banner_path }}"
      content: |
        *******************************************************************
        *                      AUTHORIZED ACCESS ONLY                     *
        * This system is restricted to authorized users for legitimate    *
        * business purposes only. Unauthorized access is prohibited.      *
        *******************************************************************
      mode: '0644'

  - name: Configure SSH server
    lineinfile:
      path: /etc/ssh/sshd_config
      regexp: "{{ item.regexp }}"
      line: "{{ item.line }}"
      state: present
      validate: "sshd -t -f %s"
    with_items:
    # Disable root login
    - regexp: '^#?PermitRootLogin'
      line: 'PermitRootLogin no'

    # Disable password authentication (already done in your setup, but ensuring it's explicitly set)
    - regexp: '^#?PasswordAuthentication'
      line: 'PasswordAuthentication no'

    # Disable challenge-response authentication
    - regexp: '^#?ChallengeResponseAuthentication'
      line: 'ChallengeResponseAuthentication no'

    # Disable empty passwords
    - regexp: '^#?PermitEmptyPasswords'
      line: 'PermitEmptyPasswords no'

    # Use strong crypto algorithms
    - regexp: '^#?Ciphers'
      line: 'Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr'

    - regexp: '^#?MACs'
      line: 'MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256'

    - regexp: '^#?KexAlgorithms'
      line: 'KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256'

    # Set SSH port
    - regexp: '^#?Port'
      line: 'Port {{ ssh_port }}'

    # Set client alive interval
    - regexp: '^#?ClientAliveInterval'
      line: 'ClientAliveInterval {{ ssh_client_alive_interval }}'

    # Set client alive count max
    - regexp: '^#?ClientAliveCountMax'
      line: 'ClientAliveCountMax {{ ssh_client_alive_count_max }}'

    # Set login grace time
    - regexp: '^#?LoginGraceTime'
      line: 'LoginGraceTime {{ ssh_login_grace_time }}'

    # Set max authentication tries
    - regexp: '^#?MaxAuthTries'
      line: 'MaxAuthTries {{ ssh_max_auth_tries }}'

    # Set max sessions
    - regexp: '^#?MaxSessions'
      line: 'MaxSessions {{ ssh_max_sessions }}'

    # # Set allowed users
    # - regexp: '^#?AllowUsers'
    #   line: 'AllowUsers {{ ssh_allow_users }}'

    # # Set allowed groups
    # - regexp: '^#?AllowGroups'
    #   line: 'AllowGroups {{ ssh_allow_groups }}'

    # Disable X11 forwarding
    - regexp: '^#?X11Forwarding'
      line: 'X11Forwarding no'

    # Enable PrintMotd
    - regexp: '^#?PrintMotd'
      line: 'PrintMotd yes'

    # Use custom banner
    - regexp: '^#?Banner'
      line: 'Banner {{ ssh_banner_path }}'

    # Log more information
    - regexp: '^#?LogLevel'
      line: 'LogLevel VERBOSE'

    # Disable TCP forwarding
    - regexp: '^#?AllowTcpForwarding'
      line: 'AllowTcpForwarding no'

    # Disable agent forwarding
    - regexp: '^#?AllowAgentForwarding'
      line: 'AllowAgentForwarding no'

    # Disable gateway ports
    - regexp: '^#?GatewayPorts'
      line: 'GatewayPorts no'

    # Disable compression (can be exploited)
    - regexp: '^#?Compression'
      line: 'Compression no'

    # Disable tunneling
    - regexp: '^#?PermitTunnel'
      line: 'PermitTunnel no'

    # Use protocol 2 only (redundant on newer versions, but good practice)
    - regexp: '^#?Protocol'
      line: 'Protocol 2'

    # Set idle timeout
    - regexp: '^#?TCPKeepAlive'
      line: 'TCPKeepAlive yes'

    # Use strong host key algorithms
    - regexp: '^#?HostKey'
      line: 'HostKey /etc/ssh/ssh_host_ed25519_key'

  - name: Ensure only strong SSH host keys are used
    file:
      path: "{{ item }}"
      state: absent
    with_items:
    - /etc/ssh/ssh_host_dsa_key
    - /etc/ssh/ssh_host_dsa_key.pub
    notify: Restart sshd

  - name: Set permissions on SSH directory
    file:
      path: /etc/ssh
      state: directory
      mode: '0700'

  - name: Set permissions on SSH host keys
    file:
      path: "{{ item }}"
      state: file
      mode: '0600'
    with_fileglob:
    - "/etc/ssh/ssh_host_*_key"

  - name: Set permissions on SSH host public keys
    file:
      path: "{{ item }}"
      state: file
      mode: '0644'
    with_fileglob:
    - "/etc/ssh/ssh_host_*_key.pub"

  handlers:
  - name: Restart sshd
    service:
      name: sshd
      state: restarted

Key Configuration Changes

  1. Update to the latest version – Always run the most recent stable version of OpenSSH to benefit from security patches
  2. Use strong authentication methods:
    • Disable password authentication in favor of key-based authentication
  3. Restrict access:
    • Limit which users can SSH into the system
    • Restrict access by IP address or network range
    • Use TCP wrappers or firewall rules
  4. Modify default settings:
    • Change the default port from 22
    • Disable root login
    • Limit login attempts
    • Set strict session timeouts

How to use this playbook:

  1. Customize the variables in the vars section to match your environment:
    • ssh_port: Choose a non-standard port
    • ssh_allow_users and ssh_allow_groups: Specify which users/groups can SSH in
    • Adjust timeouts and other settings according to your needs
  2. Run the playbook with: ansible-playbook ssh_hardening.yml -i your_inventory
  3. Important: Test these changes carefully! Since the playbook changes the SSH port and other settings, you should verify access after applying.

Final Thoughts

Hardening SSH is a fundamental step in securing your Linux infrastructure, and doing it through Ansible brings efficiency, consistency, and peace of mind. Instead of treating SSH configuration as a one-off manual task, you now have a reproducible and scalable way to secure access across your entire fleet.

Ansible playbooks make security policies portable and auditable—key attributes in any professional or homelab setup. As your infrastructure grows, these small automations will pay off significantly in time saved and vulnerabilities avoided.

In future posts, we’ll build on this foundation to cover topics like firewall automation. For now, take pride in knowing that your servers are a little more secure—and a lot more manageable.

Calendar

June 2025
S M T W T F S
1234567
891011121314
15161718192021
22232425262728
2930  

Related Post

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.