Khi bạn vào một trang HTTPS, trình duyệt xác minh chứng chỉ của server để chắc chắn "đúng là ngân hàng, không phải kẻ giả mạo". Nhưng chiều ngược lại thì sao — server có biết bạn là ai không? Trong HTTPS thường thì không: server chấp nhận mọi client, rồi mới dựa vào mật khẩu hoặc token để biết danh tính. mTLS (mutual TLS) lật nốt nửa còn lại: server cũng đòi client trình chứng chỉ hợp lệ, ngay ở tầng TLS, trước cả khi HTTP bắt đầu. Bài này (phần 15 loạt Mật mã) tự dựng một CA nội bộ bằng openssl rồi đo thật ba tình huống để thấy chính xác mTLS chặn ai và cho ai qua.
TLS thường vs mTLS: khác ở một nửa
- TLS thường (HTTPS): chỉ client xác minh server. Server trình cert do một CA công cộng ký; client kiểm cert đó. Server không biết client là ai qua TLS — nó dựa vào lớp ứng dụng (đăng nhập, API key) để nhận dạng.
- mTLS: cả hai bên xác minh nhau. Ngoài việc client kiểm server như thường, server cũng đòi client trình một cert do CA mà server tin cậy ký. Không có cert hợp lệ thì handshake đứt — kẻ lạ không chạm được tới cả tầng HTTP.
Điều này khiến mTLS đặc biệt hợp cho giao tiếp giữa các dịch vụ (service-to-service), kiến trúc zero-trust: mỗi dịch vụ có danh tính mật mã, không dịch vụ nào tin nhau chỉ vì "cùng mạng nội bộ".
# CA nội bộ tự tạo
openssl req -x509 -new -key ca.key -subj "/CN=CA-noi-bo" -out ca.crt
# cert client, ký bởi CÙNG CA
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -out client.crt
# server BẮT BUỘC client cert: -Verify 1 -CAfile ca.crt
openssl s_server -cert server.crt -key server.key -CAfile ca.crt -Verify 1
openssl s_client -connect host:8443 -cert client.crt -key client.key

Hình 1: TLS thường chỉ client xác minh server; mTLS thêm chiều server đòi client cert. Dựng bằng một CA nội bộ ký cả hai phía; -Verify 1 bắt buộc client trình cert. Danh tính đến từ chữ ký CA, không từ tên.
Đo thật: ba tình huống, ba kết cục
Mình tạo một CA nội bộ, ký cert cho server và cho một client hợp lệ, rồi tạo thêm một cert giả cho kẻ lạ: cùng tên CN=dich-vu-thanh-toan nhưng ký bởi một CA khác. Server chạy bằng openssl s_server -Verify 1 (bắt buộc client cert). Kết quả:

Hình 2: Chạy thật — client hợp lệ: Verify return code: 0 (ok), Cipher TLS_AES_256_GCM_SHA384; client không cert: tlsv13 alert certificate required, server ghi "peer did not return a certificate"; client giả (cùng CN, CA khác): server ghi verify error:num=20:unable to get local issuer certificate.
- Client hợp lệ → bắt tay thành công.
Verify return code: 0 (ok)và kênh mã hoáTLS_AES_256_GCM_SHA384thiết lập. Cả hai phía đã xác minh nhau. - Client không trình cert → bị từ chối. Server phát cảnh báo TLS
certificate required(alert 116) và ghi log "peer did not return a certificate". Handshake đứt ngay — request HTTP còn chưa kịp gửi. - Client giả (cùng CN, CA khác) → bị từ chối. Đây là điểm quan trọng nhất: kẻ lạ đặt đúng cái tên
dich-vu-thanh-toanvào cert của mình, nhưng vì ký bởi một CA server không tin cậy, server báoverify error:num=20:unable to get local issuer certificatevà chặn. Danh tính không nằm ở cái tên trong cert — nó nằm ở chữ ký của CA.
Vì sao "cùng tên nhưng khác CA" bị chặn là điểm cốt lõi
Nhiều người nghĩ cert xác thực bằng trường CN/Subject. Sai. Ai cũng tạo được một cert ghi bất kỳ tên nào — mình vừa làm đúng vậy. Cái server thật sự kiểm là: chuỗi chứng chỉ này có dẫn ngược về một CA mà tôi tin cậy không? Chữ ký số của CA (dùng khoá riêng của CA mà chỉ CA có) là thứ không giả được. Kẻ tấn công không có khoá riêng của CA nội bộ, nên không thể tạo cert được CA đó ký — dù có sao chép y hệt tên tuổi.
Đây chính là mô hình tin cậy của cả web (chỉ khác: web dùng CA công cộng, mTLS nội bộ dùng CA riêng của bạn). Hiểu điều này giúp tránh lỗi bảo mật kinh điển: đừng bao giờ xác thực client chỉ bằng cách đọc CN mà bỏ qua kiểm chuỗi CA.
Đánh đổi cần cân nhắc
mTLS mạnh nhưng gánh nặng vận hành lớn — quản lý vòng đời cert là bài toán thật. Mỗi client cần một cert, cert có hạn và phải xoay (rotate), và bạn phải có cách thu hồi khi một khoá bị lộ (CRL hoặc OCSP). Với hàng trăm dịch vụ, việc này cần tự động hoá (như SPIFFE/SPIRE, hoặc service mesh kiểu Istio/Linkerd cấp và xoay cert tự động). Bật mTLS thủ công cho hệ lớn mà không có tự động hoá là tự chuốc nợ vận hành.
mTLS xác thực máy/dịch vụ, thường không thay xác thực người dùng. Cert nằm trên máy; nó chứng minh "dịch vụ này là dịch vụ kia", rất hợp cho backend gọi backend. Nhưng phân phát và bảo vệ cert trên thiết bị của từng người dùng cuối khó hơn nhiều — với người dùng, mật khẩu/OAuth/passkey thường thực tế hơn. Dùng mTLS đúng chỗ: nội bộ, giữa các dịch vụ.
CA nội bộ phải được bảo vệ như tài sản quý nhất. Khoá riêng của CA ký được mọi cert client hợp lệ — lộ nó là kẻ tấn công tự cấp cho mình quyền vào mọi dịch vụ. Giữ khoá CA offline hoặc trong HSM, giới hạn nghiêm ngặt ai ký được cert.
Ba ý mang về
- mTLS là xác thực hai chiều ở tầng TLS: khác HTTPS thường (chỉ client kiểm server), server cũng đòi client trình cert hợp lệ. Đo thật, client hợp lệ bắt tay thành công (
Verify return code: 0), client không cert bị từ chối ngay (certificate required) trước cả HTTP. - Danh tính đến từ chữ ký CA, không từ tên: đo thật, cert giả cùng CN
dich-vu-thanh-toannhưng ký bởi CA khác bị chặn (unable to get local issuer certificate) — vì server kiểm chuỗi CA tin cậy, không đọc suông cái tên. - Sức mạnh đi kèm gánh nặng vận hành: mTLS hợp cho giao tiếp dịch vụ zero-trust, nhưng phải quản lý vòng đời cert (xoay, thu hồi qua CRL/OCSP) và bảo vệ khoá CA nội bộ như tài sản quý nhất — với hệ lớn cần tự động hoá.
Nguồn
- Cloudflare — What is mutual TLS (mTLS)?: https://www.cloudflare.com/learning/access-management/what-is-mutual-tls/
- RFC 8446 §4.4.2 — TLS 1.3: Certificate (client authentication): https://datatracker.ietf.org/doc/html/rfc8446#section-4.4.2
- OpenSSL — s_server / s_client: https://docs.openssl.org/master/man1/openssl-s_server/
Phần sau ta bàn một việc mọi hệ thật đều phải làm nhưng hay bị bỏ quên: xoay khoá (key rotation) — vì sao không khoá nào nên sống mãi, và cách xoay khoá mà không làm gián đoạn dịch vụ hay mất dữ liệu cũ.