---
id: networking-tls
title: "TLS certificate"
shortTitle: "TLS certificate"
track: networking
trackTitle: "Networking"
order: 5
level: core
status: published
estimatedMinutes: 35
prerequisites:
  - networking-http
next: networking-firewall
reviewedAgainst: 2026-10-15
sources:
  - title: "KodeKloud Engineer — 100 Days of DevOps"
    url: https://engineer.kodekloud.com/practice
  - title: "Let's Encrypt — How It Works"
    url: https://letsencrypt.org/how-it-works/
  - title: "ngx_http_ssl_module"
    url: https://nginx.org/en/docs/http/ngx_http_ssl_module.html
  - title: "openssl-req"
    url: https://docs.openssl.org/master/man1/openssl-req/
  - title: "RFC 5280"
    url: https://www.rfc-editor.org/rfc/rfc5280
---

# TLS certificate

Certificate chứng minh server đang giữ private key ứng với một tên miền. Nếu certificate hết hạn hoặc sai tên, trình duyệt dừng lại trước khi app kịp trả `200`.

## Để làm gì

Sau bài này, bạn đọc được ngày hết hạn và các tên trên certificate, gắn HTTPS trên Nginx bằng `listen 443 ssl`, tạo cặp tự ký bằng `openssl req -x509` cho lab, và hiểu Let's Encrypt gia hạn bằng challenge trên domain.

## Gồm những gì

- Certificate, private key, chain.
- SAN, chứ không chỉ CN.
- Nginx: `listen 443 ssl`, `ssl_certificate`, `ssl_certificate_key`. Không dùng `ssl on`.
- Lab tự ký: `openssl req -x509 -newkey rsa:2048 -noenc -days`.
- Gia hạn: certificate của Let's Encrypt có hiệu lực 90 ngày.

Bài này không nói về việc tự dựng CA nội bộ. Việc đó phụ thuộc chính sách của từng công ty nên không làm trong lab.

## Dùng khi nào

Bạn cần certificate khi bật HTTPS trên Nginx, Apache hoặc load balancer. Nếu proxy terminate TLS, certificate nằm ở proxy, còn app phía sau có thể chỉ nghe HTTP trong mạng private.

Đừng commit private key vào Git. Một khi key bị lộ, bạn phải thu hồi certificate và tạo cặp key mới. Chỉ thay file cert mà giữ key cũ thì bản copy đã lộ vẫn dùng được.

## Kiến thức cốt lõi

Server giữ private key. Certificate thì public, được một CA ký, và gắn với các tên miền trong Subject Alternative Name (SAN). Client kiểm tra ba thứ: chuỗi chữ ký dẫn tới một CA mà nó tin, tên trong SAN khớp với host nó vừa gọi, và certificate còn trong thời hạn.

Chỉ có CN như kiểu cũ là không đủ, vì client đối chiếu SAN. Certificate cấp cho `www.example.com` không tự động hợp lệ cho `example.com` nếu SAN không có tên đó.

Chain là certificate của các CA trung gian. Nếu server không gửi chain, một số client không dựng được đường tới root, trong khi trình duyệt của bạn vẫn vào bình thường vì đã cache sẵn CA trung gian.

Let's Encrypt cấp certificate miễn phí sau khi bạn chứng minh quyền kiểm soát tên miền. Với HTTP-01, bạn đặt một file trên port 80; với DNS-01, bạn thêm một bản ghi `TXT`. Agent như Certbot có thể tự sửa config web server hoặc chỉ lấy file về, tùy cách chạy. Sau 90 ngày certificate phải được cấp lại, nên lịch gia hạn phải chạy trước ngày hết hạn, đừng đợi đúng ngày.

Trên Nginx, TLS nằm ở khối `server`. `listen 443 ssl` vừa mở port vừa bật SSL trên listen đó. Module ghi rõ `ssl` là tham số của `listen`; directive `ssl on` đã obsolete. `ssl_certificate` trỏ file PEM: certificate lá trước, các trung gian sau. `ssl_certificate_key` trỏ private key. Thiếu key, sai path, hoặc file PEM đảo thứ tự thì `nginx -t` fail, worker không nạp được.

Lab chưa có CA thì tự ký bằng `openssl req`. `-x509` xuất certificate thay vì CSR. `-newkey rsa:2048` tạo key mới cùng lúc. `-keyout` và `-out` là hai file riêng. `-days` là số ngày hiệu lực; mặc định man page ghi 30 nếu bỏ. `-subj` điền subject trên dòng lệnh. `-noenc` (OpenSSL 3) không mã hóa key; `-nodes` vẫn chạy nhưng deprecated. Tự ký đủ để lab HTTPS; trình duyệt và `curl` mặc định không tin, cần `-k` hoặc cài CA nội bộ — đó không phải lỗi Nginx.

## Lệnh và code mẫu

```bash
curl.exe -vI https://example.com/ 2>&1 | findstr /C:"subject:" /C:"expire" /C:"SSL certificate"
```

