SSD Nodes Learn 🎉 VPS từ $5.50/tháng
Hướng dẫn Matt ConnorBởi Matt Connor

Cấu hình mTLS cho nginx bằng client certificate

Dùng openssl tạo private CA, cấp client certificate riêng và thêm 3 directive vào nginx để khóa admin panel, từ chối client thiếu hoặc sai certificate.

mTLS làm gì

Mutual TLS, thường viết là mTLS, buộc nginx yêu cầu mọi client gửi certificate và từ chối request khi certificate đó bị thiếu hoặc không được cấp bởi certificate authority (CA) do bạn kiểm soát. Việc kiểm tra diễn ra trong quá trình TLS (transport layer security) handshake, nên caller không có client certificate hợp lệ sẽ không bao giờ truy cập được ứng dụng của bạn. Đây là điểm hữu ích của mTLS: admin panel hoặc metrics endpoint có thể hoạt động trên public internet mà không cần trang đăng nhập và không để bot có gì để đoán.

Cấu hình này khá nhỏ. Bạn cần một private CA được tạo bằng openssl, một certificate cho mỗi người và ba directive trong nginx server block. Việc quyết định cấu hình này có duy trì ổn định trong một năm hay không nằm ở khâu vận hành, nên phần lớn hướng dẫn này tập trung vào thời hạn hiệu lực, việc thu hồi, certificate riêng cho từng người và cách xử lý khi client bị từ chối nhưng không ai biết nguyên nhân.

Hai chuỗi, không phải một

Trong cấu hình mTLS có hai chuỗi chứng chỉ và chúng hoàn toàn độc lập với nhau. Gộp chúng lại là sai lầm đầu tiên mà gần như ai cũng mắc phải.

Chuỗi đầu tiên là chuỗi của server. VPS của bạn cung cấp chứng chỉ cho admin.example.com do một CA công khai như Let's Encrypt cấp, còn trình duyệt kiểm tra chứng chỉ đó dựa trên kho root đi kèm hệ điều hành. mTLS không thay đổi phần này. Nếu certbot đã cấp chứng chỉ đó cho bạn, hãy giữ nguyên như hiện tại: xem cấp chứng chỉ Let's Encrypt cho nginx bằng certbot.

Chuỗi thứ hai là chuỗi của client. Bạn tự tạo một CA nhỏ, ký một chứng chỉ cho từng người cần truy cập, rồi yêu cầu nginx chỉ tin cậy CA đó khi kiểm tra client. Không có kho root công khai nào biết CA của bạn và cũng không cần biết. Bên duy nhất phải tin cậy CA đó là nginx, thông qua file ssl_client_certificate.

Vì vậy, ssl_client_certificate không bao giờ ảnh hưởng đến chứng chỉ nginx cung cấp, còn chuỗi Let's Encrypt không ảnh hưởng đến việc client nào được phép truy cập. Trỏ ssl_client_certificate đến fullchain.pem không có tác dụng như bạn nghĩ: directive đó chỉ định các issuer mà chứng chỉ client có thể được cấp bởi, tức là đầu bên kia của kết nối. Việc khiến server tự tin cậy CA của bạn cho các hoạt động outbound của chính nó là một công việc riêng, được trình bày trong thêm CA riêng vào trust store của Ubuntu, và trust store của hệ thống không phải nơi nginx đọc khi xác minh client.

Tự tạo client CA bằng openssl

Tạo CA ở một nơi khác với web server. nginx chỉ cần public certificate của CA. Private key của CA dùng để ký các client certificate mới. Vì vậy, nếu để key này trên một máy có thể truy cập từ Internet, chỉ một lần bị breach cũng đủ để kẻ tấn công tự tạo các client hợp lệ theo ý muốn.

mkdir -p ~/client-ca/certs ~/client-ca/newcerts ~/client-ca/private ~/client-ca/csr
cd ~/client-ca
chmod 700 private
touch index.txt
echo 1000 > serial
echo 1000 > crlnumber

index.txt, serialcrlnumber là cơ sở dữ liệu của CA. openssl ca sẽ không chạy nếu thiếu các file này. Chúng cũng giúp thực hiện việc thu hồi sau này, vì revocation list chứa các serial number. Do đó, CA phải ghi nhớ serial nào đã cấp cho ai.

