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:
sshd
listens on a non-default high port to mitigate exploit scans- By default all users, including newly added users are not able to authenticate via
ssh
. - Users must be explicitly granted authentication methods to allow them to login via
ssh
. - Password authentication is denied by default
- All users require MFA by default
- Connections from public ip space are denied for the
root
user - Public key authentication is enforced for the
root
user, and can be easily changed to MFA for added localnet security. - 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.
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/.
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).
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.
/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
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.