Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 10 — Security as NFR — Threat Modeling

Architecture Decision-Making Bài 10/60

Mở đầu — vì sao bài này quan trọng

Trong loạt bài về NFR (Non-Functional Requirements), chúng ta đã đi qua định nghĩa NFR ở Bài 7, rồi lần lượt scalability, reliability và availability. Bài này ta bàn về một NFR mà rất nhiều đội ngũ ở Việt Nam vẫn coi là "chuyện của cuối dự án": security (bảo mật). Sự thật phũ phàng là bảo mật không phải một tính năng bạn gắn thêm vào lúc cuối, mà là một thuộc tính chất lượng phải được thiết kế ngay từ khâu ra quyết định kiến trúc.

Tôi muốn bạn nhớ một con số. Theo các báo cáo ngành công nghiệp phần mềm nhiều năm liền, chi phí để sửa một lỗ hổng bảo mật được phát hiện ở giai đoạn thiết kế rẻ hơn hàng chục lần so với khi nó bị khai thác trên production. Một sự cố lộ dữ liệu khách hàng ở một sàn thương mại điện tử Đông Nam Á không chỉ tốn tiền vá lỗi, mà còn kéo theo phạt hành chính theo Nghị định 13/2023 về bảo vệ dữ liệu cá nhân, mất niềm tin người dùng, và báo chí đưa tin suốt vài tuần.

Với vai trò người ra quyết định kiến trúc, câu hỏi của bạn không phải "code này có bug bảo mật không" — đó là việc của khâu review và pentest. Câu hỏi của bạn là: "Kiến trúc này để hở những đường tấn công nào, và ta chấp nhận rủi ro đó tới đâu?" Công cụ để trả lời câu hỏi đó một cách có hệ thống chính là threat modeling (mô hình hóa mối đe dọa). Bài này sẽ dạy bạn quy trình đó, cùng framework STRIDE của Microsoft — thứ đã trở thành ngôn ngữ chung của giới kiến trúc sư an ninh.

Khái niệm cốt lõi

Security là một NFR, không phải một feature

Khi ta nói security là NFR, ta muốn nói nó là một ràng buộc xuyên suốt (cross-cutting concern), giống như performance hay reliability. Nó không nằm gọn trong một module. Nó ảnh hưởng tới cách bạn thiết kế API, cách bạn lưu trữ dữ liệu, cách hai service nói chuyện với nhau, cách bạn cấp quyền.

Vì là NFR, security cũng cần được định lượng và đánh đổi. Bạn không thể "bảo mật 100%" — điều đó không tồn tại và cũng không kinh tế. Một hệ thống bảo mật tuyệt đối là hệ thống không ai dùng được. Nhiệm vụ của kiến trúc sư là xác định mức độ bảo mật tương xứng với giá trị tài sản cần bảo vệ và mức độ đe dọa. Bảo vệ số dư ví điện tử khác hoàn toàn với bảo vệ danh sách bài blog công khai.

Threat modeling — quy trình xác định mối đe dọa TRƯỚC khi build

Threat modeling là một quy trình có cấu trúc để trả lời bốn câu hỏi kinh điển (do Adam Shostack của Microsoft đúc kết):

  • Chúng ta đang xây gì? — Mô hình hóa hệ thống (thường bằng data flow diagram).
  • Cái gì có thể sai? — Liệt kê các mối đe dọa (đây là lúc STRIDE vào cuộc).
  • Chúng ta sẽ làm gì với những mối đe dọa đó? — Quyết định giảm thiểu (mitigate), chấp nhận, chuyển giao, hay loại bỏ.
  • Chúng ta đã làm tốt chưa? — Kiểm chứng lại mô hình.
Điểm mấu chốt: threat modeling diễn ra trước khi hoặc trong lúc thiết kế, không phải sau khi hệ thống đã chạy. Đó là lý do nó thuộc về bài học ra quyết định kiến trúc, chứ không phải bài về vá lỗi.

STRIDE — bảng phân loại mối đe dọa của Microsoft

STRIDE là một mnemonic (từ viết tắt gợi nhớ) do Microsoft phát triển, giúp bạn suy nghĩ một cách có hệ thống về sáu loại đe dọa. Mỗi loại đe dọa nhắm vào việc phá vỡ một thuộc tính bảo mật cụ thể.

