Skip to content

Instantly share code, notes, and snippets.

@kyle0r
Last active September 7, 2023 21:30
Show Gist options
  • Save kyle0r/eb6b9e16ad6366ffa9692169906f128a to your computer and use it in GitHub Desktop.
Save kyle0r/eb6b9e16ad6366ffa9692169906f128a to your computer and use it in GitHub Desktop.
Notes around secure defaults for Debian sshd_config and MFA

Disclaimer: Your mileage may vary. Rigorous testing (e.g. pen-testing) is recommended to validate that your config behaves according to your use cases, that it is secure, locked down and not exploitable.

The following configs provide a "secure by default" configuration for sshd and enforces MFA authentication from public ip space.

A screencast walking-through and demonstrating the configuration has been posted on YouTube here: https://youtu.be/m_MCVm79xyY

In theory, the strategy/concept and configuration should work on most distros running sshd. The screencast was recorded on an instance of Debian 12 aka bookworm. OpenSSH_9.2, OpenSSL 3.0.9.

If you'd like advice or consultation on these topics, feel free to reach out or leave a comment. Ditto if you have critique or suggestions :)

The configuration strategy aims to mitigate various attacks and exploits, disables password auth, and forces users to use MFA. The implementation is roughly as follows:

  1. sshd listens on a non-default high port to mitigate exploit scans
  2. By default all users, including newly added users are not able to authenticate via ssh.
  3. Users must be explicitly granted authentication methods to allow them to login via ssh.
  4. Password authentication is denied by default
  5. All users require MFA by default
  6. Connections from public ip space are denied for the root user
  7. Public key authentication is enforced for the root user, and can be easily changed to MFA for added localnet security.
  8. A pam module is used for, and enforces MFA by default

This configuration means all current and future user accounts must be explicitly granted authentication methods to login via ssh. MFA is enforced. The system should be relatively well hardened from present and future users with single factor and/or weak credentials.

Additional recommendations for added sshd security

Filter ingress traffic to your sshd listening port

