diff --git a/roles/sshd/README.md b/roles/sshd/README.md new file mode 100644 index 0000000..1c4d1d9 --- /dev/null +++ b/roles/sshd/README.md @@ -0,0 +1,37 @@ +ansible-sshd +========= + +Deploy a hardened sshd server + +Requirements +------------ + +None + +Role Variables +-------------- + +None + +Dependencies +------------ + +None + +Example Playbook +---------------- + +```yaml +roles: + - role: genlab.sshd +``` + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me diff --git a/roles/sshd/defaults/main.yml b/roles/sshd/defaults/main.yml new file mode 100644 index 0000000..7f8dce0 --- /dev/null +++ b/roles/sshd/defaults/main.yml @@ -0,0 +1,5 @@ +--- +disable_pam: false +password_auth: false +challenge_response_auth: false +gss_api_auth: false diff --git a/roles/sshd/handlers/main.yml b/roles/sshd/handlers/main.yml new file mode 100644 index 0000000..9f08055 --- /dev/null +++ b/roles/sshd/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: "Restart sshd" + ansible.builtin.service: + name: ssh + state: restarted + enabled: true + daemon_reload: true diff --git a/roles/sshd/meta/main.yml b/roles/sshd/meta/main.yml new file mode 100644 index 0000000..45b38d2 --- /dev/null +++ b/roles/sshd/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: sshd + namespace: genlab + author: "Alexander Gorelyshev" + company: "Genlab, LLC" + description: "Deploy a hardened sshd server" + license: "MIT" + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: [ "focal", "jammy" ] + + galaxy_tags: [ ] + +dependencies: [] diff --git a/roles/sshd/tasks/algorithms.yml b/roles/sshd/tasks/algorithms.yml new file mode 100644 index 0000000..3eb0150 --- /dev/null +++ b/roles/sshd/tasks/algorithms.yml @@ -0,0 +1,33 @@ +--- +# NOTE: order of preference for openssh-server ed25519 -> rsa +- name: "Algorithms | enable ed25519 authentication algorithm" + notify: Restart sshd + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^HostKey /etc/ssh/ssh_host_ed25519_key' + line: 'HostKey /etc/ssh/ssh_host_ed25519_key' + validate: sshd -f %s -t + +- name: "Algorithms | enable the RSA authentication algorithm" + notify: Restart sshd + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^HostKey /etc/ssh/ssh_host_rsa_key' + line: 'HostKey /etc/ssh/ssh_host_rsa_key' + validate: sshd -f %s -t + +- name: "Algorithms | disable the ECDSA algorithm (deemed to be less safe)" + notify: Restart sshd + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^HostKey /etc/ssh/ssh_host_ecdsa_key' + state: absent + validate: sshd -f %s -t + +- name: "Algorithms | disable the DSA algorithm (considered to be defunct)" + notify: Restart sshd + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^HostKey /etc/ssh/ssh_host_dsa_key' + state: absent + validate: sshd -f %s -t diff --git a/roles/sshd/tasks/authentication.yml b/roles/sshd/tasks/authentication.yml new file mode 100644 index 0000000..26b690a --- /dev/null +++ b/roles/sshd/tasks/authentication.yml @@ -0,0 +1,31 @@ +--- +- name: "Authentication | Configure SSH authentication settings" + notify: Restart sshd + loop: + - { regexp: '^#?\s*PubkeyAuthentication\s+', line: 'PubkeyAuthentication yes' } + - { regexp: '^#?\s*PasswordAuthentication\s+', line: 'PasswordAuthentication {{ password_auth | ternary("yes", "no") }}' } + - { regexp: '^#?\s*PermitEmptyPasswords\s+', line: 'PermitEmptyPasswords no' } + - { regexp: '^#?\s*ChallengeResponseAuthentication\s+', line: 'ChallengeResponseAuthentication {{ challenge_response_auth | ternary("yes", "no") }}' } + - { regexp: '^#?\s*GSSAPIAuthentication\s+', line: 'GSSAPIAuthentication {{ gss_api_auth | ternary("yes", "no") }}' } + - { + regexp: '^#?\s*AuthenticationMethods\s+', + line: "{{ 'AuthenticationMethods publickey password' if password_auth else 'AuthenticationMethods publickey' }}" + } + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + validate: /usr/sbin/sshd -t -f %s + +- name: "Check if there is an SSH config forced by cloud-init" + register: sshd_cloud_init + ansible.builtin.stat: + path: "/etc/ssh/sshd_config.d/50-cloud-init.conf" + +- name: "Authentication | override password authentication by cloud-init to '{{ password_auth | ternary('yes', 'no') }}'" + when: sshd_cloud_init.stat.exists + notify: Restart sshd + ansible.builtin.lineinfile: + path: "/etc/ssh/sshd_config.d/50-cloud-init.conf" + regexp: '^#?PasswordAuthentication' + line: 'PasswordAuthentication {{ password_auth | ternary("yes", "no") }}' diff --git a/roles/sshd/tasks/encryption.yml b/roles/sshd/tasks/encryption.yml new file mode 100644 index 0000000..660a369 --- /dev/null +++ b/roles/sshd/tasks/encryption.yml @@ -0,0 +1,11 @@ +--- +- name: "Encryption | remove unsafe host keys" + loop: + - /etc/ssh/ssh_host_ecdsa_key + - /etc/ssh/ssh_host_ecdsa_key.pub + - /etc/ssh/ssh_host_dsa_key + - /etc/ssh/ssh_host_dsa_key.pub + notify: Restart sshd + ansible.builtin.file: + path: "{{ item }}" + state: absent diff --git a/roles/sshd/tasks/install.yml b/roles/sshd/tasks/install.yml new file mode 100644 index 0000000..1940c90 --- /dev/null +++ b/roles/sshd/tasks/install.yml @@ -0,0 +1,12 @@ +--- +- name: "Install | install OpenSSH (RHEL flavours)" + when: ansible_os_family == "RHEL" + ansible.builtin.dnf: + name: openssh + state: installed + +- name: "Install | Install OpenSSH (Debian flavours)" + when: ansible_os_family == "Debian" + ansible.builtin.apt: + name: openssh-server + state: present diff --git a/roles/sshd/tasks/main.yml b/roles/sshd/tasks/main.yml new file mode 100644 index 0000000..d35222a --- /dev/null +++ b/roles/sshd/tasks/main.yml @@ -0,0 +1,23 @@ +--- +- name: "Install an OpenSSH server" + ansible.builtin.include_tasks: "install.yml" + +- name: "Configure SSH algorithms" + ansible.builtin.include_tasks: "algorithms.yml" + +- name: "Configure authentication methods" + ansible.builtin.include_tasks: "authentication.yml" + +- name: "Configure SSH encryption keys" + ansible.builtin.include_tasks: "encryption.yml" + +- name: "Configure additional restrictions" + ansible.builtin.include_tasks: "restrictions.yml" + +- name: "Log at VERBOSE level" + notify: Restart sshd + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?LogLevel' + line: 'LogLevel VERBOSE' + validate: sshd -f %s -t diff --git a/roles/sshd/tasks/restrictions.yml b/roles/sshd/tasks/restrictions.yml new file mode 100644 index 0000000..723a007 --- /dev/null +++ b/roles/sshd/tasks/restrictions.yml @@ -0,0 +1,30 @@ +--- +- name: "Restrictions | Configure SSH security restrictions" + loop: + - { regexp: '^#?Protocol\s+', line: 'Protocol 2' } + - { regexp: '^PermitRootLogin yes', line: 'PermitRootLogin no' } + - { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' } + - { regexp: '^#?IgnoreRhosts', line: 'IgnoreRhosts yes' } + - { regexp: '^#?DebianBanner\s+', line: 'DebianBanner no' } + notify: Restart sshd + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + validate: /usr/sbin/sshd -t -f %s + +- name: "Restrictions | toggle PAM" + notify: Restart sshd + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?UsePAM' + line: "UsePAM {{ disable_pam | ternary('no', 'yes') }}" + validate: sshd -f %s -t + +- name: "Restrictions | ensure the SSHD config is restricted to the root user" + notify: Restart sshd + ansible.builtin.file: + path: /etc/ssh/sshd_config + owner: root + group: root + mode: "0600" diff --git a/roles/sshd/vars/main.yml b/roles/sshd/vars/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/sshd/vars/main.yml @@ -0,0 +1 @@ +---