Chữ cáiMối đe dọa (Threat)Thuộc tính bị phá vỡVí dụ dễ hình dung
SSpoofing (Giả mạo danh tính)Authentication (Xác thực)Kẻ tấn công đăng nhập bằng token đánh cắp, giả làm người dùng khác
TTampering (Sửa đổi trái phép)Integrity (Toàn vẹn)Sửa số tiền trong request từ 10.000đ thành 10đ
RRepudiation (Chối bỏ)Non-repudiation (Không thể chối bỏ)Người dùng phủ nhận đã thực hiện giao dịch, mà hệ thống không có log chứng minh
IInformation Disclosure (Rò rỉ thông tin)Confidentiality (Bảo mật)API trả về số CMND/CCCD của người dùng khác
DDenial of Service (Từ chối dịch vụ)Availability (Sẵn sàng)Gửi hàng triệu request làm sập hệ thống
EElevation of Privilege (Leo thang đặc quyền)Authorization (Ủy quyền)User thường gọi được API dành cho admin
Cách dùng bảng này trong thực tế: với từng thành phần trong sơ đồ kiến trúc của bạn (mỗi API endpoint, mỗi hàng đợi message, mỗi kết nối database), bạn lần lượt hỏi sáu câu STRIDE. "Endpoint này có thể bị spoofing không? Có thể bị tampering không?..." Cách làm này biến một câu hỏi mơ hồ ("bảo mật thế nào?") thành một checklist cụ thể, dễ giao cho cả đội cùng làm.

Ranh giới tin cậy — trust boundary

Một khái niệm không thể thiếu khi threat modeling là trust boundary (ranh giới tin cậy). Đó là đường phân chia giữa vùng bạn kiểm soát và vùng bạn không kiểm soát. Ví dụ: ranh giới giữa trình duyệt người dùng (không tin cậy) và backend của bạn; giữa service của bạn và một API bên thứ ba; giữa network public và VPC nội bộ.

Nguyên tắc vàng: mọi dữ liệu đi qua trust boundary đều phải bị nghi ngờ và kiểm tra lại. Phần lớn lỗ hổng nghiêm trọng xảy ra vì đội ngũ tin tưởng dữ liệu đến từ một nguồn mà lẽ ra không nên tin.

Tình huống thực tế

Ví dụ 1 — Fintech Việt Nam và lỗ hổng Tampering trên API thanh toán

Một startup ví điện tử tại TP.HCM (gọi là "VíNhanh") ra mắt tính năng nạp tiền qua liên kết ngân hàng. Đội backend làm rất nhanh, API nhận request dạng { "userId": 123, "amount": 500000, "signature": "..." }. Trong lúc threat modeling, một kiến trúc sư áp STRIDE lên endpoint này và dừng ở chữ T — Tampering: "Nếu client sửa userId thành của người khác thì sao? Nếu sửa amount thì sao?"

Hóa ra signature chỉ ký lên amount mà quên ký lên userId. Kẻ tấn công có thể nạp tiền vào tài khoản của mình nhưng ghi nợ tài khoản người khác. Đây là lỗ hổng Tampering phá vỡ tính Integrity. Nhờ phát hiện ở giai đoạn thiết kế, đội chỉ mất một buổi để đổi sang ký toàn bộ payload phía server và không bao giờ tin userId từ client mà lấy từ session token đã xác thực.

Bài học: STRIDE buộc bạn soi từng thuộc tính. Nếu chỉ hỏi chung chung "API này an toàn chưa", rất dễ bỏ sót vì API chữ ký — trông có vẻ an toàn. Chỉ khi hỏi cụ thể "tampering vào trường nào" thì lỗ hổng mới lộ ra.

Ví dụ 2 — Sàn TMĐT và Information Disclosure qua IDOR

Một sàn thương mại điện tử khu vực Đông Nam Á (bối cảnh giả định gần với các vụ đã xảy ra thật) có endpoint xem hóa đơn: GET /api/invoices/{invoiceId}. Backend kiểm tra người dùng đã đăng nhập chưa (authentication — chữ S đã xử lý), nhưng quên kiểm tra hóa đơn đó có thuộc về người dùng đang đăng nhập không (authorization).

Đây là lỗi kinh điển tên IDOR (Insecure Direct Object Reference), thuộc nhóm I — Information Disclosure và cũng chạm tới E — Elevation of Privilege. Một người dùng chỉ cần đổi invoiceId từ 10001 thành 10002 là xem được hóa đơn của người khác, gồm tên, địa chỉ, số điện thoại, danh sách hàng đã mua. Với hàng trăm nghìn hóa đơn, đây là một vụ rò rỉ dữ liệu cá nhân quy mô lớn, đủ để bị xử lý theo Nghị định 13/2023.

Nếu đội đã làm threat modeling và hỏi "Information Disclosure: ai được xem tài nguyên này?", họ sẽ nhận ra ngay rằng xác thực (biết bạn là ai) khác với ủy quyền (bạn được phép làm gì). Bài học: authentication và authorization là hai thuộc tính riêng biệt — S và E trong STRIDE — và phải kiểm tra riêng. Rất nhiều sự cố thật xảy ra vì đội ngũ nghĩ "đã login rồi thì an toàn".

