How to harden SSH configs with Ansible on Linux!

  • by

In this tutorial, we’ll be going over how to harden your SSH client and server configs with Ansible on Linux. At ULayer, we use the dev-sec.ssh-hardening Ansible role to achieve this. It’s really easy to work with and it’s one of our most simple playbooks. With this Ansible role, you can deploy the same SSH configs on all of your servers. Our variables allow root logins but only via SSH keys. This tutorial assumes that you’re somewhat familiar with Ansible already.

To get started, you’ll need to have Ansible already installed. Once you get that settled run this command to install the role via Ansible Galaxy.

ansible-galaxy install dev-sec.ssh-hardening

Now create a directory and cd to it. We use a directory called ansible-ssh-hardening.

mkdir ansible-ssh-hardening; cd ansible-ssh-hardening

Now that you’re in the directory you need to create 2 files; playbook.yml & vars.yml. The playbook.yml should look something like this:

---

- name: Run the dev-sec.ssh-hardening role and include custom vars
  remote_user: root
  hosts: yourhosthere
  vars_files:
    - vars.yml
  roles:
    - dev-sec.ssh-hardening
  pre_tasks:
  - name: Generate ED25519 key if needed
    command : ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""
    args:
      creates: /etc/ssh/ssh_host_ed25519_key

You’ll want to edit the hosts: and include your actual host. You’ll want to add it in /etc/ansible/hosts (by default) if you haven’t already. You can also create your own Ansible inventory file and specify it with the -i flag of the ansible-playbook command.

We like to use ED25519 SSH host keys because of security concerns with NIST ECC curves. Because of this, we also ensure an ED25519 host key is generated with the pre_tasks, if it hasn’t been already. Next we’ll create the vars.yml file. Ours looks something like this:

# true if IPv6 is needed
network_ipv6_enable: false              # sshd + ssh

# true if sshd should be started and enabled
ssh_server_enabled: true               # sshd

# true if DNS resolutions are needed, look up the remote host name, defaults to false from 6.8, see: http://www.openssh.com/txt/release-6.8
ssh_use_dns: false                      # sshd

# true or value if compression is needed
ssh_compression: false                  # sshd

# For which components (client and server) to generate the configuration for. Can be useful when running against a client without an SSH server.
ssh_client_hardening: true          # ssh
ssh_server_hardening: true          # sshd

# If true, password login is allowed
ssh_client_password_login: true          # ssh
ssh_server_password_login: false          # sshd

# ports on which ssh-server should listen
ssh_server_ports: ['22']      # sshd

# port to which ssh-client should connect
ssh_client_port: '22'         # ssh

# one or more ip addresses, to which ssh-server should listen to. Default is empty, but should be configured for security reasons!
ssh_listen_to: ['0.0.0.0']    # sshd

# Host keys to look for when starting sshd.
ssh_host_key_files: ['/etc/ssh/ssh_host_ed25519_key', '/etc/ssh/ssh_host_rsa_key']  # sshd

# Specifies  the  maximum  number  of authentication attempts permitted per connection.  Once the number of failures reaches half this value, additional failures are logged.
ssh_max_auth_retries: 3

ssh_client_alive_interval: 0          # sshd
ssh_client_alive_count: 3               # sshd

# Allow SSH Tunnels
ssh_permit_tunnel: true

# Hosts with custom options.            # ssh
# Example:
# ssh_remote_hosts:
#   - names: ['example.com', 'example2.com']
#     options: ['Port 2222', 'ForwardAgent yes']
#   - names: ['example3.com']
#     options: ['StrictHostKeyChecking no']
ssh_remote_hosts: []

# Set this to "without-password" or "yes" to allow root to login
ssh_permit_root_login: 'without-password'          # sshd

# false to disable TCP Forwarding. Set to true to allow TCP Forwarding.
ssh_allow_tcp_forwarding: true         # sshd

# false to disable binding forwarded ports to non-loopback addresses. Set to true to force binding on wildcard address.
# Set to 'clientspecified' to allow the client to specify which address to bind to.
ssh_gateway_ports: false                # sshd

# false to disable Agent Forwarding. Set to true to allow Agent Forwarding.
ssh_allow_agent_forwarding: false       # sshd

# true if SSH has PAM support
ssh_pam_support: true

# false to disable pam authentication.
ssh_use_pam: true      # sshd

# false to disable google 2fa authentication
ssh_google_auth: false # sshd

# false to disable pam device 2FA input
ssh_pam_device: false # sshd

# true if SSH support GSSAPI
ssh_gssapi_support: false

# true if SSH support Kerberos
ssh_kerberos_support: true

# if specified, login is disallowed for user names that match one of the patterns.
ssh_deny_users: ''      # sshd

# if specified, login is allowed only for user names that match one of the patterns.
ssh_allow_users: ''      # sshd

# if specified, login is disallowed for users whose primary group or supplementary group list matches one of the patterns.
ssh_deny_groups: ''      # sshd

# if specified, login is allowed only for users whose primary group or supplementary group list matches one of the patterns.
ssh_allow_groups: ''      # sshd

