Mỗi lần trang này được deploy, deploy.sh mở một kết nối SSH tới máy chủ, chép code lên và chạy bước đổi container. Mỗi lần git push, một kết nối SSH khác tới GitHub. Cả hai đều không hỏi mật khẩu. Bài này giải thích vì sao, và vì sao điều đó an toàn hơn dùng mật khẩu.
SSH là giao thức thứ hai trong seri ghép đủ cả ba công cụ: thỏa thuận khóa và mã hóa đối xứng như TLS (bài 5), nhưng cách tin máy chủ thì khác hẳn, và người dùng cũng đăng nhập bằng chữ ký thay vì mật khẩu.
Lộ trình: các bước của một kết nối SSH, xem kết nối thật tới GitHub, khóa máy chủ và known_hosts, đăng nhập bằng khóa, ed25519 so với RSA, ssh-agent và agent forwarding, chứng chỉ SSH, rồi gia cố máy chủ. Lệnh chạy bằng OpenSSH 10.3 trên máy thật. Bài chỉ kết nối tới GitHub, nơi khóa máy chủ được công bố công khai, và không in thông tin gì về máy chủ của trang này.
Ba bước của một kết nối
| Bước | Việc | Giống TLS ở đâu |
|---|---|---|
| 1. Trao đổi khóa | Hai bên thỏa thuận bí mật chung bằng khóa tạm, rồi bật mã hóa | Giống hệt: ECDHE, có forward secrecy |
| 2. Xác thực máy chủ | Máy chủ ký kết quả trao đổi khóa bằng khóa máy chủ lâu dài | Giống CertificateVerify, nhưng không có CA |
| 3. Xác thực người dùng | Người dùng chứng minh danh tính: bằng khóa (ký), hoặc mật khẩu | TLS hiếm khi có bước này |
Khác biệt lớn nhất nằm ở bước 2: không có CA nào đứng ra bảo đảm "khóa này thuộc về máy chủ kia". Phần dưới giải thích SSH xoay xở thế nào.
Xem kết nối thật tới GitHub
$ ssh -vv -o PreferredAuthentications=none [email protected]
kex: algorithm: mlkem768x25519-sha256
kex: host key algorithm: ssh-ed25519
kex: server->client cipher: [email protected] MAC: <implicit> compression: none
kex: client->server cipher: [email protected] MAC: <implicit> compression: none
Server host key: ssh-ed25519 SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU
Authentications that can continue: publickey
PreferredAuthentications=none dừng ngay trước bước 3 để chỉ quan sát. Đọc từng dòng:
mlkem768x25519-sha256: trao đổi khóa lai hậu lượng tử, cùng ý tưởng với X25519MLKEM768 trong TLS của trang này (bài 4). OpenSSH chọn sẵn, không cần cấu hình. Trước đó, từ bản 9.0 (2022), OpenSSH đã dùng mặc định một kiểu lai khác làsntrup761x25519.ssh-ed25519: GitHub chứng minh danh tính bằng chữ ký Ed25519.chacha20-poly1305: dữ liệu đi qua một AEAD, như TLS.Authentications that can continue: publickey: GitHub chỉ nhận đăng nhập bằng khóa, không nhận mật khẩu.
Khóa máy chủ và known_hosts: tin ngay lần đầu
Lần đầu SSH tới một máy chủ, bạn gặp câu này:
The authenticity of host 'github.com' can't be established.
ED25519 key fingerprint is SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU.
Are you sure you want to continue connecting (yes/no)?
Không có CA, nên SSH dùng mô hình TOFU (trust on first use, tin ngay lần đầu): lần đầu bạn tự quyết định có tin khóa này hay không. Gõ yes thì khóa được ghi vào ~/.ssh/known_hosts. Từ lần sau, nếu máy chủ đưa ra khóa khác, SSH từ chối kết nối với cảnh báo REMOTE HOST IDENTIFICATION HAS CHANGED!.
Lỗ hổng của mô hình nằm ở lần đầu: nếu kẻ đứng giữa có mặt đúng lúc đó, bạn tin luôn khóa của họ. Cách làm đúng là so dấu vân tay với một nguồn tin cậy khác kênh SSH. GitHub công bố dấu vân tay trên trang tài liệu (qua HTTPS), và cả ba khóa lấy được bằng ssh-keyscan đều khớp với danh sách đó:
256 SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM github.com (ECDSA)
256 SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU github.com (ED25519)
3072 SHA256:uNiVztksCsDhcc0u9e8BujQXVUpKZIDTMczCvj3tD2s github.com (RSA)
Với máy chủ của mình, lấy dấu vân tay từ bảng điều khiển của nhà cung cấp VPS (thường in trong log khởi động của máy), rồi so với lần kết nối đầu. Hoặc cài sẵn khóa máy chủ vào known_hosts qua một kênh khác.
Cảnh báo "host key changed" thường có lý do vô hại, như máy chủ được cài lại hay địa chỉ IP được cấp cho máy khác. Nhưng đừng bao giờ xóa dòng cũ trong known_hosts cho tới khi biết chắc vì sao khóa đổi.
Đăng nhập bằng khóa
Thay vì gửi mật khẩu, người dùng có một cặp khóa. Khóa công khai được đặt trên máy chủ, trong ~/.ssh/authorized_keys. Khi đăng nhập, máy chủ yêu cầu ký một dữ liệu gắn với phiên này (mã phiên sinh ra từ bước trao đổi khóa), và kiểm chữ ký bằng khóa công khai đã đặt sẵn.
So với mật khẩu:
| Mật khẩu | Khóa | |
|---|---|---|
| Máy chủ nhận được gì | Chính mật khẩu (dù qua kênh mã hóa) | Một chữ ký chỉ có giá trị cho phiên này |
| Máy chủ giả mạo (lần đầu tin nhầm) lấy được gì | Mật khẩu, đem đi dùng ở máy chủ thật | Không gì dùng lại được |
| Bị dò từ xa | Có, và xảy ra liên tục trên mọi máy có cổng 22 mở | Không thực tế (256 bit) |
| Dùng lại ở nhiều nơi | Người dùng hay làm vậy | Khóa công khai đặt ở nhiều nơi cũng không sao |
Một máy chủ có cổng 22 mở ra Internet sẽ nhận các lượt dò mật khẩu tự động gần như ngay khi được bật. Đó là lý do việc đầu tiên khi dựng máy là chuyển sang đăng nhập bằng khóa và tắt mật khẩu.
ed25519 so với RSA
Tạo khóa:
ssh-keygen -t ed25519 -C "an@laptop"
| ed25519 | RSA 4096 | |
|---|---|---|
Khóa công khai (.pub) |
115 byte | 759 byte |
| Khóa bí mật (file) | 432 byte | 3.401 byte |
| Ký, kiểm chữ ký | Nhanh (bài 4) | Chậm hơn khi ký |
| Sinh số ngẫu nhiên khi ký | Không cần (an toàn hơn) | Không cần, nhưng cần padding đúng |
Dùng ed25519 cho mọi khóa mới. RSA chỉ còn cần khi phải kết nối với hệ thống rất cũ. Các khóa DSA đã bị gỡ hẳn khỏi OpenSSH, và chữ ký RSA kiểu cũ ssh-rsa (dùng SHA-1) đã bị tắt mặc định từ năm 2021.
Hai thói quen quan trọng hơn chọn thuật toán:
- Đặt passphrase cho khóa bí mật. File khóa sẽ được mã hóa (bằng một hàm dẫn xuất khóa chậm, cùng ý tưởng với bài 3), nên ai chép được file cũng không dùng được ngay.
- Mỗi thiết bị một khóa. Mất laptop thì xóa đúng một dòng trong
authorized_keys, không phải đổi khóa ở mọi nơi. Phần-C(comment) giúp biết dòng nào của thiết bị nào.
Khóa có thể nằm hẳn trong khóa phần cứng (ssh-keygen -t ed25519-sk với YubiKey và các khóa FIDO2 khác): khóa bí mật không bao giờ rời thiết bị, và mỗi lần ký phải chạm tay vào khóa.
ssh-agent và agent forwarding
Gõ passphrase mỗi lần SSH rất mệt. ssh-agent giữ khóa đã mở khóa trong RAM và ký thay cho các lần kết nối. Trên macOS, nó gắn với Keychain. Khóa bí mật không bao giờ rời agent, các chương trình chỉ nhờ agent ký giúp.
Agent forwarding (ssh -A) cho máy chủ từ xa dùng agent trên laptop của bạn, ví dụ để git pull từ GitHub ngay trên máy chủ. Rủi ro: trong lúc bạn đang kết nối, ai có quyền root trên máy chủ đó cũng nhờ agent của bạn ký được, tức là đăng nhập được vào mọi nơi khóa của bạn mở được. Họ không lấy được khóa, nhưng dùng được nó. Các lựa chọn an toàn hơn:
ProxyJump(ssh -J bastion target) khi cần đi qua một máy trung gian. Kết nối tới máy đích được mã hóa từ đầu tới cuối từ laptop của bạn, máy trung gian chỉ chuyển byte.- Deploy key riêng, chỉ đọc, cho từng repo trên máy chủ.
- Nếu buộc phải forward agent, dùng
ssh-add -cđể mỗi lần ký đều hỏi xác nhận.
Chứng chỉ SSH
authorized_keys và known_hosts hợp với vài máy chủ và vài người. Với hàng trăm máy và hàng chục người, việc thêm, xóa và kiểm tra dấu vân tay bằng tay không làm nổi. Chứng chỉ SSH đưa CA vào SSH:
- Một CA nội bộ ký khóa của người dùng, kèm tên đăng nhập và hạn dùng ngắn (ví dụ 8 giờ). Máy chủ chỉ cần tin khóa công khai của CA (
TrustedUserCAKeys), không cần danh sách từng người. - CA cũng ký khóa máy chủ, nên máy khách tin mọi máy chủ do CA ký (
@cert-authoritytrongknown_hosts), và không còn câu hỏi lần đầu.
Người nghỉ việc thì chứng chỉ của họ tự hết hạn trong vài giờ. Đây là cách các công ty lớn quản lý SSH.
Gia cố máy chủ
Danh sách cho một VPS như máy chủ của trang này, theo thứ tự đáng làm:
| Việc | Cấu hình | Vì sao |
|---|---|---|
| Chỉ đăng nhập bằng khóa | PasswordAuthentication no, KbdInteractiveAuthentication no |
Chặn toàn bộ việc dò mật khẩu |
| Không đăng nhập thẳng bằng root | PermitRootLogin no (hoặc prohibit-password) |
Thêm một bước, và log biết ai đã làm gì |
| Giới hạn người được SSH | AllowUsers deploy |
Tài khoản dịch vụ không mở được shell |
| Tường lửa | Chỉ mở 22, 80, 443; tốt hơn nữa là chỉ cho phép SSH từ IP của mình | Giảm bề mặt tấn công |
| Chặn dò tự động | fail2ban, hoặc MaxAuthTries |
Giảm nhiễu trong log |
| Cập nhật | Bật cập nhật bảo mật tự động | Phần lớn lỗ hổng bị khai thác đã có bản vá từ trước |
| Không để lộ cổng database, Docker | Chỉ lắng nghe trên 127.0.0.1 hoặc mạng nội bộ |
Bài 9 |
Đổi cổng SSH sang số khác 22 làm log yên tĩnh hơn hẳn, nhưng không phải biện pháp bảo mật: một lần quét cổng là ra. Hãy làm nó sau các dòng trên, không phải thay cho chúng.
Một quy tắc khi sửa cấu hình SSH từ xa: giữ nguyên một phiên đang mở, áp cấu hình mới (sshd -t để kiểm tra cú pháp trước), rồi mở một phiên thứ hai để thử. Nếu cấu hình sai, phiên đầu vẫn còn để sửa. Bỏ qua bước này là cách nhanh nhất để tự khóa mình ngoài máy chủ.
Tóm lại
- SSH gồm ba bước: trao đổi khóa (khóa tạm, forward secrecy), xác thực máy chủ (chữ ký bằng khóa máy chủ), xác thực người dùng (chữ ký bằng khóa của bạn).
- Kết nối thật tới GitHub: mlkem768x25519-sha256 (lai hậu lượng tử), khóa máy chủ ed25519, chacha20-poly1305, và chỉ nhận đăng nhập bằng khóa.
- Không có CA, nên SSH dùng TOFU. Lần đầu phải so dấu vân tay qua một kênh khác. Khóa đổi thì tìm hiểu lý do trước khi xóa
known_hosts. - Đăng nhập bằng khóa: máy chủ chỉ nhận một chữ ký gắn với phiên, nên máy chủ giả không lấy được gì dùng lại được. Dùng ed25519, đặt passphrase, mỗi thiết bị một khóa.
- Agent forwarding cho root trên máy kia dùng khóa của bạn. Ưu tiên
ProxyJumphoặc deploy key. - Gia cố máy chủ: tắt mật khẩu, không cho root đăng nhập,
AllowUsers, tường lửa, cập nhật, và luôn giữ một phiên mở khi sửasshd_config.
Bài cuối ghép tất cả lại trên chính trang này: mô hình mối đe dọa, đi qua từng lớp từ Cloudflare tới file .env, những lỗ hổng web phổ biến, các việc còn thiếu, và một danh sách kiểm tra.