Tuần này trên Hacker News có hai bài về SSH cùng lúc lên top: một advisory của Alpine Linux về lỗi bypass password authentication trên OpenSSH-server, và một bài khác cũng nói về SSH được vote hơn 1000 điểm. Đọc comment thì thấy nhiều người hỏi cùng một câu: "Server của tôi có bị ảnh hưởng không?". Câu trả lời thật ra đơn giản: nếu server của bạn không cho phép đăng nhập bằng password ngay từ đầu, thì cả một nhóm lỗi kiểu này không đụng tới bạn được.
Mình làm vận hành cho vài chục VPS và cluster nhỏ đã nhiều năm. Bài này là checklist mình thực sự dùng để harden sshd, kèm cách kiểm tra config như một attacker nhìn vào, chứ không chỉ sửa file rồi hy vọng là nó chạy đúng.
1. Vì sao password authentication là bề mặt tấn công thừa
Mỗi phương thức xác thực bạn bật trong sshd là thêm một đoạn code có thể có bug. Password auth đi qua PAM, qua keyboard-interactive, qua nhiều nhánh xử lý khác nhau tùy distro patch. Lỗi ở advisory của Alpine là ví dụ điển hình: lỗ hổng nằm ở chỗ kết hợp giữa bản build/patch của distro với cơ chế xác thực, chứ không hẳn ở lõi OpenSSH upstream.
Trong khi đó, public key authentication có logic đơn giản hơn nhiều: server kiểm tra chữ ký bằng public key đã biết trước. Không có gì để brute-force, không có gì để đoán.
flowchart LR A[Client kết nối] --> B{Phương thức auth} B -->|password| C[PAM / keyboard-interactive] C --> D[Brute-force, credential stuffing, bug của distro patch] B -->|publickey| E[Xác minh chữ ký] E --> F[Không có secret để đoán] B -->|certificate| G[Xác minh chữ ký CA + thời hạn] G --> F Nguyên tắc của mình: chỉ bật đúng một phương thức, và đó là publickey (hoặc certificate).
2. Config sshd tối thiểu nhưng chặt
Từ OpenSSH 8.2 trở đi, hầu hết distro (Ubuntu 22.04+, Debian 12, Alpine, Rocky 9) đều hỗ trợ thư mục /etc/ssh/sshd_config.d/. Đừng sửa file gốc, hãy tạo một file drop-in để dễ quản lý bằng Ansible và dễ diff khi upgrade package.
# /etc/ssh/sshd_config.d/10-hardening.conf # Chỉ cho phép public key PubkeyAuthentication yes PasswordAuthentication no KbdInteractiveAuthentication no AuthenticationMethods publickey # Không login trực tiếp bằng root PermitRootLogin no # Giới hạn user được phép AllowGroups ssh-users # Giảm thời gian và số lần thử LoginGraceTime 20 MaxAuthTries 3 MaxStartups 10:30:60 # Tắt những thứ không dùng X11Forwarding no AllowAgentForwarding no PermitTunnel no # Chỉ dùng thuật toán hiện đại HostKeyAlgorithms ssh-ed25519,ssh-ed25519-cert-v01@openssh.com KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256 Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com Vài điểm hay bị bỏ qua:
-
AuthenticationMethods publickeylà dòng quan trọng nhất. Ngay cả khi một file config khác (do cloud-init hay package nào đó tạo ra) bật lạiPasswordAuthentication yes, dòng này vẫn bắt buộc phải qua publickey. - Thứ tự file drop-in có ý nghĩa: với
sshd, giá trị đầu tiên được đọc sẽ thắng, không phải giá trị cuối. Ubuntu cloud image hay có file50-cloud-init.confbật password. Đặt tên file của bạn là10-...để nó được đọc trước. - OpenSSH 9.8+ có
PerSourcePenaltiesbật mặc định, tự động phạt các IP kết nối hỏng liên tục. Nếu distro của bạn đã có bản này thì không cần fail2ban cho SSH nữa.
Trước khi reload, luôn test config và giữ một session SSH đang mở:
sudo sshd -t && sudo systemctl reload ssh # Debian/Ubuntu sudo sshd -t && sudo rc-service sshd reload # Alpine (OpenRC) 3. Kiểm tra config như attacker: đừng tin file, hãy tin sshd -T
Đây là phần mình thấy nhiều team bỏ qua nhất. Bạn sửa file, nhưng giá trị thực tế sshd đang dùng có thể khác do Match block, do include, do default của distro. Lệnh sshd -T in ra config đã được resolve hoàn chỉnh:
sudo sshd -T | grep -Ei 'passwordauth|kbdinteractive|authenticationmethods|permitrootlogin' # Kiểm tra cho một user/IP cụ thể (để test Match block) sudo sshd -T -C user=deploy,host=example.com,addr=203.0.113.10 | grep -i password Nhưng kiểm tra từ phía server vẫn chưa đủ. Mình viết một script nhỏ bằng Python chạy từ máy CI, hỏi thẳng server xem nó thực sự quảng bá những phương thức auth nào. Đây chính là thứ attacker nhìn thấy:
#!/usr/bin/env python3 # check_ssh_auth.py - yêu cầu: pip install paramiko==3.5.0 import sys import paramiko def allowed_methods(host: str, port: int = 22) -> list[str]: sock = paramiko.Transport((host, port)) try: sock.start_client(timeout=10) try: # Gửi auth 'none' để server trả về danh sách method được phép sock.auth_none('probe-user') except paramiko.BadAuthenticationType as e: return list(e.allowed_types) return ['none'] # Rất nguy hiểm nếu tới được đây finally: sock.close() if __name__ == '__main__': failed = False for host in sys.argv[1:]: methods = allowed_methods(host) bad = [m for m in methods if m != 'publickey'] status = 'FAIL' if bad else 'OK' print(f'{status:4} {host:30} {methods}') failed |= bool(bad) sys.exit(1 if failed else 0) Chạy python3 check_ssh_auth.py 10.0.0.11 10.0.0.12 bastion.internal trong pipeline hằng đêm. Nếu ai đó vô tình bật lại password (rất hay xảy ra khi rebuild image), CI sẽ đỏ ngay. Bổ sung thêm ssh-audit (bản 3.3.0) để kiểm tra thuật toán yếu:
pipx install ssh-audit ssh-audit -l warn bastion.internal 4. Nâng cấp lên SSH certificate thay vì quản lý authorized_keys
Khi team lên tới 10+ người và 30+ server, việc copy public key vào authorized_keys trở thành ác mộng: người nghỉ việc vẫn còn key, không ai biết key nào của ai. Giải pháp là SSH certificate, tính năng có sẵn trong OpenSSH từ lâu nhưng ít người dùng.
sequenceDiagram participant Dev participant CA as SSH CA (offline/Vault) participant Server Dev->>CA: Gửi public key + xác thực SSO CA-->>Dev: Certificate ký, hạn 8 giờ Dev->>Server: ssh với certificate Server->>Server: Kiểm tra chữ ký CA, principal, thời hạn Server-->>Dev: Cho phép đăng nhập Các bước cơ bản:
# 1. Tạo CA (giữ private key thật kỹ, tốt nhất là offline hoặc trong HSM/Vault) ssh-keygen -t ed25519 -f ssh_user_ca -C 'user-ca-2026' # 2. Ký key cho dev, hạn 8 giờ, principal là 'deploy' ssh-keygen -s ssh_user_ca -I 'andy@laptop' -n deploy -V +8h ~/.ssh/id_ed25519.pub # -> sinh ra ~/.ssh/id_ed25519-cert.pub # 3. Trên server, tin CA này echo 'TrustedUserCAKeys /etc/ssh/ssh_user_ca.pub' | sudo tee /etc/ssh/sshd_config.d/20-ca.conf Lợi ích thực tế: certificate tự hết hạn sau 8 giờ, nên khi có người nghỉ việc bạn chỉ cần ngừng ký cho họ. Không cần đi dọn authorized_keys trên từng máy. Nếu không muốn tự vận hành CA, HashiCorp Vault có SSH secrets engine làm đúng việc này.
Kết luận
Mỗi lần có advisory về OpenSSH, thay vì hoảng loạn đi kiểm tra version, hãy tự hỏi: "Phương thức xác thực bị lỗi đó có đang bật trên server của mình không?". Nếu bạn đã làm theo checklist dưới đây, câu trả lời gần như luôn là không:
- Tắt hoàn toàn password và keyboard-interactive, khóa bằng
AuthenticationMethods publickey. - Dùng drop-in
sshd_config.d/10-*.confvà nhớ quy tắc "giá trị đầu tiên thắng". - Luôn chạy
sshd -ttrước khi reload, và giữ một session đang mở để tránh tự khóa mình ngoài server. - Kiểm tra từ bên ngoài: dùng
sshd -T, script probeauth_nonevàssh-audittrong CI hằng đêm. - Team lớn thì chuyển sang SSH certificate với thời hạn ngắn, bỏ việc quản lý
authorized_keysthủ công. - Cập nhật OpenSSH lên 9.8+ để có
PerSourcePenalties, và theo dõi security advisory của distro bạn dùng, vì nhiều lỗi nằm ở patch của distro chứ không phải upstream.
Giảm bề mặt tấn công luôn hiệu quả hơn chạy theo từng CVE. Hãy dành 30 phút tuần này để audit lại SSH trên các server của bạn.

