Maximize ssh audit score

Posted on Nov 23, 2021

So I recently read this article on SSH maximizing an (ssh) audit score (which also might be a great honeypot for naive people like me) and decided to blindly take advice from strangers about things I don't understand, and decided to write yet another playbook in Ansible! You're welcome.

The playbook is pretty short but it will take some time to run the moduli commands - it took hours on the first machine I tried it on (CPU is an Intel Celeron J1900).

With that out of the way, this is the roles structure:

❯ tree
.
├── defaults
│   └── main.yml
├── handlers
│   └── main.yml
└── tasks
    └── main.yml

3 directories, 3 files

First, some variables we set in defaults/main.yml

  ---
  moduli_bits: 3072
  rsa_bits: 4096

  # Should work with most Linux distributions
  moduli_candidate_command: "ssh-keygen -M generate -O bits={{ moduli_bits }} moduli-{{ moduli_bits }}.candidates"
  moduli_file_command: "ssh-keygen -M screen -f moduli-{{ moduli_bits }}.candidates moduli-{{ moduli_bits }}"

And then handlers/main.yml where we restart sshd when needed:

  ---
  - name: restart sshd
    ansible.builtin.service: name=sshd state=restarted

And finally, the task file tasks/main.yml

  ---
  - name: Use FreeBSD specific commands
    set_fact:
      moduli_candidate_command: "ssh-keygen -G moduli-{{ moduli_bits }}.candidates -b {{ moduli_bits }}"
      moduli_file_command: "ssh-keygen -T moduli-{{ moduli_bits }} -f moduli-{{ moduli_bits }}.candidates"
    when: ansible_os_family == 'FreeBSD'

  - name: Run check bit length of RSA host key
    ansible.builtin.shell: "ssh-keygen -l -f ssh_host_rsa_key"
    args:
      chdir: "/etc/ssh"
    register: rsa_check

  - name: Parse output of bit length
    set_fact:
      rsa_bit_length: "{{ rsa_check.stdout.split().0 }}"

  - name: Move old RSA keypair if too small
    ansible.builtin.shell: "mv ssh_host_rsa_key ssh_host_rsa_key.pre-ansible"
    args:
      chdir: "/etc/ssh"
    when: rsa_bit_length|int < 4096

  - name: Regenerate RSA if current is too small
    ansible.builtin.shell: "ssh-keygen -t rsa -b 4096 -f ssh_host_rsa_key -N \"\""
    args:
      chdir: "/etc/ssh"
    when: rsa_bit_length|int < 4096
    notify: restart sshd

  - name: Check if moduli candidate file already exists
    ansible.builtin.stat:
      path: "/etc/ssh/moduli-{{ moduli_bits }}.candidates"
    register: moduli_candidate

  - name: Generate new moduli candidate file
    ansible.builtin.shell: "{{ moduli_candidate_command }}"
    args:
      chdir: "/etc/ssh/"
    when: moduli_candidate is defined and not moduli_candidate.stat.exists

  - name: Check if new moduli file already exists
    ansible.builtin.stat:
      path: "/etc/ssh/moduli-{{ moduli_bits }}"
    register: moduli_file

  - name: Generate new moduli file
    # This might take _hours_ on low end machines
    ansible.builtin.shell: "{{ moduli_file_command }}"
    args:
      chdir: "/etc/ssh/"
    when: moduli_file is defined and not moduli_file.stat.exists
    register: moduli_file_created
    notify: restart sshd

  - name: Backup/move old moduli file
    ansible.builtin.shell: "mv moduli moduli.pre-ansible"
    args:
      chdir: "/etc/ssh"
    when: moduli_file_created is defined and moduli_file_created.changed
    notify: restart sshd

  - name: Symlink to new moduli file
    ansible.builtin.file:
      src: "/etc/ssh/moduli-{{ moduli_bits }}"
      dest: "/etc/ssh/moduli"
      state: link
    when: moduli_file_created is defined and moduli_file_created.changed
    notify: restart sshd

  - name: Tune allowed algorithms
    ansible.builtin.blockinfile:
      path: /etc/ssh/sshd_config
      backup: yes
      validate: /usr/sbin/sshd -T -f %s
      block: |
        HostKeyAlgorithms rsa-sha2-512,rsa-sha2-256,ssh-ed25519
        KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256
        Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
        MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com        
    notify: restart sshd

I used a few barely necessary variables in case I'd like to change the bit lengths or such.

The minimal playbook looks something like this:

  ---
  - name: "Harden sshd"
    hosts: host1, host2
    become: true
    roles:
      - name: ../roles/general/harden-ssh