BÀI VIẾT // [SECURITY] SSH Key Authentication và Hardening Server
SSH Key Authentication và Hardening Server

SSH Key Authentication và Hardening Server

Hướng dẫn xác thực SSH bằng khoá: chọn thuật toán, tạo khoá, cấu hình client, hardening sshd, authorized_keys nâng cao, SSH certificate, chống brute force và chuẩn bị hậu lượng tử.

SECURITY

SSH là cánh cửa chính của gần như mọi server Linux, nhưng cách cấu hình SSH phổ biến trên Internet vẫn dừng ở mức của nhiều năm trước: tạo khoá, copy lên server, tắt mật khẩu. Trong khi đó, bản thân OpenSSH đã thay đổi khá nhiều — thuật toán yếu bị xoá, mặc định bảo mật được siết lại, và chữ ký hậu lượng tử đã bật sẵn từ bản 10.6.

Bài này đi từ việc chọn đúng loại khoá, tạo khoá, cấu hình cả hai phía client và server, cho tới những thứ thường bị bỏ qua: authorized_keys với ràng buộc, SSH certificate, chống brute force, vòng đời khoá và lộ trình hậu lượng tử.


Vì sao mật khẩu không còn là lựa chọn

Mật khẩu trên SSH có hai vấn đề cố hữu: nó có thể bị đoán, và nó đi qua một kênh mã hoá nhưng vẫn phụ thuộc vào độ mạnh của chính nó. Chỉ cần một botnet quét cổng 22 là bạn sẽ thấy hàng nghìn lần thử đăng nhập mỗi ngày.

Điểm dễ bị bỏ sót là PasswordAuthentication mặc định vẫn là yes trong sshd. Nghĩa là một server mới cài, dù đã thêm khoá, vẫn chấp nhận đăng nhập bằng mật khẩu nếu bạn không tắt hẳn. Xác thực bằng khoá đảo ngược mô hình: server giữ public key, private key không bao giờ rời khỏi máy bạn, và kẻ tấn công không có gì để đoán.


Những thay đổi quan trọng của OpenSSH gần đây

Trước khi cấu hình, cần biết phiên bản mình đang chạy vì hành vi mặc định đã đổi:

Phiên bảnThay đổi đáng chú ý
10.0 (04/2025)Xoá hoàn toàn thuật toán chữ ký DSA (ssh-dss)
10.1 (10/2025)Cảnh báo khi kết nối dùng key exchange không hậu lượng tử; thêm WarnWeakCrypto phía client
10.3 (04/2026)Certificate có principals rỗng không còn được hiểu là wildcard
10.4 (07/2026)Hỗ trợ thử nghiệm chữ ký lai hậu lượng tử mldsa44-ed25519
10.5 (08/2026)restrict trong authorized_keys áp dụng đúng cho cả tunnel forwarding
10.6 (10/2026)Bật mặc định chữ ký lai ssh-mldsa44-ed25519; WarnWeakCrypto cho cả server; default KDF rounds tăng lên 32; default RSA tăng lên 3072 bit

Kiểm tra phiên bản và danh sách thuật toán đang hỗ trợ:

1
2
3
4
ssh -V
ssh -Q kex        # key exchange
ssh -Q key        # loại khoá
ssh -Q sig        # thuật toán chữ ký

Chọn loại khoá phù hợp

Loại khoáKhi nào dùngGhi chú
ed25519Mặc định cho hầu hết trường hợpNhanh, khoá nhỏ, không phụ thuộc NIST curve. Là lựa chọn đầu tiên
ed25519-skKhi cần khoá gắn phần cứngPrivate key nằm trong FIDO2 token, không thể trích xuất
mldsa44-ed25519Khi muốn thử chữ ký hậu lượng tửMới và chưa phổ biến rộng; chỉ dùng khi cả hai phía đều hỗ trợ
rsaChỉ khi bắt buộc tương thíchPhải dài tối thiểu 3072 bit; mặc định mới của ssh-keygen đã là 3072
ecdsaChấp nhận được nhưng không lý tưởngPhụ thuộc NIST curve; không có lợi thế so với Ed25519
dsaKhông dùngĐã bị xoá khỏi OpenSSH từ bản 10.0

Về phía hệ sinh thái, GitHub cũng siết lại: từ 14/10/2026, mọi khoá RSA mới upload phải đạt tối thiểu 3072 bit, ssh-rsa dùng SHA-1 sẽ bị loại bỏ hoàn toàn vào 13/01/2027 (có brownout trước đó). Nếu bạn còn khoá RSA cũ trong CI/CD, đây là thứ cần xử lý ngay.