Tạo ~/client-ca/openssl.cnf. Đặt dir thành đường dẫn thực tế của thư mục đó, vì openssl ca không mở rộng ~.

[ ca ]
default_ca = client_ca

[ client_ca ]
dir               = /home/you/client-ca
database          = $dir/index.txt
new_certs_dir     = $dir/newcerts
certificate       = $dir/ca.crt
private_key       = $dir/private/ca.key
serial            = $dir/serial
crlnumber         = $dir/crlnumber
default_md        = sha256
default_days      = 365
default_crl_days  = 30
policy            = policy_loose
rand_serial       = no
unique_subject    = no
email_in_dn       = no

[ policy_loose ]
commonName              = supplied
countryName             = optional
stateOrProvinceName     = optional
organizationName        = optional
organizationalUnitName  = optional
emailAddress            = optional

[ client_ext ]
basicConstraints        = CA:FALSE
keyUsage                = critical, digitalSignature, keyEncipherment
extendedKeyUsage        = clientAuth
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid,issuer

Bây giờ tạo key của CA và certificate tự ký:

openssl genrsa -aes256 -out private/ca.key 4096
chmod 600 private/ca.key
openssl req -x509 -new -key private/ca.key -sha256 -days 3650 \
  -subj "/O=Example Ops/CN=Example Ops Client CA" \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign" \
  -out ca.crt

-aes256 đặt passphrase cho CA key, nên mỗi lần ký đều yêu cầu nhập passphrase. Đó chính là mục đích của tùy chọn này. Kiểm tra kết quả:

openssl x509 -in ca.crt -noout -subject -dates -ext basicConstraints

Subject phải là CA của bạn và thời hạn phải là 10 năm. Dòng extension phải là CA:TRUE, pathlen:0. pathlen:0 có nghĩa là CA này được phép ký các end certificate nhưng không được ký một CA khác. Nhờ đó, chain luôn chỉ có đúng một cấp và bạn có thể để ssl_verify_depth nguyên như cũ.

Cấp một client certificate cho từng người

Mỗi người một certificate. Không dùng một certificate chung cho cả team, vì không thể revoke certificate chung mà không khóa quyền truy cập của tất cả mọi người. Certificate chung cũng không cho biết ai đã thực hiện request.

openssl genrsa -out private/alice.key 2048
openssl req -new -key private/alice.key -out csr/alice.csr \
  -subj "/O=Example Ops/CN=alice"
openssl ca -config openssl.cnf -extensions client_ext \
  -days 365 -notext -in csr/alice.csr -out certs/alice.crt

openssl ca in certificate mà nó sắp ký, yêu cầu passphrase của CA, yêu cầu xác nhận hai lần, rồi thêm một dòng vào index.txt. Thêm -batch khi script hóa thao tác này. Phần client_ext quan trọng vì có một dòng trong đó: extendedKeyUsage = clientAuth. Certificate chỉ có extended key usage là serverAuth sẽ bị từ chối vì không phù hợp để client authentication. Vì vậy, hãy khai báo rõ mục đích thay vì bỏ mặc.

Xác minh cặp này với CA trước khi bàn giao:

openssl verify -CAfile ca.crt certs/alice.crt

Lệnh này in ra certs/alice.crt: OK. Nếu có output khác, certificate và CA không khớp. Không cấu hình nginx nào có thể khắc phục lỗi này.

Gộp key và certificate vào một file mà browser có thể import:

openssl pkcs12 -export -inkey private/alice.key -in certs/alice.crt \
  -name "alice at example ops" -out alice.p12

Lệnh export yêu cầu password để bảo vệ file trong quá trình truyền. Gửi file và password qua hai kênh khác nhau. Bàn giao .p12 thay vì chỉ gửi .key. Bạn có thể thêm -certfile ca.crt để đưa CA vào bundle, nhưng nginx không cần phần này: nginx đã có ca.crt, nên certificate được CA đó ký trực tiếp sẽ tự xác minh được.

OpenSSL 3, phiên bản được Ubuntu 24.04 phát hành, ghi các file PKCS#12 bằng cơ chế mã hóa hiện tại. Các browser và hệ điều hành đang được sử dụng vào August 2026 đều đọc được định dạng này. Nếu một importer cũ từ chối file, hãy export lại với -legacy. Tùy chọn này chuyển về các thuật toán cũ mà importer đó yêu cầu. Hãy đọc thông báo do importer đưa ra trước khi dùng flag này.

