SSH Hardening and Automation User Setup with Ansible

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