SSH Hardening and Automation User Setup with Ansible #
Here’s a little post about how I do SSH hardening for my RHEL9 homelab and how I ensure that the Ansible automation user is properly set. The playbook stems from an incident I had in Red Hat Insights where it was reported that I had an SSH configuration that allowed legacy ciphers. It was also adviced to create a crypto policy that disables weak algorithms.
Like I mentioned, I run a RHEL9 homelab so if you’re running RHEL10 which has been released, you might not have this problem.
SSH Security Hardening #
Disabling Root Login #
The first and most critical step is disabling direct root login via SSH:
- name: Ensure root login is disallowed
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^PermitRootLogin'
line: 'PermitRootLogin no'
state: present
notify: Restart SSH
This forces all users to log in with their own accounts and use sudo for admin operations.
Enforcing SSH Protocol 2 #
While modern SSH implementations default to protocol 2, I explicitly set it to ensure compliance:
- name: Ensure only SSH protocol 2 is allowed
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^Protocol'
line: 'Protocol 2'
state: present
notify: Restart SSH
Configuring Strong Ciphers #
I configure SSH to use only modern, secure ciphers:
- name: Ensure only modern ciphers
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^Ciphers'
line: "Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com"
state: present
notify: Restart SSH
Advanced Crypto Policy Management #
RHEL9 uses system-wide crypto policies, which I leverage to enforce additional security restrictions.
Creating Custom Policy Modules #
I create custom crypto policy modules to disable weak algorithms:
- name: Create NO-CBC policy module
ansible.builtin.copy:
dest: /etc/crypto-policies/policies/modules/NO-CBC.pmod
content: |
cipher@ssh = -*-CBC
mode: '0644'
changed_when: false
- name: Create NO-HMAC-SHA1 policy module
ansible.builtin.copy:
dest: /etc/crypto-policies/policies/modules/NO-HMAC-SHA1.pmod
content: |
mac@ssh = -HMAC-SHA1
mode: '0644'
changed_when: false
Applying the Enhanced Policy #
The custom modules are then applied to the system:
- name: Update crypto policies
ansible.builtin.command:
cmd: >
update-crypto-policies --set DEFAULT:NO-SHA1:NO-CBC:NO-HMAC-SHA1
changed_when: false
Automation User Setup #
Creating the Ansible User #
I ensure that the Ansible automation user is properly configured. This is because I used to have the ansible user be on Red Hat’s IdM (IPA).
- name: Ensure ansible user exists and belongs to wheel group
ansible.builtin.user:
name: ansible
groups: wheel
append: true
state: present
The user is added to the wheel group, which allows sudo access on RHEL systems.
SSH Key Management #
The automation user needs passwordless SSH access:
- name: Ensure ansible user's SSH authorized key is present
ansible.posix.authorized_key:
user: ansible
state: present
key: "{{ ansible_ssh_public_key }}"
The SSH public key is stored in an Ansible Vault file (group_vars/all/vault.yml) to keep it secure and version-controlled. I have written a guide on how to set it up here: How to setup Ansible Vault
Passwordless Sudo Configuration #
For automation to work seamlessly, the ansible user needs passwordless sudo:
- name: Ensure passwordless sudo for ansible user
ansible.builtin.lineinfile:
path: /etc/sudoers
state: present
regexp: '^ansible ALL=\(ALL\) NOPASSWD:ALL'
line: 'ansible ALL=(ALL) NOPASSWD:ALL'
validate: 'visudo -cf %s'
The validate parameter is important as it ensures the sudoers file syntax is correct before applying changes.
Error Handling and Service Management #
Graceful Service Restarts #
SSH configuration changes require service restarts, handled through Ansible handlers:
handlers:
- name: Restart SSH
ansible.builtin.systemd:
name: sshd
state: restarted
Complete Playbook #
Click to expand the complete SSH hardening playbook
---
- name: SSH
hosts: all
become: true
gather_facts: true
vars_files:
- "../group_vars/all/vault.yml"
any_errors_fatal: true
tasks:
- name: Ensure root login is disallowed
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^PermitRootLogin'
line: 'PermitRootLogin no'
state: present
notify: Restart SSH
- name: Ensure only SSH protocol 2 is allowed
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^Protocol'
line: 'Protocol 2'
state: present
notify: Restart SSH
- name: Ensure only modern ciphers
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^Ciphers'
line: "Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com"
state: present
notify: Restart SSH
- name: Create NO-CBC policy module
ansible.builtin.copy:
dest: /etc/crypto-policies/policies/modules/NO-CBC.pmod
content: |
cipher@ssh = -*-CBC
mode: '0644'
changed_when: false
- name: Create NO-HMAC-SHA1 policy module
ansible.builtin.copy:
dest: /etc/crypto-policies/policies/modules/NO-HMAC-SHA1.pmod
content: |
mac@ssh = -HMAC-SHA1
mode: '0644'
changed_when: false
- name: Update crypto policies
ansible.builtin.command:
cmd: >
update-crypto-policies --set DEFAULT:NO-SHA1:NO-CBC:NO-HMAC-SHA1
changed_when: false
- name: Ensure ansible user exists and belongs to wheel group
ansible.builtin.user:
name: ansible
groups: wheel
append: true
state: present
- name: Ensure ansible user's SSH authorized key is present
ansible.posix.authorized_key:
user: ansible
state: present
key: "{{ ansible_ssh_public_key }}"
- name: Ensure passwordless sudo for ansible user
ansible.builtin.lineinfile:
path: /etc/sudoers
state: present
regexp: '^ansible ALL=\(ALL\) NOPASSWD:ALL'
line: 'ansible ALL=(ALL) NOPASSWD:ALL'
validate: 'visudo -cf %s'
handlers:
- name: Restart SSH
ansible.builtin.systemd:
name: sshd
state: restarted