Cấu hình nginx với ssl_client_certificate và ssl_verify_client

Sao chép chứng chỉ CA, và chỉ chứng chỉ CA, lên server.

scp ca.crt user@admin.example.com:/tmp/client-ca.crt
ssh user@admin.example.com \
  'sudo install -o root -g root -m 644 /tmp/client-ca.crt /etc/nginx/client-ca.crt'

Mode 644 là đúng trong trường hợp này. Chứng chỉ CA là thông tin công khai. CA key vẫn nằm trên workstation của bạn.

Sau đó thêm 3 directive vào server block đã thực hiện TLS termination:

server {
    listen 443 ssl;
    server_name admin.example.com;

    ssl_certificate     /etc/letsencrypt/live/admin.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem;

    ssl_client_certificate /etc/nginx/client-ca.crt;
    ssl_verify_client      on;
    ssl_verify_depth       1;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

ssl_verify_depth 1 là giá trị mặc định của nginx. Nó yêu cầu client certificate phải được CA trong file đó ký trực tiếp. Chỉ tăng giá trị này nếu bạn thêm một intermediate. nginx cũng gửi các subject name từ ssl_client_certificate cho client trong quá trình handshake. Nhờ đó, browser biết phải cung cấp certificate nào trong số các certificate của nó. Đây là lý do dùng ssl_client_certificate thay vì ssl_trusted_certificate. Hai directive này xác minh theo cùng cách, nhưng ssl_trusted_certificate không gửi danh sách certificate.

Ubuntu 24.04 phát hành kèm nginx 1.24. Với phiên bản này, HTTP/2 được khai báo trên dòng listen bằng listen 443 ssl http2;. Trên nginx 1.25.1 trở lên, cú pháp đó đã deprecated và HTTP/2 có directive riêng là http2 on;. Cả hai cách đều không thay đổi việc kiểm tra certificate.

Reload nginx rồi đọc kết quả:

sudo nginx -t && sudo systemctl reload nginx
curl -i https://admin.example.com/

nginx -t in ra syntax is oktest is successful. Lệnh gọi curl không gửi certificate, nên kết quả phải là 400 Bad Request với body No required SSL certificate was sent. Đây là nginx từ chối request ngay tại lớp kiểm soát của nó. Điều đó cho biết cấu hình đã có hiệu lực và application chưa được gọi. Bây giờ thử đúng cách:

curl --cert certs/alice.crt --key private/alice.key https://admin.example.com/

Kết quả phải trả về nội dung mà application của bạn cung cấp.

Vì sao quy tắc kiểm soát phải nằm trong server block

Certificate được trao đổi trong quá trình TLS handshake, trước khi nginx đọc request line. Vì vậy, tại thời điểm đó nginx chưa biết request sẽ đi vào location nào. Đặt ssl_verify_client on; bên trong location sẽ yêu cầu client renegotiate giữa chừng kết nối. TLS 1.3 đã loại bỏ renegotiation và HTTP/2 cấm tính năng này, nên trên stack hiện tại, mẫu cấu hình đó sẽ fail thay vì hiển thị prompt.

Bạn phải tự xác định phạm vi áp dụng. Yêu cầu certificate ở cấp server, sau đó quyết định theo từng location:

ssl_verify_client optional;

location /metrics {
    if ($ssl_client_verify != SUCCESS) { return 403; }
    proxy_pass http://127.0.0.1:9090;
}

location /healthz {
    proxy_pass http://127.0.0.1:8080;
}

$ssl_client_verify chứa SUCCESS, hoặc NONE nếu client không gửi gì, hoặc FAILED: kèm theo lý do. Với optional, nginx yêu cầu certificate và chỉ verify nếu client có gửi. Nhờ đó, path public /healthz ở trên vẫn hoạt động trong khi /metrics bị chặn. Certificate được gửi nhưng fail verification vẫn bị nginx từ chối tại bước đó. Nếu muốn tự kiểm tra certificate bị lỗi thay vì để nginx xử lý, đó là optional_no_ca. Khi đó, bài kiểm tra của bạn phải coi mọi giá trị khác SUCCESS là từ chối.

nginx dùng các status code không theo chuẩn cho trường hợp này. error_page có thể bắt các mã đó để visitor bị từ chối nhận được giải thích thay vì chỉ thấy lỗi 400:

error_page 495 496 = @needcert;

location @needcert {
    default_type text/plain;
    return 200 "This host requires a client certificate. Ask ops for one.\n";
}

495 nghĩa là client certificate fail verification. 496 nghĩa là client không cung cấp certificate. Hãy giữ trang này ở dạng plain text, vì người đọc không có session và không có account.

Cài client certificate vào trình duyệt như thế nào?

Firefox dùng certificate store riêng: vào Settings, chọn Privacy and Security, chọn View Certificates, mở tab Your Certificates, chọn Import, rồi chọn file .p12 và nhập password của file.

Chrome và Edge dùng certificate store của hệ điều hành trên Windows và macOS. Vì vậy, mở file .p12 sẽ khởi động trình hướng dẫn import của hệ thống. Trên Linux, Chrome đọc một cơ sở dữ liệu NSS (network security services) riêng trong thư mục home của bạn. Dùng công cụ dòng lệnh là cách đáng tin cậy:

sudo apt install -y libnss3-tools
pk12util -d sql:$HOME/.pki/nssdb -i alice.p12

Sau đó tải lại site. Trình duyệt sẽ hỏi bạn muốn gửi certificate nào. Chrome ghi nhớ lựa chọn đó trong phần còn lại của phiên trình duyệt. Vì vậy, hãy restart trình duyệt khi muốn được hỏi lại. Certificate chỉ tồn tại trong một browser profile trên một máy. Certificate được import vào Firefox sẽ không xuất hiện trong Chrome, và cả hai cũng không xuất hiện trên điện thoại của bạn.

Kiểm tra bằng curl --cert

Dùng curl để debug vì nó hiển thị các thao tác đã thực hiện.

curl -v --cert certs/alice.crt --key private/alice.key https://admin.example.com/

Bạn có thể nối certificate và key vào cùng một file PEM, rồi truyền file đó bằng --cert alice.pem. Nếu key có passphrase, curl sẽ yêu cầu bạn nhập. curl cũng chấp nhận --cert alice.pem:passphrase, nhưng giá trị này sẽ nằm trong shell history, nên hãy chọn cách nhập qua prompt.

Trước khi kết luận nginx bị lỗi, hãy chạy 2 kiểm tra sau. Trước hết, certificate và key phải là một cặp:

openssl x509 -noout -pubkey -in certs/alice.crt | openssl sha256
openssl pkey -pubout -in private/alice.key | openssl sha256

Hai hash giống nhau cho biết 2 file thuộc cùng một cặp. Hai hash khác nhau nghĩa là bạn đã dùng nhầm file của 2 người khác nhau, và client sẽ không báo rõ nguyên nhân này.

Tiếp theo, server phải yêu cầu CA của bạn:

openssl s_client -connect admin.example.com:443 -servername admin.example.com </dev/null

Tìm block Acceptable client certificate CA names trong output và kiểm tra subject của CA nằm trong block đó. Nếu hoàn toàn không có block này, nginx không yêu cầu certificate trên server block đã trả lời request. Điều đó có nghĩa là các directive của bạn đã được đặt trong một server block khác, thường là default server.

Truyền CN của client cho ứng dụng

Certificate cho biết ai đã gọi, nhưng application phía sau proxy không nhìn thấy lớp TLS, nên nginx phải truyền tên đó sang.

map $ssl_client_s_dn $client_cn {
    default              "";
    "~,?CN=(?<cn>[^,]+)" $cn;
}

$ssl_client_s_dn chứa subject distinguished name ở định dạng RFC 2253, có dạng như CN=alice,O=Example Ops. Map này lấy trường CN và đưa vào $client_cn. Giữ CN ở dạng username đơn giản, vì dấu phẩy trong CN sẽ được escape ở định dạng đó và regular expression ngắn ở trên không xử lý được escape này.

location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host                 $host;
    proxy_set_header X-Forwarded-For      $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto    $scheme;
    proxy_set_header X-Client-Cert-CN     $client_cn;
    proxy_set_header X-Client-Cert-Serial $ssl_client_serial;
}