Ví dụ 3 — Nền tảng học trực tuyến và Repudiation trong hệ thống chấm điểm

Một nền tảng học trực tuyến cấp chứng chỉ nghề (bối cảnh gần với chính sản phẩm bạn đang xây). Học viên hoàn thành bài thi và nhận chứng chỉ. Vài tháng sau, một nhà tuyển dụng nghi ngờ chứng chỉ giả và khiếu nại. Đội vận hành mở hệ thống ra và... không có log nào ghi lại ai đã chấm, chấm lúc nào, điểm gốc là bao nhiêu. Học viên có thể chối, mà đội cũng không chứng minh được điều ngược lại.

Đây chính là mối đe dọa R — Repudiation, phá vỡ thuộc tính non-repudiation. Nếu threat modeling được làm từ đầu, đội sẽ thiết kế audit log bất biến (append-only, có timestamp và chữ ký) cho mọi hành động cấp/thu hồi chứng chỉ. Bài học: Repudiation là mối đe dọa hay bị lãng quên nhất trong STRIDE, vì nó không "gây hại kỹ thuật" trước mắt. Nhưng với các hệ thống liên quan tới tiền, pháp lý, hoặc chứng nhận, việc chứng minh được điều gì đã xảy ra quan trọng ngang việc ngăn chặn.

Hướng dẫn từng bước

Đây là quy trình threat modeling gọn nhẹ bạn có thể áp dụng ngay trong một buổi họp thiết kế 60–90 phút:

Bước 1 — Vẽ Data Flow Diagram (DFD). Vẽ các thành phần (external entity như user/bên thứ ba, process như service, data store như database, và data flow là các mũi tên). Đừng cầu toàn — một sơ đồ trên whiteboard là đủ. Quan trọng nhất: đánh dấu các trust boundary bằng đường nét đứt.

Bước 2 — Xác định tài sản cần bảo vệ. Liệt kê những gì có giá trị: dữ liệu cá nhân (PII), tiền, token, quyền admin, uy tín thương hiệu. Điều này giúp bạn ưu tiên — không phải mọi thành phần đều đáng đầu tư bảo mật ngang nhau.

Bước 3 — Duyệt STRIDE trên từng phần tử. Với mỗi process và mỗi data flow đi qua trust boundary, chạy qua sáu chữ S-T-R-I-D-E. Ghi lại từng mối đe dọa tìm thấy vào một bảng: thành phần | loại STRIDE | mô tả đe dọa | mức độ nghiêm trọng.

Bước 4 — Đánh giá mức độ rủi ro. Với mỗi mối đe dọa, ước lượng khả năng xảy ra × mức độ thiệt hại. Bạn có thể dùng thang đơn giản Cao/Trung bình/Thấp. Đừng sa đà vào các công thức phức tạp trong lần đầu.

Bước 5 — Quyết định xử lý. Với mỗi rủi ro, chọn một trong bốn: Mitigate (giảm thiểu — thêm kiểm soát), Accept (chấp nhận — rủi ro thấp, chi phí sửa cao), Transfer (chuyển giao — ví dụ mua bảo hiểm mạng, hoặc dùng dịch vụ managed), Eliminate (loại bỏ — bỏ luôn tính năng gây rủi ro). Đây là bước ra quyết định kiến trúc thực sự.

Bước 6 — Ghi lại thành ADR. Những quyết định giảm thiểu quan trọng nên được ghi vào Architecture Decision Record (đúng như Bài 4 đã dạy), để đội sau này hiểu vì sao kiến trúc lại thế này. Ví dụ: "Quyết định lấy userId từ JWT thay vì từ request body để chống Tampering."

Bước 7 — Lặp lại khi kiến trúc thay đổi. Threat model không phải tài liệu làm một lần rồi bỏ. Mỗi khi thêm một service, một integration mới, một trust boundary mới xuất hiện — hãy chạy lại các bước trên cho phần thay đổi.

Lỗi thường gặp & mẹo

Lỗi 1 — Làm threat modeling quá muộn. Nhiều đội đợi tới sát ngày go-live mới mời security vào. Lúc đó phát hiện lỗ hổng kiến trúc thì đã quá đắt để sửa. Mẹo: làm một phiên threat modeling nhẹ ngay khi vẽ xong sơ đồ kiến trúc đầu tiên.

Lỗi 2 — Bỏ qua Repudiation và Denial of Service. Đội ngũ thường chỉ nhớ S, T, I (những thứ "rõ ràng nguy hiểm") mà quên R và D. Mẹo: luôn chạy đủ cả sáu chữ, kể cả khi bạn nghĩ chữ đó không liên quan — chính lúc đó bạn hay phát hiện điều bất ngờ.

