Nhiều người nghĩ về HTTPS như một cái công tắc: bật lên là "an toàn", tắt đi là "không an toàn". Thực tế TLS có hai phần rất khác nhau, và gộp chúng làm một là chỗ hiểu lầm bắt đầu. Phần thứ nhất là chi phí: bật TLS tốn thêm bao nhiêu thời gian cho mỗi kết nối? Phần thứ hai, quan trọng hơn nhiều, là cấu hình — cùng một chữ "HTTPS" trên thanh địa chỉ có thể là một máy chủ chỉ nói TLS 1.3 với cipher 256-bit, cũng có thể là một máy chủ vẫn chấp nhận TLS 1.0 và một chứng chỉ chẳng ai ký. Cái khoá xanh không phân biệt hai thứ đó; nó chỉ nói "đường truyền có mã hoá", không nói "mã hoá mạnh" hay "đúng người".

Bài này đo cả hai. Tôi dựng một máy chủ TLS thật bằng openssl s_server trong container dùng một lần, đo thời gian bắt tay, rồi thử ba kiểu cấu hình sai để xem cái nào bị chặn và cái nào lọt. Mỗi phép đo lặp 3 lần. Không nhắm vào hệ thống của ai — mọi thứ chạy trên localhost trong container tự dựng rồi tự xoá.

Phần 1: bắt tay TLS tốn bao nhiêu?

Tôi cho client mở kết nối TCP tới openssl s_server, bấm giờ đúng đoạn wrap_socket — tức chỉ đo phần bắt tay TLS, không tính thời gian gửi dữ liệu. Lặp 20 lần rồi lấy trung vị, và cả bộ đo lặp 3 lần.

thoi gian bat tay TLS trung vi: 0.68 ms   (lan 1)
                                0.93 ms   (lan 2)
                                0.94 ms   (lan 3)
giao thuc thuong luong: TLSv1.3
cipher: TLS_AES_256_GCM_SHA384 (256 bit)

Chưa tới một mili giây cho mỗi lần bắt tay, trên máy cùng nhà (client và server cùng container, không có độ trễ mạng). Con số này quan trọng vì nó bác bỏ cái lý do hay được viện ra để không bật HTTPS: "TLS làm chậm trang". Trên đường truyền thật sẽ có thêm độ trễ khứ hồi — TLS 1.3 cần một vòng khứ hồi để bắt tay, TLS 1.2 cần hai — nhưng chi phí tính toán của bản thân việc bắt tay thì nhỏ như trên. Và đây là điểm mấu chốt: bắt tay chỉ xảy ra một lần cho mỗi kết nối. Một trang web tải hàng chục tài nguyên qua cùng một kết nối (HTTP keep-alive, hay ghép luồng của HTTP/2) chỉ bắt tay một lần rồi dùng lại; thêm cơ chế nối lại phiên (session resumption) thì lần sau còn rẻ hơn nữa. Nói cách khác: chi phí TLS là một khoản trả một lần, không phải khoản thu trên từng request — ai đo throughput bằng cách mở kết nối mới cho mỗi request rồi kết luận "TLS đắt" là đang đo sai thứ.

Phần 2: máy chủ có từ chối giao thức cũ không?

Đây mới là phần dễ sai. Tôi cấu hình s_server chỉ bật TLS 1.2 và 1.3, rồi cho client cố tình ép từng phiên bản giao thức:

tls1   (TLS 1.0): no protocols available  -> bi tu choi
tls1_1 (TLS 1.1): no protocols available  -> bi tu choi
tls1_2 (TLS 1.2): bat tay duoc
tls1_3 (TLS 1.3): bat tay duoc