proxy_set_header thay thế mọi header cùng tên do caller gửi, nên không ai có thể giả mạo X-Client-Cert-CN thông qua location này. Có 2 điều kiện để bảo đảm điều đó. nginx chỉ kế thừa proxy_set_header từ cấp bên ngoài khi cấp hiện tại không định nghĩa header riêng. Vì vậy, một location khác có một dòng proxy_set_header sẽ âm thầm làm mất mọi header được set ở phía trên, bao gồm cả header này. Application cũng phải không thể truy cập nếu không đi qua nginx. Điều đó có nghĩa là phải bind application vào 127.0.0.1 thay vì 0.0.0.0, vì app trên một public port sẽ đọc header giả mạo trực tiếp từ Internet. Phần proxy được trình bày trong cấu hình nginx reverse proxy được giải thích theo từng dòng. Nếu application cần toàn bộ certificate thay vì chỉ tên, $ssl_client_escaped_cert chứa certificate đó ở dạng URL-encoded và an toàn để đặt trong header.

Làm cách nào để thu hồi một client certificate?

Một người nghỉ việc hoặc một laptop bị mất. Bạn chỉ cần thu hồi certificate đó, những người khác vẫn tiếp tục làm việc. Đây là lý do chính để cấp một certificate riêng cho từng người.