Something else I highly recommend is to configure your firewall to DROP incoming traffic to your configured sshd port and whitelist CIDR's from your trusted ISP(s)/VPN(s) and optionally a few other major ISP's in your country. This will give you a fallback if your primary trusted network(s) go down - you can use a mobile hotspot or visit a local coffee shop and connect (assuming they are using one of the major ISP's you have whitelisted).

Personally I whitelist two major ISP CIDR's in my country (one of which is my cable ISP, and one of which is my mobile carrier), their connections are ubiquitous and I can easily find a way to connect to my hosts from their networks.

With this approach, you'll have multiple ingress networks, but the vast majority of ingress traffic from the public IP space will be filtered and never reach your sshd process. This, combined with using a random high listening port for sshd, is a proven method of significantly reducing exploit scanning traffic on your sshd port, i.e. reducing your attack surface.

iptables supports "ipset" and nftables supports "sets". These capabilities make it easy to whitelist/blacklist large lists of networks. There is a useful tool called iprange which supports merging adjacent ranges or eliminating overlapped ranges, which is especially helpful in optimising downloaded CIDR ranges from data sources like https://ipinfo.io/.

Install and configure fail2ban

You can also install and configure the fail2ban package on your system to ban failed ssh attempts to your node/instance/server. fail2ban works by "banning x failed attempts" for a given service/daemon.
It creates firewall "jails" for the offending remote ips, which match bad patterns in the system logs/journal. fail2ban is great for mitigating common sshd exploits, including password brute-force attempts, and also logs exploit attempts again your node/system/server, aka forms part of your IDS/IPS (Intrusion Detection/Prevention System).

VPN/VPC and bastion setups

The config/approaches covered herein are aimed at a node/instance/server that is exposed to a certain range of public ip space. Naturally that comes with some drawbacks because it leaves the running sshd exposed to a certain degree.

Stringent InfoSec and data security standards do not permit infrastructure to be exposed to public ip space and mandate higher levels of security. For example, network segmentation between untrusted (public) and trusted (private) networks. This segmentation is often achieved by implementing perimeter firewall(s) and VPN's which require AuthN and AuthZ in order to access untrusted infrastructure (often referred to as a DMZ), before allowing connections into the trusted/private network(s). For example an cloud VPC (Virtual Private Network).

A common pattern is to establish a bastion (aka jump-host or golden-host) accessible via VPN or public ip space, and from this host/network a user can access the sensitive private/trusted networks. The bastion pattern can also be implemented as a fault tolerant cluster. i.e. multi-zone and/or multi-region.

The config/approaches covered herein are not a replacement for implementing network segmentation, perimeter firewall(s), and the bastion pattern, however the approach can be used to harden (and compliment) the security configuration of the bastion host(s).

I hope the content proves insightful and useful.

Files overview

/etc/ssh/sshd_config is untouched (sans comments) and comes with the distributions openssh-server package.
Note that Debian uses the non-standard Include /etc/ssh/sshd_config.d/*.conf directive to include local sshd configuration, which overrides /etc/ssh/sshd_config and its defaults.

/etc/ssh/sshd_config.d/local.conf is the local customised sshd configuration file.

/etc/pam.d/sshd is the modified pam configuration to enforce MFA

Footnotes

A note on the pam_google_authenticator.so module, I've linked to the git repo, AFAIK this is just a straightforward OTP/MFA module that operates offline and doesn't phone home.

# non-standard port to mitigate exploit scanners traffic
Port 22350
# ensure Pubkey auth is allowed
PubkeyAuthentication yes
# the following directives ensure password and kbdinterative auth are denied by default
PasswordAuthentication no
PermitEmptyPasswords no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
# best practice default
PermitRootLogin no
# Relax security for localnet.
#+ Allow root and specific users single-factor pubkey authentication.
#+ These directives removes the MFA requirement for the given "Users"
Match Address 192.168.170.0/24 User root,op,op-backup
PermitRootLogin prohibit-password
AuthenticationMethods publickey
# Require pubkey+kbdinterative, this "Match *" overrides the defualt "any" and ensures multiple auth methods are
#+ required by default, this ensures single-factor authentication is denied by default.
#+ i.e. MFA aka multi-factor authentication is enforced by default.
#+ Due to "KbdInteractiveAuthentication no" being set, this "Match" directive actually disables any ssh login
#+ unless the "User" has been explicity granted "KbdInteractiveAuthentication yes" by a "Match" directive.
#+ If a "User" is not explicity granted "KbdInteractiveAuthentication yes" sshd will throw an error:
#+ error: No AuthenticationMethods left after eliminating disabled methods
#+ The strategy is to disable ssh logins on any "User" by default, and to enforce MFA by default.
#+ e.g. should a user be added to the system, ssh auth will not be possible without MFA, AND an explicit grant
#+ in the sshd_config.
Match Address *
AuthenticationMethods publickey,keyboard-interactive
# for non localnet, and for specific users, allow kbdinterative auth
Match Address *,!192.168.170.0/24 User op,op-backup
KbdInteractiveAuthentication yes
# PAM configuration for the Secure Shell service
# Standard Un*x authentication.
#+ When using OTP/MFA e.g. Google Authenticator, comment out common-auth to disable standard Un*x auth.
#+ If both common-auth and OTP/MFA auth are both commented out, sshd still prompts for password auth.
#+ This is because if there is no "auth" directive, then PAM falls back to the common-auth specified
#+ in /etc/pam.d/other
#@include common-auth
# Disallow non-root logins when /etc/nologin exists.
account required pam_nologin.so
# Uncomment and edit /etc/security/access.conf if you need to set complex
# access limits that are hard to express in sshd_config.
# account required pam_access.so
# Standard Un*x authorization.
@include common-account
# SELinux needs to be the first session rule. This ensures that any
# lingering context has been cleared. Without this it is possible that a
# module could execute code in the wrong domain.
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so close
# Set the loginuid process attribute.
session required pam_loginuid.so
# Create a new session keyring.
session optional pam_keyinit.so force revoke
# Standard Un*x session setup and teardown.
@include common-session
# Print the message of the day upon successful login.
# This includes a dynamically generated part from /run/motd.dynamic
# and a static (admin-editable) part from /etc/motd.
session optional pam_motd.so motd=/run/motd.dynamic
session optional pam_motd.so noupdate
# Print the status of the user's mailbox upon successful login.
session optional pam_mail.so standard noenv # [1]
# Set up user limits from /etc/security/limits.conf.
session required pam_limits.so
# Read environment variables from /etc/environment and
# /etc/security/pam_env.conf.
session required pam_env.so # [1]
# In Debian 4.0 (etch), locale-related environment variables were moved to
# /etc/default/locale, so read that as well.
session required pam_env.so user_readenv=1 envfile=/etc/default/locale
# SELinux needs to intervene at login time to ensure that the process starts
# in the proper default security context. Only sessions which are intended
# to run in the user's context should be run after this.
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so open
# Standard Un*x password updating.
@include common-password
# Google Authenticator
#+ https://github.com/google/google-authenticator-libpam
#+ This module is used in conjunction with sshd directive "KbdInteractiveAuthentication yes".
#+ When an account is configured by sshd_config to require kbdinterative authentication, it will be
#+ prompted for a validation code during login.
#+ Appending nullok causes this module to return IGNORE, IF the user does not have secret configured.
#+ The consequence is that authentication is deferred to other pam modules and the sshd cfg.
#+ Omitting nullok causes the mechanism to prompt for a validation code regardless, which will fail for accounts
#+ without secrets, and fail authentication.
auth required pam_google_authenticator.so
# This is the sshd server system-wide configuration file. See
# sshd_config(5) for more information.
# This sshd was compiled with PATH=/usr/local/bin:/usr/bin:/bin:/usr/games
# The strategy used for options in the default sshd_config shipped with
# OpenSSH is to specify options with their default value where
# possible, but leave them commented. Uncommented options override the
# default value.
# The first encountered configuration is applied, so directives in the sshd_config.d files are applied first.
# It is possible to output the interpreted cfg by running sshd -T
Include /etc/ssh/sshd_config.d/*.conf
# file continues with distribution defaults
# ...
# ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment