Secure Shell Hardening Beyond SSH Config
SSH configuration (/etc/ssh/sshd_config) is the first step. key-only auth, no root login, specific ciphers. But the session that follows SSH authentication has its own attack surface. This guide covers what happens after the connection is established: PAM controls, sudo hardening, session logging, and reducing SSH exposure entirely.
PAM - Restrict Who Can Log In
PAM (Pluggable Authentication Modules) controls authentication and session setup.
/etc/security/access.conf. allow only specific users/groups
Format - permission : user/group : origin
Deny all except members of the sshusers group
/etc/pam.d/sshd. add this line near the top:
account required pam_access.so
/etc/security/access.conf
Allow members of sshusers from anywhere
+ : (sshusers) : ALL
Allow specific user from specific IP range
+ : alice : 10.0.0.0/24
Deny everyone else
- : ALL : ALL
Create the sshusers group and add users
sudo groupadd sshusers
sudo usermod -aG sshusers alice
sudo usermod -aG sshusers bob
Test PAM access (does not actually log in)
sudo pamtester login alice authenticate
Time-Based Login Restrictions
Limit SSH access to business hours using PAM time:
/etc/security/time.conf
Format - service;tty;users;time
Deny SSH for contractor account on weekends
sshd;*;contractor;!Sa-Su0000-2400
sshd;*;contractor;Wk0800-1800 # weekdays 8am-6pm only
/etc/pam.d/sshd. add:
account required pam_time.so
Session Timeouts and Idle Lockout
/etc/profile.d/autologout.sh. set for all users
readonly TMOUT=600 # 10-minute idle timeout, disconnect
export TMOUT
For SSH specifically (belt-and-suspenders with sshd_config):
ClientAliveInterval 300
ClientAliveCountMax 2
These are in sshd_config, not profile. but document both
Lock screen on inactivity using vlock (for console sessions)
sudo apt install vlock
Users can run - vlock -a to lock all virtual consoles
For remote sessions, TMOUT in /etc/profile.d/ is the primary control
sudo Hardening
/etc/sudoers.d/hardened. do not edit /etc/sudoers directly
Always - sudo visudo -f /etc/sudoers.d/hardened
Require password every time (disable sudo timestamp caching)
Defaults timestamp_timeout=0
Log all sudo commands (default) and log to both syslog and a file
Defaults logfile="/var/log/sudo.log"
Defaults log_input, log_output
Defaults iolog_dir="/var/log/sudo-io/%{user}"
Require full path for commands
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Prevent sudo from running interactive shells (su escalation)
Defaults !visiblepw
Defaults always_set_home
Defaults env_reset
Per-user - allow specific commands only, no shell
alice ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx, /usr/bin/systemctl reload nginx
bob ALL=(ALL) PASSWD: /usr/bin/apt-get update, /usr/bin/apt-get upgrade
Test sudo policy
sudo -l -U alice # list what alice can do
sudo -l # list what you can do
Review sudo logs
sudo journalctl -u sudo
tail -f /var/log/sudo.log
Login Banner and Legal Warning
A proper login banner establishes that unauthorized access is prohibited. required for many compliance frameworks.
/etc/issue.net. displayed before SSH login
sudo tee /etc/issue.net << 'EOF'
*
AUTHORIZED USE ONLY
Unauthorized access to this system is prohibited. All activity is monitored
and logged. Disconnect immediately if you do not have authorization.
By continuing, you consent to monitoring in accordance with company policy.
*
EOF
Enable in sshd_config:
Banner /etc/issue.net
Local console banner (shown before login prompt)
/etc/issue. same content
sudo cp /etc/issue.net /etc/issue
SSH Session Logging with script
Every command run in a SSH session can be recorded:
/etc/profile.d/session-log.sh
Logs all sessions to /var/log/sessions/
LOGDIR="/var/log/sessions"
mkdir -p "$LOGDIR"
if [ -n "$SSH_CONNECTION" ] && [ -z "$SCRIPT" ]; then
LOGFILE="${LOGDIR}/$(date +%Y%m%d-%H%M%S)-$(whoami)-$(echo $SSH_CONNECTION | awk '{print $1}').log"
export SCRIPT="$LOGFILE"
exec script -qf "$LOGFILE"
fi
This records a full typescript of every SSH session. Combine with logrotate:
/etc/logrotate.d/sessions
/var/log/sessions/*.log {
weekly
rotate 12
compress
delaycompress
missingok
notifempty
}
fail2ban: Beyond Default SSH Protection
The default fail2ban SSH jail bans after 5 failures. Tighten it:
/etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600 # 1 hour (default is 600 seconds)
findtime = 600
maxretry = 3 # reduced from 5
Permanent ban after repeated banning
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
maxretry = 3
findtime = 86400 # 24 hours
bantime = 604800 # 7 days
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
Block scan tools that send malformed SSH packets
[sshd-ddos]
enabled = true
port = ssh
filter = sshd-ddos
logpath = /var/log/auth.log
maxretry = 2
bantime = 7200
sudo systemctl restart fail2ban
Check current bans
sudo fail2ban-client status sshd
sudo fail2ban-client status recidive
Manually unban an IP
sudo fail2ban-client set sshd unbanip 1.2.3.4
Port Knocking - Hide SSH from Scanners
Port knocking keeps port 22 closed until a correct sequence of connection attempts unlocks it. Effective against automated scanners.
Install knockd
sudo apt install knockd
/etc/knockd.conf
[options]
UseSyslog
[openSSH]
sequence = 7000,8000,9000
seq_timeout = 5
command = /sbin/iptables -I INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
tcpflags = syn
[closeSSH]
sequence = 9000,8000,7000
seq_timeout = 5
command = /sbin/iptables -D INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
tcpflags = syn
Default block SSH in iptables
sudo iptables -A INPUT -p tcp --dport 22 -j DROP
sudo systemctl enable --now knockd
To connect from a remote machine:
knock your-server 7000 8000 9000
ssh user@your-server
knock your-server 9000 8000 7000 # close the port again
Auditing Current Login Activity
Who is logged in right now
who
w
Recent successful logins
last -n 20
Recent failed logins
lastb -n 20
All authentication events
sudo journalctl -u ssh --since "24 hours ago" | grep -E "(Accepted|Failed|Invalid)"
IP addresses that attempted login
sudo grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -20
Related Articles