cd ~/client-ca
openssl ca -config openssl.cnf -revoke certs/alice.crt
openssl ca -config openssl.cnf -gencrl -out crl.pem

Lệnh đầu tiên đổi trạng thái của serial đó trong index.txt từ V thành R. Lệnh thứ hai tạo certificate revocation list (CRL), là một file được ký chứa các serial đã bị thu hồi. Đưa file này lên server và trỏ nginx đến file bằng ssl_crl /etc/nginx/client-ca.crl;, đặt cùng các directive khác.

scp crl.pem user@admin.example.com:/tmp/client-ca.crl
ssh user@admin.example.com 'sudo install -m 644 /tmp/client-ca.crl /etc/nginx/client-ca.crl && sudo nginx -t && sudo systemctl reload nginx'

Đây là lỗi có thể khiến mọi client bị chặn. CRL có ngày nextUpdate, được đặt bằng default_crl_days, trong cấu hình trên là 30. Khi quá ngày đó, OpenSSL coi danh sách đã hết hạn và làm xác minh thất bại cho mọi client certificate với CRL has expired, không chỉ certificate đã bị thu hồi. nginx đọc file khi tải cấu hình, nên CRL mới trên disk sẽ không có tác dụng cho đến khi reload. Hãy tạo lại CRL và reload theo lịch nằm an toàn trong khoảng thời gian này, chẳng hạn mỗi tuần một lần với thời hạn 30 ngày. Kiểm tra các ngày trước khi copy:

openssl crl -in crl.pem -noout -lastupdate -nextupdate

Với một số ít user, có một lựa chọn đơn giản hơn. CA thuộc quyền quản lý của bạn, nên nginx có thể từ chối trực tiếp một serial và không cần dùng CRL:

map $ssl_client_serial $revoked {
    default 0;
    "1002"  1;
}

Kết hợp cách này với if ($revoked) { return 403; } trong location. Nó không có ngày hết hạn để bạn quên. Tuy nhiên, thông tin này không được phân phối đến nơi khác, nên mọi hệ thống khác tin cậy CA của bạn sẽ không biết về nó. Nếu chỉ có một nginx đứng trước một ứng dụng, đây là lựa chọn đơn giản và đúng bản chất. Khi có nhiều hơn một điểm kiểm soát, hãy chuyển sang dùng CRL.

Thời hạn của client certificate nên là bao lâu?

Hãy đặt thời hạn cho client certificate là 1 năm, hoặc ngắn hơn nếu bạn có thể xử lý việc cấp lại thường xuyên hơn. Hết hạn là lỗi âm thầm trong trường hợp này vì không có cảnh báo trước cho người giữ certificate. Một buổi sáng, họ mở panel, nginx từ chối kết nối, còn trình duyệt mô tả việc từ chối bằng cách riêng của nó; thông báo này hiếm khi có từ “expired”. Giữ CA trong 10 năm và ghi ngày hết hạn ở nơi bạn thực sự sẽ kiểm tra. Khi CA certificate hết hạn, mọi certificate do CA đó cấp sẽ ngừng được xác minh trong cùng ngày.