Trên bash:

```bash
curl -vI https://example.com/ 2>&1 | awk 'BEGIN{IGNORECASE=1} /subject:|expire date|SSL certificate verify/'
```

curl in thông tin ngày và tên ra stderr, nên bạn phải gộp stderr vào pipe (`2>&1`).

Lab tự ký rồi gắn vào Nginx (chỉ trên VM lab, đừng dùng cho domain public):

```bash
sudo openssl req -x509 -newkey rsa:2048 -noenc -days 30 \
  -keyout /etc/nginx/ssl/lab.key \
  -out /etc/nginx/ssl/lab.crt \
  -subj "/CN=lab.example.com"
```

```nginx
server {
    listen 443 ssl;
    server_name lab.example.com;
    ssl_certificate     /etc/nginx/ssl/lab.crt;
    ssl_certificate_key /etc/nginx/ssl/lab.key;
}
```

`nginx -t` rồi `systemctl reload nginx`. `curl -vkI https://lab.example.com/` — `-k` vì certificate tự ký.

## Thực hành

1. Chạy lệnh curl tới `example.com`.
2. Ghi lại dòng subject và dòng expire.
3. Đọc khối `server` mẫu `listen 443 ssl`. Chỉ ra file cert và file key.
4. Nếu có VM lab: tạo cặp tự ký bằng `openssl req -x509`, `nginx -t`, rồi `curl -vkI`. Đừng tạo private key cho domain thật, cũng đừng đăng ký tài khoản CA trong bài này.

## Kết quả mong đợi

Output có một dòng subject chứa tên trên certificate, một dòng ngày hết hạn nằm trong tương lai, và bước verify báo thành công.

## Checklist

- [ ] SAN chứa mọi tên mà client sẽ gọi, kể cả tên không có `www` nếu người dùng gõ như vậy.
- [ ] Private key không nằm trong repo và không nằm trong image layer.
- [ ] Có lịch gia hạn chạy trước mốc 90 ngày, và có alert khi certificate còn dưới một tháng.
- [ ] Nginx: `listen 443 ssl` + `ssl_certificate` + `ssl_certificate_key`. Không viết `ssl on`.
- [ ] Lab tự ký: `openssl req -x509 -newkey` rồi `nginx -t`. `curl -k` vì client không tin self-signed.

## Thẻ ôn

### networking-tls-san
kind: distinguish
cue: Trình duyệt đối chiếu tên với SAN, không chỉ CN. Certificate cấp cho `www.example.com` không tự có hiệu lực cho `example.com` nếu SAN không ghi tên đó.

Q: Certificate được cấp cho `www.example.com`. Người dùng gõ `example.com`, không có `www`. Trình duyệt có chấp nhận không, và nó còn kiểm tra những gì ngoài cái tên?

A: Không chấp nhận nếu SAN không chứa đúng tên người dùng đã gõ. Hai tên đó không tự suy ra nhau. Client đối chiếu tên với Subject Alternative Name, kiểm tra certificate còn hạn, và đi chuỗi chữ ký lên một CA mà nó tin. Chỉ có CN kiểu cũ, không có SAN khớp, là không đủ với trình duyệt hiện nay. Cần cả hai tên thì cả hai tên phải có trong SAN lúc phát hành, hoặc dùng một certificate khác cho tên còn lại.

### networking-tls-key
kind: trap
cue: Private key lỡ vào Git thì xóa ở commit sau là chưa hết. Bản đã push vẫn còn trên remote. Thu hồi certificate và tạo cặp key mới.

Q: Private key của certificate lỡ được commit vào Git rồi push. Bạn xóa file ở commit sau. Bản trên remote còn key không, và phải làm gì với certificate? Key được phép nằm ở đâu?

A: Còn. Commit cũ vẫn trên remote, ai có lịch sử repo là đọc được key. Xóa ở đầu nhánh không thu hồi bí mật đã phát đi. Phải thu hồi certificate và tạo một cặp key mới, vì cặp cũ coi như đã lộ. Private key không nằm trong repo và không nằm trong một layer của image, vì layer đó ai kéo image là lấy được. Nếu proxy là chỗ terminate TLS, certificate và key nằm ở proxy, không copy vào từng app phía sau cho tiện.

### networking-tls-chain
kind: trap
cue: Máy bạn vẫn vào được HTTPS sau khi đổi certificate, nhưng CI fail, thường là thiếu chain. Dùng fullchain: certificate của server trước, CA trung gian sau.

Q: Bạn vừa thay certificate. Trình duyệt trên máy mình vẫn vào được, job CI gọi cùng URL thì fail vì không dựng được chuỗi tin cậy. Máy bạn và CI khác nhau chỗ nào, và file nên dùng là gì?

