Merge pull request #99 from corvus-migratorius/sshd-further-hardening
sshd further hardening
This commit is contained in:
@@ -1,28 +1,76 @@
|
|||||||
sshd
|
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
|
Requirements
|
||||||
------------
|
------------
|
||||||
|
|
||||||
None
|
- Target hosts must be Debian-family or RedHat-family Linux systems
|
||||||
|
|
||||||
|
Supported Platforms
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- Debian
|
||||||
|
- Ubuntu
|
||||||
|
- RHEL 9+
|
||||||
|
- Rocky Linux 9+
|
||||||
|
|
||||||
Role Variables
|
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
|
Example Playbook
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
See: [converge.yml](molecule/default/converge.yml)
|
See: [converge.yml](molecule/default/converge.yml)
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
License
|
License
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|||||||
@@ -3,3 +3,7 @@ sshd_disable_pam: false
|
|||||||
sshd_password_auth: false
|
sshd_password_auth: false
|
||||||
sshd_challenge_response_auth: false
|
sshd_challenge_response_auth: false
|
||||||
sshd_gss_api_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
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
---
|
---
|
||||||
- name: "Restart sshd"
|
- name: "Restart the ssh service"
|
||||||
|
listen: "Restart ssh"
|
||||||
|
when: ansible_os_family == 'Debian'
|
||||||
ansible.builtin.service:
|
ansible.builtin.service:
|
||||||
name: ssh
|
name: ssh
|
||||||
state: restarted
|
state: restarted
|
||||||
enabled: true
|
enabled: true
|
||||||
daemon_reload: 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
|
||||||
|
|||||||
@@ -3,4 +3,9 @@
|
|||||||
hosts: all
|
hosts: all
|
||||||
roles:
|
roles:
|
||||||
- role: "genlab.common.sshd"
|
- 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"
|
sshd_allow_users: "testusr"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
# next task requires this directory to exist for sshd -t flag
|
# 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:
|
ansible.builtin.file:
|
||||||
path: /run/sshd
|
path: /run/sshd
|
||||||
state: directory
|
state: directory
|
||||||
@@ -10,33 +10,33 @@
|
|||||||
|
|
||||||
# NOTE: order of preference for openssh-server ed25519 -> rsa
|
# NOTE: order of preference for openssh-server ed25519 -> rsa
|
||||||
- name: "Algorithms | enable ed25519 authentication algorithm"
|
- name: "Algorithms | enable ed25519 authentication algorithm"
|
||||||
notify: Restart sshd
|
notify: "Restart ssh"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: /etc/ssh/sshd_config
|
path: /etc/ssh/sshd_config
|
||||||
regexp: '^HostKey /etc/ssh/ssh_host_ed25519_key'
|
regexp: '^HostKey /etc/ssh/ssh_host_ed25519_key'
|
||||||
line: '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"
|
- name: "Algorithms | enable the RSA authentication algorithm"
|
||||||
notify: Restart sshd
|
notify: "Restart ssh"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: /etc/ssh/sshd_config
|
path: /etc/ssh/sshd_config
|
||||||
regexp: '^HostKey /etc/ssh/ssh_host_rsa_key'
|
regexp: '^HostKey /etc/ssh/ssh_host_rsa_key'
|
||||||
line: '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)"
|
- name: "Algorithms | disable the ECDSA algorithm (deemed to be less safe)"
|
||||||
notify: Restart sshd
|
notify: "Restart ssh"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: /etc/ssh/sshd_config
|
path: /etc/ssh/sshd_config
|
||||||
regexp: '^HostKey /etc/ssh/ssh_host_ecdsa_key'
|
regexp: '^HostKey /etc/ssh/ssh_host_ecdsa_key'
|
||||||
state: absent
|
state: absent
|
||||||
validate: sshd -f %s -t
|
validate: /usr/sbin/sshd -t -f %s
|
||||||
|
|
||||||
- name: "Algorithms | disable the DSA algorithm (considered to be defunct)"
|
- name: "Algorithms | disable the DSA algorithm (considered to be defunct)"
|
||||||
notify: Restart sshd
|
notify: "Restart ssh"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: /etc/ssh/sshd_config
|
path: /etc/ssh/sshd_config
|
||||||
regexp: '^HostKey /etc/ssh/ssh_host_dsa_key'
|
regexp: '^HostKey /etc/ssh/ssh_host_dsa_key'
|
||||||
state: absent
|
state: absent
|
||||||
validate: sshd -f %s -t
|
validate: /usr/sbin/sshd -t -f %s
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
---
|
---
|
||||||
- name: "Authentication | Configure SSH authentication settings"
|
- name: "Authentication | configure SSH authentication settings"
|
||||||
notify: Restart sshd
|
notify: "Restart ssh"
|
||||||
loop:
|
loop:
|
||||||
- { regexp: '^#?\s*PubkeyAuthentication\s+', line: 'PubkeyAuthentication yes' }
|
- { regexp: '^#?\s*PubkeyAuthentication\s+', line: 'PubkeyAuthentication yes' }
|
||||||
- { regexp: '^#?\s*PasswordAuthentication\s+', line: 'PasswordAuthentication {{ sshd_password_auth | ternary("yes", "no") }}' }
|
- { regexp: '^#?\s*PasswordAuthentication\s+', line: 'PasswordAuthentication {{ sshd_password_auth | ternary("yes", "no") }}' }
|
||||||
- { regexp: '^#?\s*PermitEmptyPasswords\s+', line: 'PermitEmptyPasswords no' }
|
- { regexp: '^#?\s*PermitEmptyPasswords\s+', line: 'PermitEmptyPasswords no' }
|
||||||
- { regexp: '^#?\s*ChallengeResponseAuthentication\s+', line: 'ChallengeResponseAuthentication {{ sshd_challenge_response_auth | ternary("yes", "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: '^#?\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+',
|
regexp: '^#?\s*AuthenticationMethods\s+',
|
||||||
line: "{{ 'AuthenticationMethods publickey password' if sshd_password_auth else 'AuthenticationMethods publickey' }}"
|
line: "{{ 'AuthenticationMethods publickey password' if sshd_password_auth else 'AuthenticationMethods publickey' }}"
|
||||||
@@ -17,14 +20,14 @@
|
|||||||
line: "{{ item.line }}"
|
line: "{{ item.line }}"
|
||||||
validate: /usr/sbin/sshd -t -f %s
|
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
|
register: sshd_cloud_init
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "/etc/ssh/sshd_config.d/50-cloud-init.conf"
|
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') }}'"
|
- name: "Authentication | override password authentication by cloud-init to '{{ sshd_password_auth | ternary('yes', 'no') }}'"
|
||||||
when: sshd_cloud_init.stat.exists
|
when: sshd_cloud_init.stat.exists
|
||||||
notify: Restart sshd
|
notify: "Restart ssh"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: "/etc/ssh/sshd_config.d/50-cloud-init.conf"
|
path: "/etc/ssh/sshd_config.d/50-cloud-init.conf"
|
||||||
regexp: '^#?PasswordAuthentication'
|
regexp: '^#?PasswordAuthentication'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
- /etc/ssh/ssh_host_ecdsa_key.pub
|
- /etc/ssh/ssh_host_ecdsa_key.pub
|
||||||
- /etc/ssh/ssh_host_dsa_key
|
- /etc/ssh/ssh_host_dsa_key
|
||||||
- /etc/ssh/ssh_host_dsa_key.pub
|
- /etc/ssh/ssh_host_dsa_key.pub
|
||||||
notify: Restart sshd
|
notify: "Restart ssh"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ item }}"
|
path: "{{ item }}"
|
||||||
state: absent
|
state: absent
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
- name: "Install | install OpenSSH (RHEL flavours)"
|
- name: "Install | install OpenSSH server (RHEL flavours)"
|
||||||
when: ansible_os_family == "RHEL"
|
when: ansible_os_family == "RHEL"
|
||||||
ansible.builtin.dnf:
|
ansible.builtin.dnf:
|
||||||
name: openssh
|
name: openssh-server
|
||||||
state: installed
|
state: installed
|
||||||
|
|
||||||
- name: "Install | Install OpenSSH (Debian flavours)"
|
- name: "Install | install OpenSSH server (Debian flavours)"
|
||||||
when: ansible_os_family == "Debian"
|
when: ansible_os_family == "Debian"
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
name: openssh-server
|
name: openssh-server
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
ansible.builtin.include_tasks: "whitelists.yml"
|
ansible.builtin.include_tasks: "whitelists.yml"
|
||||||
|
|
||||||
- name: "Log at VERBOSE level"
|
- name: "Log at VERBOSE level"
|
||||||
notify: Restart sshd
|
notify: "Restart ssh"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: /etc/ssh/sshd_config
|
path: /etc/ssh/sshd_config
|
||||||
regexp: '^#?LogLevel'
|
regexp: '^#?LogLevel'
|
||||||
line: 'LogLevel VERBOSE'
|
line: 'LogLevel VERBOSE'
|
||||||
validate: sshd -f %s -t
|
validate: /usr/sbin/sshd -t -f %s
|
||||||
|
|||||||
@@ -1,28 +1,42 @@
|
|||||||
---
|
---
|
||||||
- name: "Restrictions | Configure SSH security restrictions"
|
- name: "Restrictions | configure SSH security restrictions"
|
||||||
loop:
|
loop:
|
||||||
- { regexp: '^#?Protocol\s+', line: 'Protocol 2' }
|
- { 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: '^#?X11Forwarding', line: 'X11Forwarding no' }
|
||||||
- { regexp: '^#?IgnoreRhosts', line: 'IgnoreRhosts yes' }
|
- { regexp: '^#?\s*PermitUserEnvironment\s+', line: 'PermitUserEnvironment no' }
|
||||||
- { regexp: '^#?DebianBanner\s+', line: 'DebianBanner no' }
|
- { regexp: '^#?\s*AllowAgentForwarding\s+', line: 'AllowAgentForwarding {{ sshd_allow_agent_forwarding | ternary("yes", "no") }}' }
|
||||||
notify: Restart sshd
|
- { 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:
|
ansible.builtin.lineinfile:
|
||||||
path: /etc/ssh/sshd_config
|
path: /etc/ssh/sshd_config
|
||||||
regexp: "{{ item.regexp }}"
|
regexp: "{{ item.regexp }}"
|
||||||
line: "{{ item.line }}"
|
line: "{{ item.line }}"
|
||||||
validate: /usr/sbin/sshd -t -f %s
|
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"
|
- name: "Restrictions | toggle PAM"
|
||||||
notify: Restart sshd
|
notify: "Restart ssh"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: /etc/ssh/sshd_config
|
path: /etc/ssh/sshd_config
|
||||||
regexp: '^#?UsePAM'
|
regexp: '^#?UsePAM'
|
||||||
line: "UsePAM {{ sshd_disable_pam | ternary('no', 'yes') }}"
|
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"
|
- name: "Restrictions | ensure the SSHD config is restricted to the root user"
|
||||||
notify: Restart sshd
|
notify: "Restart ssh"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: /etc/ssh/sshd_config
|
path: /etc/ssh/sshd_config
|
||||||
owner: root
|
owner: root
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
---
|
---
|
||||||
- name: "Configure AllowUsers"
|
- name: "Whitelists | configure AllowUsers"
|
||||||
when: sshd_allow_users is defined
|
when: sshd_allow_users is defined
|
||||||
notify: Restart sshd
|
notify: "Restart ssh"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: /etc/ssh/sshd_config
|
path: /etc/ssh/sshd_config
|
||||||
regexp: '^#?\s*AllowUsers\s+'
|
regexp: '^#?\s*AllowUsers\s+'
|
||||||
line: "AllowUsers {{ sshd_allow_users }}"
|
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
|
when: sshd_allow_groups is defined
|
||||||
notify: Restart sshd
|
notify: "Restart ssh"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: /etc/ssh/sshd_config
|
path: /etc/ssh/sshd_config
|
||||||
regexp: '^#?\s*AllowGroups\s+'
|
regexp: '^#?\s*AllowGroups\s+'
|
||||||
line: "AllowGroups {{ sshd_allow_groups }}"
|
line: "AllowGroups {{ sshd_allow_groups }}"
|
||||||
validate: sshd -f %s -t
|
validate: /usr/sbin/sshd -t -f %s
|
||||||
|
|||||||
Reference in New Issue
Block a user