Tạo khoá đúng chuẩn

Tạo khoá Ed25519 với số vòng KDF cao hơn mặc định để chống brute-force khi private key bị lộ:

1
ssh-keygen -t ed25519 -a 100 -C "hoangduong@workstation-01"
  • -a 100: số vòng KDF (bcrypt_pbkdf). Mặc định hiện tại là 32; tăng lên giúp mỗi lần thử passphrase chậm hơn đáng kể với kẻ tấn công.
  • -C: comment đặt theo user@máy để sau này biết khoá nào của ai. Comment không ảnh hưởng bảo mật nhưng ảnh hưởng khả năng vận hành.
  • Luôn đặt passphrase. Private key không passphrase nghĩa là bất kỳ ai copy được file là đăng nhập được.

Đối chiếu fingerprint của khoá để dùng về sau khi xác minh host key:

1
ssh-keygen -lf ~/.ssh/id_ed25519.pub

Nếu buộc phải dùng RSA, chỉ định rõ độ dài thay vì để mặc định:

1
ssh-keygen -t rsa -b 4096 -a 100 -C "compat-key-01"

Khoá cứng FIDO2 cho môi trường nhạy cảm

Với server production hoặc tài khoản có quyền cao, khoá nằm trong thiết bị phần cứng là lựa chọn tốt hơn file trên ổ đĩa. Private key được sinh bên trong token và không thể trích xuất:

1
ssh-keygen -t ed25519-sk -O resident -O verify-required -C "yubikey-01"
  • -O resident: lưu key handle trong token, dùng được trên nhiều máy.
  • -O verify-required: mỗi lần ký phải xác minh bằng PIN hoặc sinh trắc học.
  • restrict và verify-required cũng có thể ràng buộc ở phía authorized_keys.

Nạp khoá resident từ token vào agent:

1
2
ssh-keygen -K
ssh-add -P     # bỏ qua bước nhập PIN với token không yêu cầu

Đưa public key lên server

Cách gọn nhất là ssh-copy-id, nó thêm khoá vào đúng file với đúng quyền:

1
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@<server-host>

Kiểm tra lại quyền trên server — sai quyền là nguyên nhân phổ biến nhất khiến publickey thất bại:

1
2
3
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod go-w ~                 # home không được để group/other ghi

sshd bật StrictModes yes mặc định: nếu thư mục home, ~/.ssh hoặc authorized_keys bị nhóm/người khác ghi được, sshd sẽ từ chối dùng khoá đó. Trên hệ thống có SELinux, nhớ gán lại nhãn:

1
restorecon -R -v ~/.ssh

ssh-agent và cạm bẫy agent forwarding

Agent giữ private key đã giải mã trong bộ nhớ, giúp bạn không phải nhập passphrase liên tục:

1
2
eval "$(ssh-agent -s)"
ssh-add -t 8h ~/.ssh/id_ed25519     # tự hết hạn sau 8 giờ

Trên macOS, thêm khoá vào Keychain bằng UseKeychain yes trong ~/.ssh/config để agent sống qua các lần reboot.

Agent forwarding (ForwardAgent) là thứ nên tránh mặc định. Khi bật, bất kỳ ai có quyền root trên máy đích đều có thể dùng agent socket được forward để ký bằng khoá của bạn — không lấy được private key, nhưng dùng được nó. Nếu buộc phải forward, hãy giới hạn theo từng host chứ đừng bật toàn cục. Từ OpenSSH 10.6 có AgentSocketPath cho phép kiểm soát vị trí socket agent trên server.


Cấu hình client gọn gàng và an toàn

Đặt mọi thứ vào ~/.ssh/config thay vì gõ cờ dài mỗi lần:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Host prod-web-01
    HostName 203.0.113.10
    User deploy
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes
    AddKeysToAgent confirm