Lỗi 3 — Nhầm authentication với authorization. "Đã đăng nhập" không có nghĩa là "được phép". Đây là gốc rễ của lỗi IDOR ở Ví dụ 2. Mẹo: với mỗi truy cập tài nguyên, hỏi riêng hai câu — "bạn là ai?" (S) và "bạn được làm gì với cái này?" (E).

Lỗi 4 — Tin dữ liệu qua trust boundary. Tin userId từ client, tin dữ liệu từ webhook bên thứ ba mà không verify chữ ký, tin header từ proxy. Mẹo: gạch đậm mọi mũi tên cắt qua ranh giới tin cậy trên DFD và đối xử với chúng như dữ liệu độc hại cho tới khi được kiểm chứng.

Lỗi 5 — Cầu toàn hóa quy trình. Một số đội bỏ hàng tuần vẽ DFD siêu chi tiết rồi kiệt sức, không bao giờ làm lần hai. Mẹo: threat modeling "đủ tốt và thường xuyên" luôn thắng "hoàn hảo nhưng một lần". Whiteboard 60 phút mỗi sprint là tuyệt vời.

Mẹo bổ sung — nguyên tắc least privilege và defense in depth. Khi giảm thiểu, hãy ưu tiên hai nguyên tắc: cấp quyền tối thiểu (mỗi thành phần chỉ có đúng quyền nó cần) và phòng thủ nhiều lớp (đừng dựa vào một lớp bảo vệ duy nhất). Nếu một lớp thủng, lớp sau vẫn đỡ.

Bài tập thực hành

Hãy chọn một tính năng nhỏ trong hệ thống bạn đang làm (hoặc dùng ví dụ: "chức năng đặt lịch với mentor có gửi email và Telegram thông báo") và thực hiện:

  • Vẽ DFD cho tính năng đó trên giấy hoặc whiteboard, gồm: người dùng, backend, database, và service gửi thông báo bên thứ ba. Đánh dấu ít nhất hai trust boundary.
  • Lập bảng STRIDE. Với endpoint "tạo booking", chạy qua cả sáu chữ S-T-R-I-D-E và viết ra ít nhất một mối đe dọa cụ thể cho mỗi chữ. Cố gắng đừng bỏ chữ nào — kể cả Repudiation.
  • Xếp hạng rủi ro. Gán Cao/Trung bình/Thấp cho từng mối đe dọa dựa trên khả năng × thiệt hại.
  • Ra quyết định. Với hai mối đe dọa nghiêm trọng nhất, chọn Mitigate/Accept/Transfer/Eliminate và viết một câu giải thích vì sao. Viết một trong số đó thành một ADR ngắn (bối cảnh — quyết định — hệ quả).
Sau bài tập này, bạn sẽ thấy: chỉ cần 30 phút áp STRIDE có kỷ luật là bạn đã phát hiện ra những rủi ro mà trước đây trôi tuột qua trong lúc code.

Tóm tắt

  • Security là NFR, một thuộc tính chất lượng xuyên suốt phải được thiết kế từ đầu, không phải feature gắn thêm lúc cuối. Nó cần được định lượng và đánh đổi tương xứng với giá trị tài sản.
  • Threat modeling là quy trình có cấu trúc trả lời bốn câu hỏi: xây gì, cái gì có thể sai, làm gì với nó, và đã tốt chưa. Nó diễn ra trước khi build.
  • STRIDE của Microsoft phân loại sáu mối đe dọa — Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege — mỗi loại phá vỡ một thuộc tính bảo mật cụ thể (authentication, integrity, non-repudiation, confidentiality, availability, authorization).
  • Trust boundary là nơi mọi dữ liệu phải bị nghi ngờ; phần lớn sự cố đến từ việc tin nhầm nguồn dữ liệu.
  • Ba ví dụ thực tế cho thấy Tampering, Information Disclosure (IDOR) và Repudiation đều có thể bị bắt sớm nếu ta chạy STRIDE có kỷ luật — và đều rất đắt nếu bỏ sót.
  • Quy trình thực dụng: vẽ DFD, xác định tài sản, duyệt STRIDE, xếp hạng rủi ro, ra quyết định xử lý, ghi thành ADR, và lặp lại khi kiến trúc đổi. "Đủ tốt và thường xuyên" thắng "hoàn hảo một lần".
Ở bài tiếp theo, chúng ta sẽ chuyển sang một NFR khác — Performance với latency, throughput và P99 — nhưng tư duy đánh đổi có hệ thống bạn học ở đây sẽ theo bạn suốt cả khóa.