Maximize ssh audit score
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