Kết quả này giống hệt qua cả 3 lần đo. TLS 1.0 và 1.1 bị từ chối thẳng với thông báo no protocols available — hai bên không tìm được phiên bản chung nào, bắt tay dừng ngay. Đó là hành vi đúng: TLS 1.0 (năm 1999) và 1.1 (2006) đã bị khai tử vì cho phép những cipher yếu và dính các đòn như BEAST, POODLE; trình duyệt hiện đại bỏ hẳn từ 2020. Nhưng "đúng" ở đây là nhờ tôi đã cấu hình đúng. Một máy chủ để nguyên cấu hình mặc định cũ, hoặc một người quản trị bật lại TLS 1.0 "cho tương thích với khách hàng dùng máy cũ", sẽ cho cái bắt tay đó lọt qua — và cái nguy hiểm là không có lỗi nào hiện ra: trang vẫn chạy, khoá vẫn xanh, chỉ có điều nó chấp nhận một cuộc thương lượng mà kẻ đứng giữa có thể ép tụt xuống mức yếu nhất. Cách duy nhất để biết là chủ động kiểm tra. Muốn kiểm nhanh một máy chủ (của chính bạn):

# thu ep TLS 1.0 - phai bi tu choi
echo | openssl s_client -connect example.com:443 -tls1 2>&1 | grep -i protocol

Nếu nó bắt tay được, đó là một phát hiện cần sửa.

Phần 3: chứng chỉ tự ký — ai từ chối, ai lọt?

Phần cuối là về danh tính. TLS làm hai việc: mã hoá đường truyền, và chứng minh "tôi đúng là máy chủ bạn muốn nói chuyện". Việc thứ hai dựa vào chứng chỉ được một CA (tổ chức chứng thực) đáng tin ký; chứng chỉ tự ký thì không có ai ký cả — nó mã hoá được, nhưng không chứng minh được danh tính. Tôi cho client kết nối tới máy chủ dùng chứng chỉ tự ký, theo hai cách:

client mac dinh (kiem CA):  TU CHOI - CERTIFICATE_VERIFY_FAILED
client TAT kiem tra:        chap nhan (nhung mo cua MITM)

Client mặc định — trình duyệt, curl, thư viện HTTP chuẩn — kiểm chứng chỉ và từ chối cái tự ký với lỗi CERTIFICATE_VERIFY_FAILED. Đây chính là trang cảnh báo đỏ "Kết nối của bạn không riêng tư" mà ai cũng từng gặp: nó khó chịu, nhưng đang làm đúng việc — nó không có cách nào biết chứng chỉ tự ký này là của máy chủ thật hay của kẻ đứng giữa. Vế thứ hai mới là bài học đắt. Khi lập trình viên gặp lỗi chứng chỉ trong lúc phát triển, phản xạ thường thấy là tắt kiểm tra đi cho xong:

# Python
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
curl --insecure ...      # hay -k

Đúng là hết lỗi thật, và kết nối vẫn được mã hoá. Nhưng bạn vừa vứt bỏ toàn bộ khả năng chống kẻ-đứng-giữa (MITM): một kẻ chen được vào đường truyền — Wi-Fi công cộng, DNS bị bẻ, proxy độc hại — chỉ cần chìa ra chứng chỉ tự ký của chính hắn, và client đã tắt kiểm tra sẽ vui vẻ nhận, mã hoá dữ liệu bằng khoá của kẻ tấn công rồi gửi thẳng cho hắn. Mã hoá mà không xác minh danh tính là khoá cửa nhà rồi đưa chìa cho người lạ đứng ngoài tự nhận là chủ nhà. Cái bẫy là dòng CERT_NONE đó hay bị viết trong lúc dev rồi theo bài lên production — nó không gây lỗi, không có cảnh báo, test vẫn xanh, cho tới ngày có người thật sự đứng giữa. Nếu môi trường dev cần chứng chỉ riêng, cách đúng là thêm CA nội bộ vào danh sách tin cậy, chứ không phải tắt kiểm tra cho tất cả.