Hai lệnh sau giúp bạn chủ động kiểm tra:

openssl x509 -in certs/alice.crt -noout -subject -serial -enddate
awk -F'\t' '{print $1, $2, $4}' ~/client-ca/index.txt

Cột đầu tiên của index.txt là trạng thái: V khi còn hợp lệ, R khi đã bị thu hồi, E khi đã hết hạn. Cột thứ hai là thời điểm hết hạn theo định dạng YYMMDDHHMMSSZ, còn cột thứ tư là serial. File đó là bản ghi duy nhất về việc ai đang giữ certificate nào, nên hãy backup file cùng với CA key và bảo mật cả hai như secret.

Gia hạn là cấp certificate mới, không phải kéo dài certificate cũ. Hãy tạo key và CSR (certificate signing request) mới, ký certificate, bàn giao certificate mới, rồi thu hồi certificate cũ sau khi người đó xác nhận certificate mới hoạt động.

mTLS bảo vệ khỏi những gì và không bảo vệ khỏi những gì

Điều mTLS loại bỏ là khả năng truy cập mà không cần xác thực. Scanner phát hiện hostname của bạn sẽ bị từ chối trong lúc handshake, nên không bao giờ gửi HTTP request, không thấy form đăng nhập và không thể thử mật khẩu bị đánh cắp trên form đó. Credential stuffing không còn mục tiêu để thử. Lỗ hổng trong luồng đăng nhập của ứng dụng cũng không thể bị người không có certificate truy cập. mTLS còn loại bỏ shared secret mà mọi người thường dán vào chat, vì private key là một file khó vô tình sao chép.

mTLS không xử lý được client đã bị compromise. Malware trên laptop có private key, đồng thời có passphrase ngay khi chủ máy nhập nó. Đối với server, attacker đó trông hoàn toàn giống một user hợp lệ, vì certificate chỉ chứng minh quyền sở hữu một file, không chứng minh có đúng người đang sử dụng hay không. Mật khẩu .p12 và mã hóa toàn bộ disk vẫn rất quan trọng.

mTLS cũng không phải là cơ chế phân quyền. Mọi certificate hợp lệ đều truy cập được mọi thứ mà server block đó phục vụ, trừ khi bạn kiểm tra $client_cn rồi xử lý theo giá trị này. Theo mặc định, hai người giữ certificate có quyền truy cập giống hệt nhau.

mTLS chỉ bảo vệ đường truy cập qua nginx. Nếu ứng dụng cũng lắng nghe trên một public port, mTLS phía trước ứng dụng chỉ mang tính hình thức: bind ứng dụng vào 127.0.0.1 và giữ firewall đóng trên port đó. Cửa khác vào cùng máy là SSH, và cửa này cũng cần được chú ý tương tự; phần này được trình bày trong hardening quyền truy cập SSH trên VPS.

Còn một giới hạn cuối cùng, và nó sẽ gây lỗi ngay ngày bạn bật mTLS. Bất kỳ thành phần nào không thể gửi certificate đều sẽ ngừng hoạt động: uptime monitor, webhook từ payment provider, RSS reader hoặc mobile app không có certificate store mà bạn có thể truy cập. Hãy quyết định cách xử lý các thành phần đó trước khi đặt ssl_verify_client on, vì lỗi sẽ xảy ra hoàn toàn và phía các thành phần đó thường không báo gì.

Khi client bị từ chối, hãy đọc thông báo mà client hiển thị

Thông báo khi client bị từ chối phụ thuộc vào browser, phiên bản curl và TLS library bên dưới. Vì vậy, hãy đọc nội dung mà client của bạn in ra thay vì đối chiếu với một thông báo được ghi ở nơi khác. Chi tiết hữu ích nằm trên server.

sudo tail -n 50 /var/log/nginx/error.log

Chứng chỉ bị từ chối sẽ để lại một dòng chứa client SSL certificate verify error, theo sau là lý do do OpenSSL đưa ra. Bạn cần xử lý dựa trên lý do đó. Thường chỉ có một vài nguyên nhân. Chứng chỉ được cấp bởi CA khác với CA được chỉ định trong file mang tên ssl_client_certificate. Chứng chỉ đã quá hoặc chưa đến thời hạn hiệu lực. CRL trên server đã vượt quá nextUpdate, nên hiện tại mọi client đều bị lỗi thay vì chỉ một client.