Host *
    HashKnownHosts yes
    StrictHostKeyChecking accept-new
    UpdateHostKeys yes
    ServerAliveInterval 60
    ServerAliveCountMax 3
  • IdentitiesOnly yes: chỉ dùng đúng khoá đã khai báo, tránh việc agent thử hàng loạt khoá và bị server đếm vào MaxAuthTries.
  • AddKeysToAgent confirm: hỏi trước khi nạp khoá vào agent, tránh việc khoá bị dùng ngầm.
  • HashKnownHosts yes: che tên host trong known_hosts phòng khi file bị lộ. Lưu ý mặc định vẫn là no.
  • StrictHostKeyChecking accept-new: tự thêm host key mới nhưng vẫn từ chối khi host key thay đổi — điểm cân bằng tốt cho tự động hoá. Với môi trường yêu cầu kiểm soát chặt, dùng yes và quản lý known_hosts tập trung.
  • UpdateHostKeys yes: cho phép server gửi host key bổ sung sau khi xác thực, hỗ trợ xoay host key mượt mà.
  • ServerAliveInterval: phát hiện kết nối chết, tránh treo phiên.

Mẹo kiểm tra khoá nào sẽ được thử, theo đúng thứ tự, mà không cần đăng nhập thật (từ 10.5):

1
ssh -Z deploy@<server-host>

Xác minh host key và known_hosts

Client phải kiểm tra danh tính server, nếu không thì mã hoá mạnh đến đâu cũng vô nghĩa trước MITM. known_hosts hỗ trợ hai marker hữu ích:

  • @cert-authority: một dòng chứa khoá CA tin cậy cho cả một miền, ví dụ *.example.com.
  • @revoked: đánh dấu khoá đã bị thu hồi, ssh sẽ luôn cảnh báo và không chấp nhận.
1
2
3
ssh-keygen -F prod-web-01        # tra host key của một host
ssh-keygen -R prod-web-01        # xoá host key cũ khi server đổi khoá
ssh-keygen -H                    # hash toàn bộ known_hosts

Với hạ tầng có DNS an toàn, có thể dùng SSHFP để xác minh qua DNS:

1
VerifyHostKeyDNS ask

Khi cần thu hồi khẩn cấp, dùng Key Revocation List (KRL) gắn vào sshd qua RevokedKeys, hoặc RevokedHostKeys phía client.


Hardening phía server

