Hình dung bạn vừa thuê một VPS để chạy side project, chưa kịp cài gì ngoài SSH thì vài phút sau mở journalctl -u ssh đã thấy hàng trăm dòng “Failed password for root from …”, IP từ khắp nơi, toàn bot quét Internet tự động. Nhiều người ban đầu chỉ đổi password cho khó hơn rồi yên tâm. Mấy tháng sau đọc lại log mới thấy: mỗi ngày có hàng nghìn lần thử login, từ hàng trăm IP khác nhau. Password dù mạnh đến đâu cũng chỉ là một lớp phòng thủ, và nếu lỡ dùng password yếu ở đâu đó hoặc bị lộ qua breach khác thì chỉ là vấn đề thời gian.
Mặc định của OpenSSH đã khá an toàn, nhưng “khá” không có nghĩa “đủ” cho một server có IP public. Bài này tổng hợp checklist cần áp dụng cho mọi server Linux mới, kèm lý do phía sau mỗi bước để bạn hiểu cái nào thực sự quan trọng, cái nào chỉ là “thuốc an thần” cho yên tâm.
Hiểu cách SSH xác thực trước khi cứng hoá
Trước khi chỉnh bất cứ gì, cần hiểu SSH xác thực user như thế nào, vì hardening xoay quanh việc chỉ cho phép phương pháp mạnh và giảm bề mặt tấn công.
OpenSSH thử lần lượt các phương pháp theo thứ tự cấu hình. Phổ biến nhất là publickey, client ký một challenge bằng private key, server verify bằng public key đã lưu trong authorized_keys. Tiếp theo là password, client gửi password qua kênh đã mã hoá, server verify với /etc/shadow. Và cuối cùng là keyboard-interactive, qua PAM, có thể là OTP hoặc 2FA.
Password authentication là thứ mà bot khai thác, chúng thử hàng nghìn password phổ biến mỗi giờ. Publickey thì bot không làm gì được vì không có private key của bạn. Vậy nên bước đầu tiên và quan trọng nhất luôn là: chuyển sang key-based authentication rồi tắt password.
Trước khi bắt đầu: đừng tự khoá mình ra
Đây là sai lầm kinh điển: đổi cấu hình SSH, restart service, rồi mất kết nối vì chính bạn bị chặn. Quy trình an toàn rất đơn giản nhưng cực kỳ quan trọng.
Giữ một session SSH đang mở trong suốt quá trình chỉnh sửa. Đây là dây cứu sinh, nếu config mới sai, session cũ vẫn sống và bạn có thể sửa lại. Trước khi reload, luôn chạy syntax check:
sudo sshd -t # kiểm tra syntax
sudo sshd -T # in ra cấu hình effective để verify
Sau khi reload, mở một terminal mới để thử login, đừng đóng terminal cũ cho đến khi chắc chắn login được bình thường. Nếu ở cloud thì snapshot disk trước khi chỉnh cũng là một lớp bảo vệ thêm. VPS provider thường có console serial, đó là phao cuối nếu mọi thứ hỏng.
Nghe thì hiển nhiên nhưng không ít người lockout bản thân khỏi production server vì quên bước test trước khi reload. Sửa bằng serial console lúc nửa đêm không phải trải nghiệm ai muốn lặp lại.
Key-based authentication
Tạo key đúng cách
Ed25519 là lựa chọn mặc định nên dùng cho mọi key mới, ngắn gọn, nhanh, và an toàn hơn RSA ở key size nhỏ hơn nhiều:
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/id_ed25519_prod -C "me@laptop"
Tham số -a 100 tăng số vòng KDF khi mã hoá key bằng passphrase, nếu ai đó lấy được file key, brute force passphrase sẽ chậm hơn nhiều. Và luôn đặt passphrase, key không có passphrase nghĩa là mất laptop là mất quyền truy cập server, không cần bước nào thêm.
Hay gặp tình huống dev tạo key không passphrase “cho tiện” rồi commit cả private key vào Git repo (đúng, chuyện này xảy ra thật). Passphrase là lớp bảo vệ cuối cùng cho tình huống key bị lộ.
Đưa key lên server
Cách nhanh nhất và ít sai nhất:
ssh-copy-id -i ~/.ssh/id_ed25519_prod.pub user@server
Nếu cần làm thủ công, append public key vào /home/user/.ssh/authorized_keys, thì phải set permission chặt:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chown -R user:user ~/.ssh
SSH sẽ từ chối đọc authorized_keys nếu permission quá lỏng, đây là bảo vệ có chủ đích, không phải bug. Hay gặp tình huống debug “tại sao key đúng mà vẫn bị hỏi password”, hoá ra authorized_keys có permission 644 thay vì 600.
ssh-agent và vấn đề agent forwarding
ssh-agent giữ private key đã unlock trong memory để bạn không phải nhập passphrase mỗi lần SSH. Rất tiện, nên dùng.
Nhưng agent forwarding (ssh -A) thì cần cẩn thận. Khi bạn forward agent sang server đích, bất kỳ ai có quyền root trên server đó có thể dùng agent socket để ký, tương đương có private key của bạn trong thời gian session còn sống. Nếu server đó bị compromise, attacker có thể dùng agent của bạn để nhảy sang server khác.
Thay vì agent forwarding, nên dùng ProxyJump, an toàn hơn vì key không rời máy local:
# ~/.ssh/config
Host app-*.prod
User deploy
ProxyJump bastion.prod.example.com
Khi SSH tới app-web.prod, client tự tunnel qua bastion mà private key chỉ ở máy bạn. Bastion không bao giờ thấy key.
.ssh/config, alias cho server
File config phía client giúp cuộc sống dễ thở hơn nhiều, đặc biệt khi quản lý nhiều server:
Host prod-web
HostName web1.prod.example.com
User deploy
IdentityFile ~/.ssh/id_ed25519_prod
IdentitiesOnly yes
AddKeysToAgent yes
ServerAliveInterval 60
IdentitiesOnly yes là setting nên luôn bật, nó ngăn SSH gửi tất cả key trong agent lên server. Không bật thì mỗi lần connect, SSH thử lần lượt mọi key, vừa chậm vừa để server biết bạn có bao nhiêu key (fingerprint tracking). ServerAliveInterval 60 gửi keepalive mỗi 60 giây để connection không bị NAT timeout drop.
Jump host / Bastion
Thay vì mở SSH port ra Internet cho mọi server, nên đặt một bastion host ở ngoài, đây là server duy nhất có SSH port accessible từ Internet. Các server nội bộ chỉ cho phép SSH từ bastion qua private network.
Internet → [Bastion :22] → private network → [App servers]
Lợi ích rất rõ: bề mặt tấn công SSH từ Internet thu nhỏ về đúng một host. Bạn chỉ cần hardening cực kỹ host đó, monitor log SSH ở một chỗ, và nếu cần block toàn bộ SSH access thì chỉ cần chặn ở bastion. Các app server phía sau có thể thậm chí không có public IP.
Cấu hình sshd_config mẫu
Đây là file cấu hình dùng cho mọi server production mới. Có thể đặt ở /etc/ssh/sshd_config hoặc tách riêng vào /etc/ssh/sshd_config.d/hardening.conf:
# Authentication
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
PermitEmptyPasswords no
AuthenticationMethods publickey
# User control
AllowGroups sshusers
MaxAuthTries 3
LoginGraceTime 30
# Network
Port 22
AddressFamily inet
ListenAddress 0.0.0.0
# Session
ClientAliveInterval 300
ClientAliveCountMax 2
MaxSessions 10
MaxStartups 10:30:60
X11Forwarding no
AllowTcpForwarding yes
PermitTunnel no
# Logging
LogLevel VERBOSE
Sau đây là logic đằng sau mỗi block quan trọng.
PermitRootLogin no, mọi attacker đều biết user root tồn tại, nên đó là target đầu tiên của brute force. Tắt root login buộc phải dùng user thường rồi sudo, thêm một lớp attacker phải vượt qua: đoán đúng username trước.
PasswordAuthentication no, đây là thay đổi quan trọng nhất. Một khi bạn đã setup key-based auth và xác nhận login bằng key hoạt động, tắt password authentication khiến toàn bộ brute force bot trở nên vô nghĩa, chúng không có private key của bạn, dù thử bao nhiêu password cũng không vào được.
AllowGroups sshusers, chỉ user thuộc group sshusers mới SSH được. Điều này ngăn các account hệ thống (postgres, www-data, mysql) bị dùng để login dù có ai vô tình thêm key cho chúng. Tạo group và thêm user hợp pháp:
sudo groupadd sshusers
sudo usermod -aG sshusers deploy
MaxAuthTries 3 và LoginGraceTime 30, giới hạn số lần thử trong một connection và thời gian phải hoàn thành login. Giảm hiệu quả của slow brute force attacks.
LogLevel VERBOSE, log cả fingerprint của key dùng để login. Khi điều tra incident, biết chính xác key nào được dùng là rất giá trị, đặc biệt khi một user có nhiều key trên nhiều thiết bị.
MaxStartups 10:30:60, giới hạn concurrent unauthenticated connections. Khi có 10 connection đang chờ auth, bắt đầu drop 30% connection mới, tăng dần đến 100% khi đạt 60. Chống connection flood attack nhắm vào pre-auth phase.
Sau khi chỉnh, luôn test trước khi reload:
sudo sshd -t && sudo systemctl reload sshd
Dùng reload thay restart, reload không ngắt session hiện tại.
Đổi port SSH: có đáng không?
Đây là chủ đề gây tranh cãi nhất trong SSH hardening, và cần nói thẳng: đổi port từ 22 sang port khác không tăng bảo mật thực sự với attacker có nmap. Quét 65535 port mất vài phút với tool hiện đại, port nào cũng tìm ra.
Nhưng vẫn nên đổi, vì lý do thực dụng khác. Bot scan mặc định chỉ nhắm port 22, đổi port giảm log noise đáng kể. Từ hàng nghìn failed login mỗi ngày xuống gần như zero. Log sạch hơn thì khi có event thật sự bất thường, bạn nhìn thấy ngay thay vì chìm trong biển bot.
Ngoài ra, mỗi handshake fail cũng tốn CPU và bandwidth nhỏ, trên server nhỏ thì không đáng kể, nhưng trên server xử lý nhiều thứ khác thì giảm được noise cũng tốt.
Nếu đổi port, nên chọn port trên 1024 (không cần root privilege) và tránh xung đột với service phổ biến. Nhưng quan trọng hơn cả việc chọn port là nhớ cập nhật mọi thứ liên quan: firewall rule, client .ssh/config, monitoring/SIEM, và nếu dùng RHEL/CentOS với SELinux thì phải:
sudo semanage port -a -t ssh_port_t -p tcp <new_port>
Quên bước SELinux là lý do phổ biến nhất khiến “đổi port rồi mà SSH không vào được”. Rất nhiều người mắc lúc mới dùng CentOS, service start OK nhưng SELinux chặn connection đến port mới, log chỉ hiện trong audit.log mà không biết nhìn ở đó.
Firewall: giới hạn ai connect được
Nếu chỉ được chọn một biện pháp bảo mật ngoài key-based auth, firewall whitelist là lựa chọn hiệu quả nhất. Nếu attacker không connect được tới port SSH, mọi attack vector khác đều vô nghĩa.
ufw trên Ubuntu/Debian
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 203.0.113.0/24 to any port 22 proto tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
from 203.0.113.0/24 giới hạn SSH về range IP văn phòng hoặc VPN. Cực kỳ hiệu quả, bất kể password hay key, nếu IP không match thì connection bị drop trước khi đến SSH daemon.
firewalld trên RHEL/Fedora
sudo firewall-cmd --permanent --new-zone=ssh-admin
sudo firewall-cmd --permanent --zone=ssh-admin --add-source=203.0.113.0/24
sudo firewall-cmd --permanent --zone=ssh-admin --add-service=ssh
sudo firewall-cmd --reload
Cloud Security Group
Nếu chạy trên AWS/GCP/Azure, Security Group (hoặc tương đương) là firewall đầu tiên, traffic bị chặn trước khi chạm OS. Nên giới hạn SSH source CIDR ở SG trước, rồi mới config firewall trên OS. Hai lớp firewall không phải thừa, SG chặn ở network level, ufw/firewalld chặn ở host level.
Nếu IP không cố định (làm việc remote, di chuyển nhiều), giải pháp tốt nhất là VPN hoặc bastion host. Tailscale hoặc WireGuard cho bạn IP ổn định trên mesh network, SSH qua VPN, firewall whitelist IP VPN. Gọn, sạch, và an toàn hơn hẳn mở port 22 ra 0.0.0.0/0.
fail2ban, giảm noise khi không whitelist IP được
Không phải lúc nào cũng whitelist được, field engineer, developer remote ở nhiều nơi, IP thay đổi liên tục. fail2ban theo dõi log SSH và tự động ban IP có quá nhiều failed attempt:
# /etc/fail2ban/jail.d/sshd.local
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = %(sshd_log)s
maxretry = 5
findtime = 600
bantime = 3600
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24
5 lần fail trong 10 phút thì ban 1 giờ. ignoreip whitelist IP nội bộ và văn phòng để không tự ban nhầm, chuyện này xảy ra thường xuyên hơn bạn nghĩ, đặc biệt khi ai đó gõ sai passphrase nhiều lần.
Cần nói rõ: khi đã tắt PasswordAuthentication, fail2ban ít việc hơn nhiều vì bot không thể brute force được. Nhưng nó vẫn hữu ích để chặn scanner, giảm log noise và giảm resource consumption từ handshake liên tục.
Bản thân fail2ban không thay thế cho hardening thực sự, nó chỉ là lớp giảm ồn ào. Xem nó như “dọn rác trước cửa”, nhà vẫn cần khoá cửa đàng hoàng.
sshguard là alternative nhẹ hơn, viết bằng C, nếu bạn muốn ít overhead. CrowdSec thì hay ở chỗ có reputation feed phân tán, ban IP từ danh sách cộng đồng, giống virus signature database nhưng cho IP.
2FA / MFA cho account quan trọng
Nếu server quan trọng (production, chứa data nhạy cảm), nên thêm lớp 2FA. Key bị lộ (laptop mất, key file bị copy) mà có 2FA thì attacker vẫn chưa vào được.
Cấu hình trong sshd_config:
AuthenticationMethods publickey,keyboard-interactive
Nghĩa là phải có key và qua PAM challenge (OTP). Cài libpam-google-authenticator là cách đơn giản nhất, user quét QR code bằng app authenticator, mỗi lần login phải nhập OTP 6 số.
Nếu budget cho phép, hardware key (YubiKey) là level tiếp theo. SSH hỗ trợ key Ed25519-SK (security key), tạo bằng ssh-keygen -t ed25519-sk, yêu cầu touch vật lý trên YubiKey mỗi lần login. Không có device vật lý, không login được, dù có copy key file cũng vô dụng vì key material nằm trong hardware.
Nên dùng 2FA cho tất cả admin account trên production. Cho developer account thông thường trên staging thì không bắt buộc, trade-off giữa bảo mật và developer experience. Cứng quá thì dev tìm cách bypass, lỏng quá thì rủi ro. Tuỳ threat model của team.
Match block: policy khác nhau cho từng nhóm
Một tính năng của sshd_config mà ít người biết: Match block cho phép áp rule khác nhau tuỳ theo user, group, hoặc source IP. Đặc biệt hữu ích khi cùng server nhưng có nhiều loại user với yêu cầu khác nhau.
Match Group admins
AuthenticationMethods publickey,keyboard-interactive
Match Group sftpusers
ChrootDirectory /srv/sftp/%u
ForceCommand internal-sftp
X11Forwarding no
AllowTcpForwarding no
Match Address 10.0.0.0/8
PermitRootLogin prohibit-password
Ví dụ trên: admins bắt buộc 2FA, sftpusers bị chroot vào thư mục riêng và chỉ dùng được SFTP (không shell), còn connection từ internal network thì cho phép root login bằng key (cho automation tool như Ansible).
Match block phải đặt cuối cùng trong config file, các directive sau Match chỉ áp dụng cho connection match điều kiện đó. Đặt giữa file sẽ gây nhầm lẫn hoặc syntax error.
Pattern này rất hữu ích cho server cần phục vụ nhiều mục đích, vừa là bastion cho admin, vừa là SFTP server cho đối tác upload file, vừa là target cho Ansible automation. Mỗi nhóm có policy phù hợp mà không cần chạy nhiều SSH daemon.
Audit và logging
Hardening mà không có logging thì giống khoá cửa mà không có camera, bạn không biết ai đã thử vào, ai đã vào thành công, và lúc nào.
Log cần theo dõi
SSH log nằm ở /var/log/auth.log (Debian/Ubuntu) hoặc /var/log/secure (RHEL/CentOS). Với LogLevel VERBOSE, mỗi login thành công ghi lại key fingerprint, rất giá trị khi cần biết “ai login bằng key nào”. Xem live:
journalctl -u ssh -f
Các command cơ bản hữu ích:
last -a # login thành công gần đây
sudo lastb -a # login thất bại gần đây
w # ai đang online
who -u # session hiện tại + idle time
Tập trung log về một chỗ
Log nằm rải rác trên từng server thì khi cần điều tra, bạn phải SSH vào từng máy grep, chậm và dễ sót. Đẩy log về SIEM hoặc log aggregator (ELK, Loki, Datadog) là đầu tư rất xứng đáng. Tập trung rồi thì tìm pattern dễ hơn nhiều: login từ IP lạ ngoài giờ làm việc, failed attempt tăng đột biến (brute force), hoặc user hệ thống (postgres, root) bất ngờ login, dấu hiệu compromise.
Thông báo login realtime
Với server quan trọng, nên setup thông báo mỗi khi có SSH login thành công:
# /etc/ssh/sshrc
echo "SSH login: $USER from $SSH_CONNECTION at $(date)" \
| mail -s "SSH login $USER@$(hostname)" ops@example.com
Hoặc gửi qua Slack webhook, Telegram bot, tuỳ tool team dùng. Mục đích là biết ngay khi có login bất thường mà không phải chờ đến lúc review log.
Rotation key và vệ sinh định kỳ
Hardening không phải việc làm một lần rồi quên. Server sống lâu tích luỹ nhiều thứ cần dọn.
Audit authorized_keys định kỳ là việc ít team làm nhưng cực kỳ quan trọng. Người rời team thì key vẫn còn trên server, nếu không có quy trình offboard xoá key, cựu nhân viên vẫn có thể access. Nên viết script so khớp danh sách key trên server với danh sách nhân sự active, chạy monthly, alert nếu có key mồ côi.
Host key rotation không cần làm thường xuyên, nhưng phải thay ngay khi nghi ngờ bị lộ (server bị compromise rồi rebuild chẳng hạn). Khi đổi host key, client sẽ thấy warning “host key changed”, cần thông báo trước cho team để tránh ai đó hoảng nghĩ bị MITM.
Patch OpenSSH, SSH cũng có 0-day. CVE-2024-6387 (regreSSHion) là ví dụ gần đây, ảnh hưởng hàng triệu server. Bật auto security update (unattended-upgrades trên Ubuntu) để patch kịp thời, chỉ nên tự động cho security updates, không auto-update package thường để tránh break.
known_hosts phía client, khi reinstall server, host key đổi, client sẽ báo lỗi. Xoá entry cũ bằng ssh-keygen -R hostname rồi connect lại để accept key mới. Nhưng hãy chắc chắn bạn biết tại sao host key đổi trước khi accept, nếu server không reinstall mà key đổi thì có thể đang bị MITM.
VPN: giải pháp sạch nhất
Còn một hướng tiếp cận ngày càng được ưa chuộng hơn các giải pháp phía trên: đặt SSH hoàn toàn sau VPN.
WireGuard hoặc Tailscale tạo mesh network riêng, server chỉ listen SSH trên interface VPN, không expose ra Internet. Firewall chặn port 22 trên public interface hoàn toàn. Muốn SSH thì phải connect VPN trước.
Internet → [Server: port 22 BLOCKED]
VPN mesh → [Server: port 22 OPEN on VPN interface only]
Với cách này, toàn bộ SSH brute force, bot scan, log noise đều biến mất, vì port 22 không exist trên public Internet. fail2ban không cần, đổi port không cần. Bạn chỉ cần bảo mật VPN access, mà Tailscale với SSO integration và device authorization đã làm rất tốt.
Tailscale phù hợp cho hầu hết side project và server cá nhân. Cho production enterprise thì WireGuard tự host hoặc VPN provider riêng tuỳ yêu cầu compliance. Nhưng concept giống nhau: ẩn SSH khỏi Internet hoàn toàn.
Không phải mọi tình huống đều dùng được VPN, có server cần SSH access từ CI/CD pipeline không qua VPN, hoặc khách hàng cần SFTP access. Lúc đó quay lại combo key + firewall whitelist + fail2ban ở trên.
Tóm tắt
Cứng hoá SSH xoay quanh ba tầng: ai được đến (firewall whitelist, bastion, VPN), đến rồi phải chứng minh gì (key + passphrase, 2FA, AllowGroups), và bạn thấy được gì (VERBOSE log, tập trung log, alert login).
Thứ tự ưu tiên nếu bạn chỉ có 15 phút cho server mới: tạo Ed25519 key với passphrase, tắt PasswordAuthentication, tắt PermitRootLogin, set AllowGroups. Bốn dòng config đó đã loại bỏ 99% attack surface từ bot.
Nếu có thêm thời gian: firewall whitelist IP, fail2ban, 2FA cho admin account, log tập trung. Và nếu có thể, đặt SSH sau VPN và quên hết mấy thứ phía trên.
Không có cấu hình “vĩnh viễn an toàn”, chỉ có cấu hình phù hợp với threat model hiện tại. Audit key khi người rời team, patch OpenSSH khi có CVE, review config vài tháng một lần. Server mới cần 15 phút hardening ban đầu; phần thưởng là những đêm ngủ yên hơn.