Ba con số, một mạch suy nghĩ: (1) bắt tay rẻ và trả một lần (~0,7–0,9 ms) — đừng lấy "TLS chậm" làm cớ, trừ khi bạn đo bằng cách mở kết nối mới cho mỗi request nghĩa là đang tự làm hỏng phép đo; (2) giao thức cũ phải bị từ chối — TLS 1.0/1.1 lọt qua là một lỗ hổng im lặng, chỉ chủ động kiểm mới thấy; (3) kiểm chứng chỉ là phần không được tắt — mã hoá bảo vệ nội dung khỏi bị đọc, xác minh chứng chỉ bảo vệ bạn khỏi nói chuyện nhầm người, bỏ vế sau thì vế đầu thành vô nghĩa.

Mẫu số chung

Hai ý cốt lõi ở đây — mã hoá không phải xác thực (một kênh mã hoá tới nhầm người vẫn là mất trắng), và mặc định đã an toàn, nguy hiểm là hành động tắt nó đi — lặp lại y hệt ở mọi client TLS, chỉ đổi tên cái công tắc opt-out.

Trong thế giới Java, HttpsURLConnection và SSLContext mặc định kiểm chứng chỉ và tên miền; cái anti-pattern kinh điển là tự viết một X509TrustManager "tin tất cả" (checkServerTrusted để trống) hay một HostnameVerifier luôn trả true để "hết lỗi lúc dev" — đúng dòng CERT_NONE của Java, và nó cũng theo bài lên production y hệt. Muốn dev với CA riêng thì nạp CA đó vào truststore, đừng vô hiệu verifier.

Trong Go, crypto/tls mặc định xác minh đầy đủ, và công tắc opt-out là tls.Config{InsecureSkipVerify: true} — cái tên đã tự cảnh báo "insecure" mà vẫn bị dán khắp nơi để qua lỗi chứng chỉ; cùng lời khuyên: thêm CA nội bộ vào RootCAs, không bật InsecureSkipVerify.

Còn ở cơ sở dữ liệu, đây là cái bẫy nổi tiếng của PostgreSQL: sslmode=require mã hoá nhưng không xác minh máy chủ — tức chống nghe lén nhưng không chống MITM, đúng cảnh "mã hoá mà không xác thực"; muốn an toàn thật phải sslmode=verify-full (kiểm cả chứng chỉ lẫn tên máy chủ). Rất nhiều chuỗi kết nối dừng ở require và tưởng thế là đủ, y như client tắt kiểm chứng chỉ.

Mẫu số chung: mã hoá và xác thực là hai việc tách rời — mã hoá giấu nội dung khỏi người ngoài, xác thực bảo đảm bạn đang nói với đúng người; một kênh mã hoá tới nhầm người thì kẻ tấn công đọc trọn, nên mã hoá mà bỏ xác minh danh tính là vô nghĩa. Và mọi client hiện đại đã xác minh mặc định — lỗ hổng gần như luôn là một dòng ai đó chủ động tắt nó đi. Vì thế câu hỏi đúng khi thấy một kết nối "đã mã hoá" không phải "có mã hoá chưa" mà là "nó có xác minh danh tính đầu kia không, hay tôi (hoặc thư viện) đã tắt cái đó ở đâu đó" — vì đó, không phải chuyện mã hoá, mới là chỗ MITM chui vào.

Muốn tự kiểm trong ba mươi giây, trỏ vào một máy chủ của chính bạn:

# 1. Xem no thuong luong giao thuc va cipher gi
echo | openssl s_client -connect your-server:443 2>/dev/null | grep -iE "protocol|cipher"

# 2. Thu ep TLS 1.0 - phai thay 'no protocols available'
echo | openssl s_client -connect your-server:443 -tls1 2>&1 | grep -iE "protocol|no protocols"

Nếu dòng đầu ra TLSv1.3 với một cipher GCM, và dòng sau báo no protocols available — máy chủ của bạn đang làm đúng. Nếu TLS 1.0 bắt tay được, bạn vừa tìm ra việc cần làm chiều nay.