Nguyên tắc an toàn: không sửa trực tiếp file cấu hình chính. Trên Debian/Ubuntu, sshd_config thường đã có Include /etc/ssh/sshd_config.d/*.conf; hãy tạo một file drop-in riêng:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
sudo tee /etc/ssh/sshd_config.d/00-hardening.conf >/dev/null <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
PubkeyAuthentication yes
PermitEmptyPasswords no
AllowGroups ssh-users
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowTcpForwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
EOF

Giải thích nhanh các dòng quan trọng:

DirectiveÝ nghĩa
PasswordAuthentication noTắt hẳn đăng nhập bằng mật khẩu
KbdInteractiveAuthentication noTắt kênh keyboard-interactive (mặc định đang là yes)
PermitRootLogin prohibit-passwordRoot không đăng nhập bằng mật khẩu; mặc định an toàn nhưng nên khai báo rõ
AllowGroups ssh-usersChỉ nhóm được chỉ định mới đăng nhập được
MaxAuthTries 3Giảm số lần thử trước khi ngắt kết nối
LoginGraceTime 30Rút ngắn cửa sổ cho kết nối chưa xác thực
AllowTcpForwarding noTắt forwarding nếu không cần

Kiểm tra cấu hình trước khi nạp lại — bước này bắt buộc:

1
2
3
sudo sshd -t                                   # kiểm tra cú pháp
sudo sshd -T | grep -Ei 'passwordauth|permitrootlogin|allowgroups'
sudo systemctl reload ssh    # hoặc sshd, tùy distro

sshd -T in ra cấu hình hiệu lực sau khi áp dụng mọi drop-in và Match, đây là cách duy nhất chắc chắn biết mình đã tắt đúng thứ cần tắt. Trên các distro có systemd socket activation, hãy dùng systemctl reload ssh.socket nếu service được kích hoạt theo socket.

Chống tự khoá mình ra ngoài: luôn giữ một phiên SSH đang mở trong lúc thay đổi, và mở terminal thứ hai để kiểm tra đăng nhập trước khi đóng phiên cũ. Với người dùng cần ngoại lệ, dùng Match:

1
2
3
4
Match User backup-bot
    PasswordAuthentication no
    AllowTcpForwarding no
    PermitTTY no

authorized_keys không chỉ là danh sách khoá

Mỗi dòng trong authorized_keys có thể mang theo ràng buộc. Đây là công cụ bị dùng ít nhất so với giá trị của nó:

OptionTác dụng
restrictTắt toàn bộ forwarding, PTY và ~/.ssh/rc; bật lại từng thứ khi cần
from="..."Chỉ chấp nhận từ dải IP/CIDR hoặc hostname nhất định
command="..."Ép chạy đúng một lệnh, bỏ qua lệnh client gửi
expiry-time="..."Khoá tự hết hạn theo ngày/giờ
permitopen / permitlistenGiới hạn đích của -L / -R forwarding
no-touch-required / verify-requiredRàng buộc riêng cho khoá FIDO2

Ví dụ một khoá dành riêng cho job backup, chỉ chạy từ dải IP nội bộ và buộc đúng script:

1
restrict,from="198.51.100.0/24",command="/usr/local/bin/backup.sh" ssh-ed25519 AAAA... backup-bot

Lưu ý: force-command chỉ ép lệnh, không tự động tắt forwarding. Vì vậy restrict nên là điểm bắt đầu cho mọi khoá không cần shell tương tác.


SSH Certificate cho hạ tầng nhiều máy

Quản lý authorized_keys thủ công không mở rộng được. SSH certificate giải quyết bằng cách để mọi server chỉ tin một CA, còn user key được CA ký với thời hạn ngắn:

1
2
3
4
5
6
# Trên máy CA
ssh-keygen -t ed25519 -f ~/.ssh/ca_user_key -C "ssh-ca"

# Ký certificate cho user, hiệu lực 8 giờ
ssh-keygen -s ~/.ssh/ca_user_key -I "hoangduong@company" \
    -n deploy -V +8h ~/.ssh/id_ed25519.pub

Phía server, chỉ cần tin CA và quy định principal hợp lệ:

1
2
TrustedUserCAKeys /etc/ssh/ca_user_key.pub
AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u

CA cũng có thể được tin trực tiếp trong authorized_keys bằng cert-authority,principals="deploy". Ưu điểm lớn nhất là certificate hết hạn tự động, nên không còn tình trạng khoá cũ nằm lại trên hàng trăm server sau khi nhân sự thay đổi. Host certificate (ssh-keygen -h) áp dụng cùng ý tưởng cho host key, giúp xoay host key ở quy mô lớn.


Chống brute force và bật 2FA

Nếu từng dựng fail2ban chỉ để chặn brute force SSH, có tin tốt: sshd hiện đã có cơ chế phạt theo nguồn, bật mặc định qua PerSourcePenalties:

KeywordMặc địnhÝ nghĩa
authfail5sXác thực thất bại rồi ngắt kết nối
invaliduser5sThử user không tồn tại
noauth1sKết nối rồi ngắt mà không xác thực
grace-exceeded10sQuá hạn LoginGraceTime
max10mMức phạt tối đa tích luỹ

Có thể siết thêm bằng PerSourceMaxStartups giới hạn số kết nối chưa xác thực từ một IP. fail2ban vẫn hữu ích khi cần ghi log tập trung hoặc chặn ở tầng firewall, nhưng nó không còn là lớp phòng thủ duy nhất.

Với tài khoản quyền cao, bật hai lớp xác thực bằng PAM (TOTP):

1
AuthenticationMethods publickey,keyboard-interactive

Lưu ý AuthenticationMethods yêu cầu từng phương thức phải được bật tương ứng — nếu bạn đã tắt KbdInteractiveAuthentication, hãy bật lại cho nhóm user cần 2FA thay vì bật toàn cục.


Chuẩn bị cho hậu lượng tử

Traffic SSH hôm nay có thể bị ghi lại và giải mã sau này. OpenSSH đã chuyển sang key exchange lai hậu lượng tử mặc định:

  • mlkem768x25519-sha256 nằm trong danh sách KexAlgorithms mặc định.
  • Từ 10.6, chữ ký lai ssh-mldsa44-ed25519 được bật mặc định; khoá tạo bằng bản thử nghiệm 10.4 phải tạo lại vì tên thuật toán đã thay đổi.
  • WarnWeakCrypto (mặc định yes) sẽ ghi log khi một kết nối dùng key exchange không hậu lượng tử, giúp bạn phát hiện client cũ.

Hai điểm cần lưu ý khi vận hành:

1
2
ssh -Q kex | grep -i mlkem
ssh -o KexAlgorithms=mlkem768x25519-sha256,curve25519-sha256 deploy@<server-host>

Nếu buộc phải tương thích với client cũ, dùng + để thêm chứ đừng thay thế danh sách mặc định. Về nén dữ liệu: từ 10.6 OpenSSH đã tắt bộ mã hoá từ điển LZ77 để vá một kênh side-channel trong phiên multiplex, nên Compression kém hiệu quả hơn; nếu cần nén, nén ở tầng ứng dụng sẽ tốt hơn.


Vòng đời khoá: xoay, thu hồi và sao lưu

Khoá không phải thứ tạo một lần rồi để đó. Một quy trình tối thiểu:

  • Kiểm kê: sshd -T để biết cấu hình hiệu lực, lastlog/journalctl -u ssh để xem ai đăng nhập bằng gì.
  • Xoay khoá: tạo khoá mới, thêm vào trước, xác minh đăng nhập được, rồi mới xoá khoá cũ. Không xoá trước khi thêm.
  • Thu hồi: xoá dòng tương ứng trong authorized_keys, hoặc dùng KRL khi số lượng lớn; với SSH certificate chỉ cần ngừng ký hoặc đưa vào KRL.
  • Sao lưu: backup private key ở nơi mã hoá, tách khỏi máy trạm. Mất private key không có cách nào phục hồi ngoài việc tạo lại.
  • Rà soát khoá cũ: mọi khoá ssh-rsa không dùng SHA-2 hoặc nhỏ hơn 3072 bit nên được thay thế.

Ghi chú triển khai

  • Thứ tự triển khai an toàn: thêm khoá → xác minh đăng nhập bằng khoá → mới tắt mật khẩu. Không làm ngược.
  • Luôn có đường thoát: giữ một phiên SSH đang mở, và nếu có thể, một kênh console/IPMI trước khi động vào sshd_config.
  • Kiểm tra bằng sshd -t rồi sshd -T trước khi reload; drop-in trong sshd_config.d chỉ hoạt động nếu file sshd_config gốc có directive Include.
  • Không coi đổi cổng SSH là bảo mật. Nó chỉ giảm tiếng ồn từ scanner; tắt mật khẩu và hardening mới là biện pháp thật.
  • Với tự động hoá/CI: dùng khoá riêng, ràng buộc bằng restrict,from=, và tuyệt đối không nhúng private key không passphrase vào biến môi trường dùng chung.
  • Đừng copy authorized_keys giữa các máy khi có thể dùng SSH certificate; mỗi bản copy là một chỗ để quên thu hồi.
  • Trên môi trường dùng SELinux, sau khi tạo ~/.ssh hoặc khôi phục từ backup, luôn chạy restorecon -R -v ~/.ssh.

Troubleshooting thường gặp

Permission denied (publickey) — chạy ssh -vvv để xem client có gửi khoá hay không, sau đó kiểm tra phía server:

1
2
3
4
ssh -vvv deploy@<server-host>
sudo sshd -T | grep -Ei 'pubkey|authorizedkeysfile|strictmodes'
ls -ld ~ ~/.ssh; ls -l ~/.ssh/authorized_keys
sudo journalctl -u ssh -n 50

Phần lớn trường hợp đến từ: sai quyền file, home bị group-writable, khoá không nằm trong AuthorizedKeysFile, hoặc SELinux context sai.

REMOTE HOST IDENTIFICATION HAS CHANGED — host key server đã đổi (cài lại, hoặc tấn công MITM). Xác minh fingerprint với quản trị viên qua kênh khác, rồi:

1
ssh-keygen -R <server-host>

sshd -t báo lỗi sau khi thêm drop-in — thường do option không được phép trong Match, hoặc sai tên directive. sshd -T với -C mô phỏng kết nối cụ thể sẽ cho biết cấu hình nào thực sự được áp dụng:

1
sudo sshd -T -C user=deploy,host=<server-host>,addr=203.0.113.10

Lời kết

SSH an toàn không nằm ở một dòng cấu hình thần kỳ, mà ở việc chọn đúng loại khoá, tắt hẳn mật khẩu, ràng buộc khoá bằng authorized_keys, và chuyển sang certificate khi số máy bắt đầu tăng. Phần hậu lượng tử đã được OpenSSH xử lý sẵn ở tầng key exchange — việc của bạn là đừng vô hiệu hoá nó vì một client cũ.

Hãy bắt đầu bằng việc kiểm tra server hiện tại: sshd -T xem PasswordAuthentication và PermitRootLogin đang là gì. Nếu vẫn là yes, đó là việc nên làm trước cả khi đọc hết bài này.

THẢO LUẬN & BÌNH LUẬN