diff --git a/roles/sshd/README.md b/roles/sshd/README.md index 6702986..26886ff 100644 --- a/roles/sshd/README.md +++ b/roles/sshd/README.md @@ -1,28 +1,76 @@ sshd ==== -Deploy a hardened `sshd` server +A hardened OpenSSH server role for Debian/Ubuntu and RHEL/Rocky systems. + +This role installs and configures the `sshd` server with secure defaults and a small set of tunable options for authentication and forwarding. Requirements ------------ -None +- Target hosts must be Debian-family or RedHat-family Linux systems + +Supported Platforms +------------------- + +- Debian +- Ubuntu +- RHEL 9+ +- Rocky Linux 9+ Role Variables -------------- -None +Authentication -Dependencies ------------- +- `sshd_disable_pam`: disable PAM support in `sshd_config`. Default: `false` +- `sshd_password_auth`: allow password authentication. Default: `false` +- `sshd_challenge_response_auth`: allow challenge-response authentication. Default: `false` +- `sshd_gss_api_auth`: allow GSSAPI authentication. Default: `false` -None +Forwarding & tunneling + +- `sshd_allow_agent_forwarding`: allow SSH agent forwarding. Default: `false` +- `sshd_allow_tcp_forwarding`: allow TCP forwarding. Default: `false` +- `sshd_gateway_ports`: allow gateway ports. Default: `false` +- `sshd_permit_tunnel`: allow SSH tunneling. Default: `false` + +Access control + +- `sshd_allow_users`: optional space-separated list of users permitted to log in via SSH +- `sshd_allow_groups`: optional space-separated list of groups permitted to log in via SSH + +Behavior +-------- + +By default this role: + +- installs `openssh-server` +- enforces `Protocol 2` +- disables `PermitRootLogin` +- disables `X11Forwarding`, `HostbasedAuthentication`, `KbdInteractiveAuthentication`, and `PermitUserEnvironment` +- disables weak(-er) host keys (`ecdsa`, `dsa`) +- disables empty passwords +- sets `LogLevel VERBOSE` +- restricts `/etc/ssh/sshd_config` to `0600` + +Compatibility Notes +------------------- + +- The role uses `sshd` validation via `/usr/sbin/sshd -t -f %s`. +- Debian/Ubuntu systems use the `ssh` service name; RHEL/Rocky systems use `sshd`. +- The role is intentionally conservative with forwarding and tunneling defaults. Example Playbook ---------------- See: [converge.yml](molecule/default/converge.yml) +Dependencies +------------ + +None + License ------- diff --git a/roles/sshd/defaults/main.yml b/roles/sshd/defaults/main.yml index 5354282..4169bdb 100644 --- a/roles/sshd/defaults/main.yml +++ b/roles/sshd/defaults/main.yml @@ -3,3 +3,7 @@ sshd_disable_pam: false sshd_password_auth: false sshd_challenge_response_auth: false sshd_gss_api_auth: false +sshd_allow_agent_forwarding: false +sshd_allow_tcp_forwarding: false +sshd_gateway_ports: false +sshd_permit_tunnel: false diff --git a/roles/sshd/handlers/main.yml b/roles/sshd/handlers/main.yml index 9f08055..56ced7d 100644 --- a/roles/sshd/handlers/main.yml +++ b/roles/sshd/handlers/main.yml @@ -1,7 +1,18 @@ --- -- name: "Restart sshd" +- name: "Restart the ssh service" + listen: "Restart ssh" + when: ansible_os_family == 'Debian' ansible.builtin.service: name: ssh state: restarted enabled: true daemon_reload: true + +- name: "Restart the sshd service" + listen: "Restart ssh" + when: ansible_os_family == 'RedHat' + ansible.builtin.service: + name: sshd + state: restarted + enabled: true + daemon_reload: true diff --git a/roles/sshd/molecule/default/converge.yml b/roles/sshd/molecule/default/converge.yml index bb22a32..be83814 100644 --- a/roles/sshd/molecule/default/converge.yml +++ b/roles/sshd/molecule/default/converge.yml @@ -3,4 +3,9 @@ hosts: all roles: - role: "genlab.common.sshd" + sshd_password_auth: false + sshd_allow_agent_forwarding: false + sshd_allow_tcp_forwarding: false + sshd_gateway_ports: false + sshd_permit_tunnel: false sshd_allow_users: "testusr" diff --git a/roles/sshd/tasks/algorithms.yml b/roles/sshd/tasks/algorithms.yml index a43581d..1e5faae 100644 --- a/roles/sshd/tasks/algorithms.yml +++ b/roles/sshd/tasks/algorithms.yml @@ -1,6 +1,6 @@ --- # next task requires this directory to exist for sshd -t flag -- name: Ensure /run/sshd exists +- name: "Algorithms | ensure /run/sshd exists" ansible.builtin.file: path: /run/sshd state: directory @@ -10,33 +10,33 @@ # NOTE: order of preference for openssh-server ed25519 -> rsa - name: "Algorithms | enable ed25519 authentication algorithm" - notify: Restart sshd + notify: "Restart ssh" 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 + validate: /usr/sbin/sshd -t -f %s - name: "Algorithms | enable the RSA authentication algorithm" - notify: Restart sshd + notify: "Restart ssh" 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 + validate: /usr/sbin/sshd -t -f %s - name: "Algorithms | disable the ECDSA algorithm (deemed to be less safe)" - notify: Restart sshd + notify: "Restart ssh" ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^HostKey /etc/ssh/ssh_host_ecdsa_key' state: absent - validate: sshd -f %s -t + validate: /usr/sbin/sshd -t -f %s - name: "Algorithms | disable the DSA algorithm (considered to be defunct)" - notify: Restart sshd + notify: "Restart ssh" ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^HostKey /etc/ssh/ssh_host_dsa_key' state: absent - validate: sshd -f %s -t + validate: /usr/sbin/sshd -t -f %s diff --git a/roles/sshd/tasks/authentication.yml b/roles/sshd/tasks/authentication.yml index 8b4e63b..7bda62e 100644 --- a/roles/sshd/tasks/authentication.yml +++ b/roles/sshd/tasks/authentication.yml @@ -1,12 +1,15 @@ --- -- name: "Authentication | Configure SSH authentication settings" - notify: Restart sshd +- name: "Authentication | configure SSH authentication settings" + notify: "Restart ssh" loop: - { regexp: '^#?\s*PubkeyAuthentication\s+', line: 'PubkeyAuthentication yes' } - { regexp: '^#?\s*PasswordAuthentication\s+', line: 'PasswordAuthentication {{ sshd_password_auth | ternary("yes", "no") }}' } - { regexp: '^#?\s*PermitEmptyPasswords\s+', line: 'PermitEmptyPasswords no' } - { regexp: '^#?\s*ChallengeResponseAuthentication\s+', line: 'ChallengeResponseAuthentication {{ sshd_challenge_response_auth | ternary("yes", "no") }}' } - { regexp: '^#?\s*GSSAPIAuthentication\s+', line: 'GSSAPIAuthentication {{ sshd_gss_api_auth | ternary("yes", "no") }}' } + - { regexp: '^#?IgnoreRhosts', line: 'IgnoreRhosts yes' } + - { regexp: '^#?\s*KbdInteractiveAuthentication\s+', line: 'KbdInteractiveAuthentication no' } + - { regexp: '^#?\s*HostbasedAuthentication\s+', line: 'HostbasedAuthentication no' } - { regexp: '^#?\s*AuthenticationMethods\s+', line: "{{ 'AuthenticationMethods publickey password' if sshd_password_auth else 'AuthenticationMethods publickey' }}" @@ -17,14 +20,14 @@ line: "{{ item.line }}" validate: /usr/sbin/sshd -t -f %s -- name: "Check if there is an SSH config forced by cloud-init" +- name: "Authentication | 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 '{{ sshd_password_auth | ternary('yes', 'no') }}'" when: sshd_cloud_init.stat.exists - notify: Restart sshd + notify: "Restart ssh" ansible.builtin.lineinfile: path: "/etc/ssh/sshd_config.d/50-cloud-init.conf" regexp: '^#?PasswordAuthentication' diff --git a/roles/sshd/tasks/encryption.yml b/roles/sshd/tasks/encryption.yml index 660a369..d000a35 100644 --- a/roles/sshd/tasks/encryption.yml +++ b/roles/sshd/tasks/encryption.yml @@ -5,7 +5,7 @@ - /etc/ssh/ssh_host_ecdsa_key.pub - /etc/ssh/ssh_host_dsa_key - /etc/ssh/ssh_host_dsa_key.pub - notify: Restart sshd + notify: "Restart ssh" ansible.builtin.file: path: "{{ item }}" state: absent diff --git a/roles/sshd/tasks/install.yml b/roles/sshd/tasks/install.yml index ff03595..5d32274 100644 --- a/roles/sshd/tasks/install.yml +++ b/roles/sshd/tasks/install.yml @@ -1,11 +1,11 @@ --- -- name: "Install | install OpenSSH (RHEL flavours)" +- name: "Install | install OpenSSH server (RHEL flavours)" when: ansible_os_family == "RHEL" ansible.builtin.dnf: - name: openssh + name: openssh-server state: installed -- name: "Install | Install OpenSSH (Debian flavours)" +- name: "Install | install OpenSSH server (Debian flavours)" when: ansible_os_family == "Debian" ansible.builtin.apt: name: openssh-server diff --git a/roles/sshd/tasks/main.yml b/roles/sshd/tasks/main.yml index f567900..a24c5cb 100644 --- a/roles/sshd/tasks/main.yml +++ b/roles/sshd/tasks/main.yml @@ -18,9 +18,9 @@ ansible.builtin.include_tasks: "whitelists.yml" - name: "Log at VERBOSE level" - notify: Restart sshd + notify: "Restart ssh" ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^#?LogLevel' line: 'LogLevel VERBOSE' - validate: sshd -f %s -t + validate: /usr/sbin/sshd -t -f %s diff --git a/roles/sshd/tasks/restrictions.yml b/roles/sshd/tasks/restrictions.yml index 6d14920..655b77b 100644 --- a/roles/sshd/tasks/restrictions.yml +++ b/roles/sshd/tasks/restrictions.yml @@ -1,28 +1,42 @@ --- -- name: "Restrictions | Configure SSH security restrictions" +- name: "Restrictions | configure SSH security restrictions" loop: - { regexp: '^#?Protocol\s+', line: 'Protocol 2' } - - { regexp: '^PermitRootLogin yes', line: 'PermitRootLogin no' } + - { regexp: '^#?\s*PermitRootLogin\s+', line: 'PermitRootLogin no' } - { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' } - - { regexp: '^#?IgnoreRhosts', line: 'IgnoreRhosts yes' } - - { regexp: '^#?DebianBanner\s+', line: 'DebianBanner no' } - notify: Restart sshd + - { regexp: '^#?\s*PermitUserEnvironment\s+', line: 'PermitUserEnvironment no' } + - { regexp: '^#?\s*AllowAgentForwarding\s+', line: 'AllowAgentForwarding {{ sshd_allow_agent_forwarding | ternary("yes", "no") }}' } + - { regexp: '^#?\s*AllowTcpForwarding\s+', line: 'AllowTcpForwarding {{ sshd_allow_tcp_forwarding | ternary("yes", "no") }}' } + - { regexp: '^#?\s*GatewayPorts\s+', line: 'GatewayPorts {{ sshd_gateway_ports | ternary("yes", "no") }}' } + - { regexp: '^#?\s*PermitTunnel\s+', line: 'PermitTunnel {{ sshd_permit_tunnel | ternary("yes", "no") }}' } + - { regexp: '^#?\s*StrictModes\s+', line: 'StrictModes yes' } + - { regexp: '^#?\s*IgnoreUserKnownHosts\s+', line: 'IgnoreUserKnownHosts yes' } + notify: "Restart ssh" ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: "{{ item.regexp }}" line: "{{ item.line }}" validate: /usr/sbin/sshd -t -f %s +- name: "Restrictions | disable Debian banner" + when: "ansible_os_family == 'Debian'" + notify: "Restart ssh" + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?DebianBanner\s+' + line: "DebianBanner no" + validate: /usr/sbin/sshd -t -f %s + - name: "Restrictions | toggle PAM" - notify: Restart sshd + notify: "Restart ssh" ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^#?UsePAM' line: "UsePAM {{ sshd_disable_pam | ternary('no', 'yes') }}" - validate: sshd -f %s -t + validate: /usr/sbin/sshd -t -f %s - name: "Restrictions | ensure the SSHD config is restricted to the root user" - notify: Restart sshd + notify: "Restart ssh" ansible.builtin.file: path: /etc/ssh/sshd_config owner: root diff --git a/roles/sshd/tasks/whitelists.yml b/roles/sshd/tasks/whitelists.yml index 3952302..47e83d4 100644 --- a/roles/sshd/tasks/whitelists.yml +++ b/roles/sshd/tasks/whitelists.yml @@ -1,18 +1,18 @@ --- -- name: "Configure AllowUsers" +- name: "Whitelists | configure AllowUsers" when: sshd_allow_users is defined - notify: Restart sshd + notify: "Restart ssh" ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^#?\s*AllowUsers\s+' line: "AllowUsers {{ sshd_allow_users }}" - validate: sshd -f %s -t + validate: /usr/sbin/sshd -t -f %s -- name: "Configure AllowGroups" +- name: "Whitelists | configure AllowGroups" when: sshd_allow_groups is defined - notify: Restart sshd + notify: "Restart ssh" ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^#?\s*AllowGroups\s+' line: "AllowGroups {{ sshd_allow_groups }}" - validate: sshd -f %s -t + validate: /usr/sbin/sshd -t -f %s