Tuần này trên Hacker News có một thread hơn 600 điểm về vụ một intermediate CA của eMudhra bị compromise. Đọc comment thì thấy phần lớn dev phản ứng kiểu "ồ, chuyện của browser vendor" rồi lướt qua. Mình không nghĩ vậy. Khi một CA bị lộ key hoặc cấp cert sai, kẻ tấn công có thể có một certificate hoàn toàn hợp lệ cho domain của bạn. Browser hiện ổ khóa xanh, user không thấy gì bất thường, còn bạn thì không biết gì cho tới khi có sự cố. Tin tốt là bạn không phải ngồi chờ. Có ba việc làm được ngay hôm nay mà chẳng tốn đồng nào: theo dõi Certificate Transparency (CT) logs, khóa domain bằng CAA record, và tự kiểm tra cert chain đang chạy trên production. Bài này đi qua cả ba, mỗi phần có code chạy được.
Vì sao CA bị compromise lại là vấn đề của bạn
Mô hình tin cậy của TLS khá đơn giản: browser tin khoảng 100+ root CA, mỗi root lại ủy quyền cho nhiều intermediate CA. Bất kỳ CA nào trong chuỗi đó cũng ký được cert cho bất kỳ domain nào. Hệ thống không có cơ chế nào quy định "chỉ Let's Encrypt mới được cấp cert cho shop.vn", trừ khi bạn tự khai báo.
flowchart TD
A[Root CA trong trust store] --> B[Intermediate CA hợp lệ]
A --> C[Intermediate CA bị compromise]
B --> D[Cert thật của shop.vn]
C --> E[Cert giả của shop.vn]
E --> F[Attacker MITM / phishing]
D --> G[User]
F --> G
E -.-> H[CT Log - bắt buộc ghi lại]
H -.-> I[Bạn phát hiện qua monitoring]
Điểm mấu chốt nằm ở nhánh nét đứt. Từ năm 2018, Chrome yêu cầu mọi publicly-trusted cert phải có SCT (Signed Certificate Timestamp), nghĩa là cert phải được ghi vào CT log công khai. Safari cũng áp chính sách tương tự. Vì vậy cert giả muốn được browser chấp nhận thì gần như chắc chắn phải xuất hiện trên CT log. Bạn chỉ cần đi xem log đó.
Bước 1: Monitor CT logs cho domain của bạn
Cách đơn giản nhất là dùng crt.sh, một CT log aggregator miễn phí do Sectigo vận hành. Nó có endpoint JSON, gọi thẳng từ script được.
Dưới đây là script mình đang chạy bằng cron mỗi giờ. Script giữ một whitelist các issuer "hợp lệ", và hễ thấy cert mới từ issuer lạ thì báo động:
#!/usr/bin/env python3
# ct_monitor.py - Python 3.11+, requests 2.32
import json
import sys
from pathlib import Path
import requests
DOMAINS = ["example.vn", "api.example.vn"]
ALLOWED_ISSUERS = ["Let's Encrypt", "Google Trust Services"]
STATE_FILE = Path("seen_certs.json")
def fetch_certs(domain: str) -> list[dict]:
url = f"https://crt.sh/?q=%25.{domain}&output=json&exclude=expired"
resp = requests.get(url, timeout=60)
resp.raise_for_status()
return resp.json()
def is_allowed(issuer: str) -> bool:
return any(name in issuer for name in ALLOWED_ISSUERS)
def main() -> int:
seen = set(json.loads(STATE_FILE.read_text())) if STATE_FILE.exists() else set()
alerts = []
for domain in DOMAINS:
for cert in fetch_certs(domain):
cert_id = cert["id"]
if cert_id in seen:
continue
seen.add(cert_id)
if not is_allowed(cert["issuer_name"]):
alerts.append(
f"[!] {cert['name_value']} | issuer={cert['issuer_name']} "
f"| not_before={cert['not_before']} | https://crt.sh/?id={cert_id}"
)
STATE_FILE.write_text(json.dumps(sorted(seen)))
for line in alerts:
print(line)
return 1 if alerts else 0
if __name__ == "__main__":
sys.exit(main())
Vài lưu ý từ kinh nghiệm thực tế:
-
crt.sh hay timeout với domain có nhiều cert. Để
timeout=60và thêm retry. Lần chạy đầu sẽ rất chậm vì phải kéo toàn bộ lịch sử. - Exit code 1 khi có alert để cron hoặc CI tự gửi thông báo, ví dụ pipe output sang webhook Slack hay Telegram.
- Lần chạy đầu tiên script sẽ alert hàng loạt cert cũ của chính bạn. Chạy một lần để "seed" state file, đọc lại kết quả rồi mới bật alert.
- Cần real-time thì có thể subscribe CT log stream qua các thư viện như
certstream. Tuy vậy, với team nhỏ thì polling mỗi giờ là đủ: cert giả thường được dùng trong vài ngày, không phải vài phút.
Bước 2: Khóa domain bằng CAA record
Monitoring chỉ giúp phát hiện. CAA (Certification Authority Authorization, RFC 8659) giúp ngăn chặn: bạn khai báo trong DNS rằng chỉ những CA nào được cấp cert cho domain này. Từ 2017, CA/Browser Forum bắt buộc mọi CA phải kiểm tra CAA trước khi cấp.
# Kiểm tra CAA hiện tại
dig +short CAA example.vn
# Nếu trống -> mọi CA đều được phép cấp. Thêm record (ví dụ zone file BIND):
example.vn. 3600 IN CAA 0 issue "letsencrypt.org"
example.vn. 3600 IN CAA 0 issue "pki.goog"
example.vn. 3600 IN CAA 0 issuewild ";"
example.vn. 3600 IN CAA 0 iodef "mailto:security@example.vn"
# Verify sau khi propagate
dig +short CAA example.vn @1.1.1.1
Giải thích nhanh:
-
issue "letsencrypt.org": chỉ Let's Encrypt được cấp cert thường. -
issuewild ";": cấm hoàn toàn wildcard cert. Nếu không dùng*.example.vnthì nên bật, vì wildcard cert rơi vào tay kẻ xấu là thảm họa. -
iodef: CA nào nhận yêu cầu vi phạm policy có thể báo về email này (không phải CA nào cũng làm).
Cần nói thẳng: CAA không cứu được bạn nếu chính CA bị compromise hoàn toàn, vì attacker cầm key thì cứ thế ký, chẳng ai check DNS. CAA bảo vệ bạn khỏi trường hợp phổ biến hơn nhiều: CA validation sai, social engineering, hoặc một CA "dễ dãi" cấp cert khi bị lừa. Kết hợp với CT monitoring thì bạn có cả lớp chặn lẫn lớp phát hiện.
Bước 3: Tự kiểm tra cert chain trên production
Khi có tin CA bị revoke hoặc distrust, câu hỏi đầu tiên là: "Server của mình có đang dùng cert từ chain đó không?" Đừng đoán, kiểm tra luôn:
#!/usr/bin/env bash
# check_chain.sh - OpenSSL 3.x
set -euo pipefail
for host in example.vn api.example.vn admin.example.vn; do
echo "=== $host ==="
echo | openssl s_client -connect "$host:443" -servername "$host" -showcerts 2>/dev/null \
| awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/' \
| csplit -s -z -f "/tmp/${host}-cert-" - '/BEGIN CERTIFICATE/' '{*}'
for f in /tmp/${host}-cert-*; do
openssl x509 -in "$f" -noout -subject -issuer -enddate -fingerprint -sha256
echo "---"
done
rm -f /tmp/${host}-cert-*
done
Script in ra từng cert trong chain (leaf → intermediate) kèm issuer, ngày hết hạn và SHA-256 fingerprint. Đem fingerprint đối chiếu với danh sách intermediate bị distrust mà browser vendor công bố là biết mình có dính không.
Quy trình xử lý khi có sự cố, mình tóm lại như sau:
sequenceDiagram
participant Cron as CT Monitor
participant Dev as On-call Dev
participant CA as CA hợp lệ
participant Srv as Production
Cron->>Dev: Alert: cert lạ cho api.example.vn
Dev->>Dev: Check crt.sh, xác nhận không phải của team
Dev->>CA: Báo cáo + yêu cầu revoke qua CA phát hành
Dev->>Srv: Chạy check_chain.sh, rotate key nếu cần
Dev->>Dev: Siết CAA, review DNS/registrar access
Một điểm hay bị bỏ qua: cert giả cho domain của bạn chưa chắc đến từ CA bị hack. Rất nhiều trường hợp là do DNS hoặc tài khoản registrar bị chiếm, attacker tự xin cert hợp lệ qua DNS-01 challenge. Vì vậy bước cuối luôn là rà soát quyền truy cập DNS: bật 2FA cho registrar, đặt registry lock cho domain quan trọng.
Kết luận
Vụ eMudhra không phải lần đầu và chắc chắn không phải lần cuối. DigiNotar, Symantec, TrustCor đều từng dính. Bạn không kiểm soát được CA, nhưng kiểm soát được việc mình biết sớm hay muộn. Checklist làm ngay tuần này:
-
Chạy
dig CAAcho tất cả domain production. Nếu trống, thêm recordissuecho đúng CA bạn đang dùng, vàissuewild ";"nếu không cần wildcard. - Setup CT monitor bằng script ở trên hoặc tương tự, chạy cron mỗi giờ, đẩy alert về kênh chat của team.
-
Lưu
check_chain.shvào runbook để khi có tin CA bị distrust thì 30 giây là biết mình có bị ảnh hưởng không. - Bật 2FA và registry lock cho tài khoản domain registrar. Đây là điểm yếu thực tế hơn nhiều so với CA bị hack.
- Ghi rõ trong tài liệu nội bộ team đang dùng CA nào, cert nào, ai có quyền xin cert. Khi alert nổ lúc 2 giờ sáng, đó là thông tin bạn cần đầu tiên.
Tất cả chỉ mất khoảng một buổi chiều. So với việc phát hiện cert giả qua email phàn nàn của khách hàng, đó là khoản đầu tư rẻ nhất bạn làm được cho security năm nay.
Top comments (0)