Khi browser hoàn toàn không gửi certificate, vấn đề xảy ra trước bước verification. nginx gửi tên các issuer được chấp nhận trong quá trình handshake, nhưng browser không tìm thấy certificate nào khớp trong store của nó nên không có gì để gửi. Hãy import lại .p12 vào đúng profile mà bạn đang dùng để duyệt web.

Có một trường hợp khác cũng cần lưu ý. Nếu bạn kiểm tra bằng một client certificate self-signed độc lập thay vì certificate do CA của bạn ký, verification không thể thành công, vì nginx kiểm tra chữ ký dựa trên file CA còn certificate self-signed không nằm trong file đó. Cách tạo certificate giống như trong tạo certificate self-signed trên Ubuntu. mTLS chỉ cần thêm bước CA của bạn ký certificate đó.

FAQ

Tôi vẫn cần chứng chỉ Let's Encrypt nếu dùng mTLS phải không?

Có. Hai chứng chỉ này không liên quan đến nhau. Máy chủ gửi chứng chỉ của chính nó để trình duyệt tin cậy hostname, và chứng chỉ đó vẫn phải do một CA mà trình duyệt đã biết cấp. Client CA của bạn là một chuỗi riêng tư, chỉ dùng để kiểm tra ai đang kết nối. Thiết lập ssl_client_certificate không thay đổi chứng chỉ nginx gửi cho trình duyệt và không được trỏ đến chain Let's Encrypt của bạn.

Tại sao trình duyệt không bao giờ yêu cầu tôi chọn chứng chỉ?

nginx gửi danh sách các issuer được chấp nhận trong quá trình handshake. Danh sách này được tạo từ file trong ssl_client_certificate. Trình duyệt chỉ cung cấp các chứng chỉ có issuer xuất hiện trong danh sách đó. Vì vậy, không có prompt nghĩa là trình duyệt không có chứng chỉ nào từ CA của bạn: bạn đã import vào browser profile khác, hoặc chứng chỉ được ký bởi CA khác với CA đã cài trên máy chủ. Chạy openssl s_client -connect admin.example.com:443 và tìm tên CA của client certificate được chấp nhận trong output để biết máy chủ thực sự đang yêu cầu CA nào.

Tôi có thể chỉ yêu cầu client certificate trên một URL không?

Không thể dùng ssl_verify_client on bên trong một location. Chứng chỉ được trao đổi trong quá trình handshake, trước khi nginx biết request path. Cơ chế renegotiation có thể dùng để xử lý việc này đã bị loại bỏ khỏi TLS 1.3 và bị cấm trong HTTP/2. Đặt ssl_verify_client optional; trong server block, sau đó tại mỗi location cần bảo vệ, kiểm tra $ssl_client_verify và trả về 403 khi giá trị đó không phải SUCCESS.

Làm cách nào để thu hồi quyền truy cập của một người?

Thu hồi chứng chỉ đó bằng openssl ca -revoke, tạo lại danh sách bằng openssl ca -gencrl, copy danh sách lên máy chủ rồi reload nginx để nginx đọc file mới. Những người khác không bị ảnh hưởng. Cách này chỉ hoạt động khi mỗi người có chứng chỉ riêng, thay vì dùng chung một chứng chỉ. Theo dõi ngày nextUpdate của CRL, vì CRL hết hạn sẽ khiến việc xác minh thất bại với mọi client, không chỉ các client đã bị thu hồi.

mTLS có thay thế trang đăng nhập không?

Về khả năng truy cập thì có: nếu không có chứng chỉ, không có request nào đến được application, nên không có form để tấn công và không có password để đoán. Về việc xác định danh tính bên trong application thì không. Chứng chỉ chỉ chứng minh caller đang giữ một key file, vì vậy một laptop bị đánh cắp vẫn được xem là user hợp lệ. Truyền CN lên upstream, giữ nguyên các account và permission mà application đang có, và xem chứng chỉ là lớp kiểm soát ở phía trước các cơ chế đó.

#tls#mtls#nginx#openssl#access-control