# change default file that contains the public keys that can be used for user authentication.
ssh_authorized_keys_file: ''      # sshd

# specifies the file containing trusted certificate authorities public keys used to sign user certificates.
ssh_trusted_user_ca_keys_file: ''      # sshd

# set the trusted certificate authorities public keys used to sign user certificates.
# Example:
# ssh_trusted_user_ca_keys:
#   - 'ssh-rsa ... comment1'
#   - 'ssh-rsa ... comment2'
ssh_trusted_user_ca_keys: []      # sshd

# specifies the file containing principals that are allowed. Only used if ssh_trusted_user_ca_keys_file is set.
# Example:
# ssh_authorized_principals_file: '/etc/ssh/auth_principals/%u'
#
# %h is replaced by the home directory of the user being authenticated, and %u is
# replaced by the username of that user. After expansion, the path is taken to be
# an absolute path or one relative to the user's home directory.
#
ssh_authorized_principals_file: ''      # sshd

# list of hashes containing file paths and authorized principals. Only used if ssh_authorized_principals_file is set.
# Example:
# ssh_authorized_principals:
#   - { path: '/etc/ssh/auth_principals/root', principals: [ 'root' ], owner: "{{ ssh_owner }}", group: "{{ ssh_group }}", directoryowner: "{{ ssh_owner }}", directorygroup: "{{ ssh_group}}" }
#   - { path: '/etc/ssh/auth_principals/myuser', principals: [ 'masteradmin', 'webserver' ] }
ssh_authorized_principals: []      # sshd

# false to disable printing of the MOTD
ssh_print_motd: false      # sshd

# false to disable display of last login information
ssh_print_last_log: true    # sshd

# false to disable serving /etc/ssh/banner.txt before authentication is allowed
ssh_banner: false # sshd

# false to disable distribution version leakage during initial protocol handshake
ssh_print_debian_banner: false # sshd (Debian OS family only)

# true to enable sftp configuration
sftp_enabled: true

# false to disable sftp chroot
sftp_chroot: true

# change default sftp chroot location
sftp_chroot_dir: /home/%u

# enable experimental client roaming
ssh_client_roaming: false

# list of hashes (containing user and rules) to generate Match User blocks for.
ssh_server_match_user: false            # sshd

# list of hashes (containing group and rules) to generate Match Group blocks for.
ssh_server_match_group: false           # sshd

ssh_server_permit_environment_vars: false

# maximum number of concurrent unauthenticated connections to the SSH daemon
ssh_max_startups: '10:30:100'           # sshd

ssh_ps53: 'yes'
ssh_ps59: 'sandbox'

ssh_macs: []
ssh_ciphers: []
ssh_kex: []

ssh_macs_53_default:
  - hmac-ripemd160
  - hmac-sha1

ssh_macs_59_default:
  - hmac-sha2-512
  - hmac-sha2-256
  - hmac-ripemd160

ssh_macs_66_default:
  - hmac-sha2-512-etm@openssh.com
  - hmac-sha2-256-etm@openssh.com
  - umac-128-etm@openssh.com
  - hmac-sha2-512
  - hmac-sha2-256

ssh_macs_76_default:
  - hmac-sha2-512-etm@openssh.com
  - hmac-sha2-256-etm@openssh.com
  - umac-128-etm@openssh.com
  - hmac-sha2-512
  - hmac-sha2-256

ssh_ciphers_53_default:
  - aes256-ctr
  - aes192-ctr
  - aes128-ctr

ssh_ciphers_66_default:
  - chacha20-poly1305@openssh.com
  - aes256-gcm@openssh.com
  - aes128-gcm@openssh.com
  - aes256-ctr
  - aes192-ctr
  - aes128-ctr

ssh_kex_59_default:
  - diffie-hellman-group-exchange-sha256

ssh_kex_66_default:
  - curve25519-sha256@libssh.org
  - diffie-hellman-group-exchange-sha256

# directory where to store ssh_password policy
ssh_custom_selinux_dir: '/etc/selinux/local-policies'

sshd_moduli_file: '/etc/ssh/moduli'
sshd_moduli_minimum: 3072

# disable ChallengeResponseAuthentication
ssh_challengeresponseauthentication: false

# a list of public keys that are never accepted by the ssh server
ssh_server_revoked_keys: []

# Set to false to turn the role into a no-op. Useful when using
# the Ansible role dependency mechanism.
ssh_hardening_enabled: true

# Custom options for SSH client configuration file
ssh_custom_options: []

# Custom options for SSH daemon configuration file
sshd_custom_options: []

These variables are slightly modified from the original source. Our vars prefer the use of ED25519 host keys and KEX that we feel are safer.

Now that you’ve created all of the necessary files you can run the playbook.yml by executing this command:

ansible-playbook playbook.yml

If you configured everything correctly and have the correct hosts: in your playbook.yml it should run the role against your specified server. If you have your own inventory file you can specify it with ansible-playbook -i hosts-file playbook.yml for example. If anything fails, just read the error in the stderr output 🙂

If you need further assistance, we’d be happy to help out.

Just email us! -> support [@] ulayer.net

Leave a Reply

Your email address will not be published. Required fields are marked *