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.
--- # 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
- Update to the latest version – Always run the most recent stable version of OpenSSH to benefit from security patches
- Use strong authentication methods:
- Disable password authentication in favor of key-based authentication
- Restrict access:
- Limit which users can SSH into the system
- Restrict access by IP address or network range
- Use TCP wrappers or firewall rules
- Modify default settings:
- Change the default port from 22
- Disable root login
- Limit login attempts
- Set strict session timeouts
How to use this playbook:
- Customize the variables in the
vars
section to match your environment:ssh_port
: Choose a non-standard portssh_allow_users
andssh_allow_groups
: Specify which users/groups can SSH in- Adjust timeouts and other settings according to your needs
- Run the playbook with:
ansible-playbook ssh_hardening.yml -i your_inventory
- 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.
0 Comments