A: Thường là thiếu certificate trung gian. Máy bạn có thể đã gặp CA trung gian đó từ trước và còn cache, nên tự ghép được chuỗi. Môi trường CI sạch không có cache đó. Server phải gửi đủ chuỗi. Dùng file fullchain: certificate của chính server đứng trước, CA trung gian đứng sau. Chỉ gửi mỗi certificate lá thì client không có đường lên CA gốc mà nó tin.

### networking-tls-reload
kind: command
cue: Let's Encrypt đã ghi certificate mới trên đĩa mà trình duyệt vẫn thấy bản cũ: Nginx đọc file lúc start. `nginx -t` rồi reload.

Q: Gia hạn Let's Encrypt xong, file trên đĩa đã là certificate mới, nhưng trình duyệt vẫn thấy bản cũ sắp hết hạn. Nginx đang giữ cái gì, và bước nào để nó đọc file mới mà không reload mù?

A: Nginx đọc certificate lúc start hoặc lúc reload, rồi giữ trong worker đang chạy. Nó không theo dõi file trên đĩa để tự biết Let's Encrypt vừa ghi đè. Đĩa mới, bộ nhớ của worker vẫn là bản cũ, nên trình duyệt vẫn thấy certificate cũ. Chạy `nginx -t` để chắc file mới nạp được, rồi reload để worker nhận bản trên đĩa. Gia hạn mà quên reload thì đến hạn trình duyệt báo hết hạn dù file đã được cấp lại từ hôm trước.

### networking-tls-nginx-listen
kind: command
cue: Nginx HTTPS: `listen 443 ssl` + `ssl_certificate` (lá rồi trung gian) + `ssl_certificate_key`. `ssl on` đã obsolete.

Q: Bạn bật HTTPS trên Nginx. Directive nào mở port 443 kèm SSL, hai file nào phải khai, thứ tự trong file PEM là gì, và vì sao không viết `ssl on;`?

A: `listen 443 ssl` vừa listen vừa bật SSL trên listen đó. `ssl_certificate` trỏ PEM: certificate lá trước, các trung gian sau. `ssl_certificate_key` trỏ private key. Thiếu key hoặc đảo thứ tự PEM thì `nginx -t` fail. Module `ngx_http_ssl_module` ghi `ssl` là tham số của `listen`; `ssl on` đã obsolete, đừng thêm.

### networking-tls-openssl-selfsigned
kind: command
cue: Lab tự ký: `openssl req -x509 -newkey rsa:2048 -noenc -days`. `curl` mặc định không tin self-signed; dùng `-k` trên lab, không phải lỗi Nginx.

Q: Lab cần HTTPS mà chưa có CA. Lệnh `openssl req` nào tạo certificate tự ký kèm key mới, flag nào bỏ mã hóa key trên OpenSSL 3, và `curl` không `-k` fail nghĩa là Nginx sai config?

A: `openssl req -x509 -newkey rsa:2048 -keyout … -out … -days …`. `-x509` xuất certificate, không phải CSR. `-newkey` tạo key cùng lúc. `-noenc` (OpenSSL 3) ghi key không mã hóa; `-nodes` vẫn chạy nhưng deprecated. Mặc định `-days` là 30 nếu bỏ. `curl` không tin self-signed là đúng hành vi client. Fail verify không chứng minh Nginx nghe sai. Trên lab dùng `curl -k`. Đừng dùng cặp tự ký cho domain public.

## Lỗi thường gặp

**Thay certificate nhưng quên chain.** Máy bạn vẫn vào được vì đã cache CA trung gian, nhưng CI hoặc một thư viện cũ thì fail. Hãy dùng file fullchain mà nhà cung cấp đưa, theo đúng thứ tự: certificate của server trước, CA trung gian sau.

**Gia hạn xong mà Nginx vẫn dùng certificate cũ.** Nginx đọc file lúc start nên không tự biết file trên đĩa đã đổi. Sau khi gia hạn, chạy `nginx -t` rồi reload.

**`ssl on;` trong khối `server`.** Directive đã obsolete. Dùng `listen 443 ssl`.

**`curl` fail verify trên certificate tự ký rồi sửa lung tung `ssl_certificate`.** Client không tin self-signed là đúng. `nginx -t` pass thì path file ổn. Lab: `curl -k`.

## Nguồn

1. [KodeKloud Engineer — 100 Days of DevOps](https://engineer.kodekloud.com/practice) — đề cương ngày Setup SSL for Nginx. Trang practice cần đăng nhập. Bài này viết lại lệnh, không chép lab.
2. [How Let's Encrypt works](https://letsencrypt.org/how-it-works/) — thử thách và thời hạn certificate.
3. [ngx_http_ssl_module](https://nginx.org/en/docs/http/ngx_http_ssl_module.html) — `listen … ssl`, `ssl_certificate`, `ssl_certificate_key`.
4. [openssl-req](https://docs.openssl.org/master/man1/openssl-req/) — `-x509`, `-newkey`, `-noenc`, `-days`.
5. [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280) — cấu trúc certificate và chain.

## Tiếp theo

[Firewall và port](./07-firewall.md)
