Bảo mật chuỗi cung ứng phần mềm nghe như vấn đề của những tổ chức có hẳn một bộ phận an ninh, và cách đóng khung đó gây hiểu lầm. Một đội nhỏ vận hành vài dịch vụ thường phụ thuộc vào hàng trăm gói mà không ai trong đội từng đọc qua, được kéo về lúc build từ những registry họ không kiểm soát, rồi chạy script cài đặt ngay trên máy đang giữ khóa triển khai.
Mức phơi nhiễm không tỉ lệ với quy mô công ty. Nó tỉ lệ với số lượng phụ thuộc và mức tự động hóa của quy trình build, mà đội nhỏ thường có nhiều thứ nhất và ít người soi thứ hai hơn so với các tổ chức công khai lo lắng về chuyện này.
Phép tính khó chịu: ứng dụng của bạn có lẽ có khoảng một tá phụ thuộc trực tiếp và vài trăm phụ thuộc gián tiếp. Một tá đó do bạn chọn. Phần còn lại thì không, bạn chưa đọc gói nào trong số đó, và bất kỳ gói nào khi chạy script cài đặt đều có đúng quyền truy cập như tiến trình build của bạn. Đó mới là bề mặt tấn công thật sự, và nó không hiện ra trong danh sách phụ thuộc do chính bạn viết.
Bảo mật chuỗi cung ứng bắt đầu từ lúc cài đặt
Khoảnh khắc nguy hiểm thường không phải lúc chạy code. Mà là lúc cài nó.
Trình quản lý gói cho phép script chạy trong quá trình cài đặt, nghĩa là một gói bị chiếm quyền hoặc cố tình độc hại sẽ chạy với quyền của người vừa gõ lệnh cài. Trên máy lập trình viên, đó là thông tin đăng nhập của họ. Trong pipeline build, đó là khóa triển khai, và tình huống này tệ hơn nhiều.
Ba dạng tấn công chiếm phần lớn các sự cố có thật.
Typosquatting. Một gói có tên lệch đúng một ký tự so với gói phổ biến, được công bố rồi nằm chờ một lệnh cài đặt gõ sai. Rẻ để dựng và hiệu quả ở quy mô lớn.
Chiếm tài khoản của một gói hợp lệ. Một gói dùng rộng rãi, người bảo trì bị đánh cắp thông tin đăng nhập, rồi gói được cập nhật kèm thứ độc hại. Đây là dạng khó phòng nhất, vì cả tên gói lẫn lượt tải đều không có gì bất thường.
Dependency confusion. Một gói công khai được đăng dưới tên gói nội bộ của bạn, trong khi resolver cấu hình sai lại ưu tiên registry công khai hơn registry riêng. Hoàn toàn là vấn đề cấu hình và hoàn toàn ngăn được.
Không dạng nào đòi hỏi ai đó nhắm riêng vào bạn, và đó chính là điểm mấu chốt. Chúng cơ hội và chúng nhân rộng rất nhanh.
Biện pháp đáng công sức với đội nhỏ
Đưa lockfile vào repo và cài từ nó. Lockfile ghim đúng phiên bản kèm hash. Cài từ manifest thì phiên bản mới được giải lúc build, khiến bản build không tái lập được và một thay đổi từ thượng nguồn lọt thẳng lên production mà không ai xem lại. Hãy dùng lệnh cài báo lỗi khi lockfile không khớp, đừng dùng lệnh âm thầm cập nhật nó.
Tắt script cài đặt ở nơi có thể. Nhiều hệ sinh thái hỗ trợ điều này, và phần lớn gói không cần đến chúng. Đây là mức giảm phơi nhiễm lớn nhất có được mà không cần thêm công cụ, còn vài gói hỏng vì nó lại chính là những gói bạn nên biết.
Tự động hóa cảnh báo lỗ hổng, rồi phân loại. Máy quét sinh ra nhiều kết quả hơn sức một đội nhỏ, và kiểu hỏng không phải bỏ sót cảnh báo mà là bỏ qua tất cả vì đa số không liên quan. Hãy lọc theo phần thực sự chạm tới được từ code của bạn và xử lý phần đó. Một hàng đợi không ai đọc còn tệ hơn không có hàng đợi.
Ghim cả công cụ build. Image container, phiên bản runtime, action hay plugin mà pipeline gọi đều là phụ thuộc; trỏ vào tag thay đổi được thay vì digest bất biến nghĩa là bản build tự đổi mà bạn không hay.
Tách khóa build khỏi mọi thứ khác. Pipeline được phép triển khai thì không nên giữ khóa làm được nhiều hơn thế, và một bản build bị chiếm quyền không nên chạm tới dữ liệu production.
SBOM cho biết gì và không cho biết gì
Danh mục thành phần phần mềm, gọi tắt là SBOM, liệt kê những gì có trong ứng dụng của bạn, và giá trị của nó nằm ở chỗ trả lời nhanh một câu hỏi duy nhất: chuyện này có ảnh hưởng tới chúng ta không?
Câu hỏi đó trước đây mất nhiều ngày. Khi một thành phần dùng rộng rãi lộ ra lỗ hổng, tổ chức có bản kiểm kê cập nhật trả lời trong vài phút, còn tổ chức không có thì mất cả tuần đi tìm. Khác biệt đó là toàn bộ lập luận, và đó là lý do việc áp dụng SBOM đang được thúc đẩy như thực hành nền tảng chứ không phải biện pháp nâng cao.
Điều nó không làm được là khiến bạn an toàn. Bản kiểm kê là một danh sách, không phải một lớp phòng thủ. Nó không cho biết lỗ hổng được liệt kê có chạm tới được trong code của bạn không, có đáng kể trong cấu hình của bạn không, hay đã có thứ gì bị chiếm quyền chưa. Đội nào tạo ra một bản rồi cất đi là đã thêm một tài liệu, không phải một biện pháp kiểm soát.
Hãy sinh nó từ quy trình build, để nó mô tả đúng thứ đã phát hành chứ không phải thứ manifest tuyên bố. Giữ lại bản khớp với từng phiên bản phát hành. Và chấp nhận rằng giá trị của nó nằm trọn ở tốc độ trả lời một câu hỏi sẽ được đặt ra khi bạn đang chịu áp lực.
Đội nhỏ thực sự thiệt hại ở đâu
Hiếm khi vì một vụ chiếm quyền gói kỳ lạ. Mà vì những chuyện rất bình thường.
Secret nằm trong repo, xóa rồi vẫn còn trong lịch sử và bị máy quét tự động tìm ra chỉ vài phút sau khi push. Một phụ thuộc ba năm không cập nhật có exploit đã công bố và không còn đường nâng cấp, vì việc nâng phiên bản bị hoãn đến khi thành cả một dự án. Pipeline build có quyền quá rộng, nơi một pull request từ fork chạy với quyền lẽ ra không được có. Không ai theo dõi, nên một vụ xâm nhập kéo dài vì chẳng cảnh báo nào tới được tay người thật.
Kết luận không hào nhoáng là giữ phụ thuộc luôn mới chính là phần lớn công việc bảo mật chuỗi cung ứng trên thực tế. Chi phí bảo trì phần mềm mà các đội coi là tùy chọn chính là công việc đó, và hoãn nó lại biến một lần nâng cấp thường lệ thành sự cố khẩn cấp kèm sẵn một exploit.
Điểm khởi đầu vừa sức
Với một đội nhỏ, theo thứ tự: đưa secret ra khỏi repo và xoay vòng mọi thứ đã lộ. Đưa lockfile vào repo và cài nghiêm ngặt từ nó. Bật cảnh báo phụ thuộc và phân loại hằng tuần thay vì liên tục. Ghim công cụ build vào tham chiếu bất biến. Giảm quyền của pipeline xuống mức tối thiểu. Sinh một SBOM cho mỗi bản phát hành khi phần còn lại đã ổn.
Trình tự đó bao phủ các mối đe dọa thực tế mà không cần bộ phận an ninh riêng, và mỗi bước tính bằng giờ chứ không phải bằng tuần. Những biện pháp ở tầng trên, gồm chứng thực nguồn gốc, build tái lập được và artifact có chữ ký, đều thực sự giá trị nhưng thuộc về giai đoạn khi phần nền đã chạy ổn định.
Mecanik rà soát và gia cố pipeline build trong khuôn khổ dịch vụ phân tích bảo mật ứng dụng. Thứ tìm thấy gần như không bao giờ là một vụ tấn công tinh vi. Đó là một token nằm trong repo và một phụ thuộc không ai cập nhật từ ngày nó được chọn.
Câu hỏi thường gặp
Vì sao cài đặt gói lại là thời điểm rủi ro? Vì trình quản lý gói cho phép script chạy trong lúc cài đặt, với quyền của người vừa gõ lệnh cài. Trên máy lập trình viên, đó là thông tin đăng nhập của họ; trong pipeline build, đó là khóa triển khai. Đoạn code đó thậm chí không cần được ứng dụng của bạn gọi tới mới gây hại được.
Dependency confusion là gì? Một kiểu tấn công trong đó gói công khai được đăng dưới tên một gói nội bộ của bạn, còn resolver cấu hình sai lại ưu tiên registry công khai hơn registry riêng. Nó không cần ai nhắm riêng vào bạn và có thể ngăn hoàn toàn bằng cách cấu hình resolver cho đúng.
SBOM thực sự mang lại điều gì? Một câu trả lời nhanh cho đúng một câu hỏi: chuyện này có ảnh hưởng tới chúng ta không? Có bản kiểm kê cập nhật thì mất vài phút; không có thì mất cả tuần đi tìm. Nó không cho biết lỗ hổng được liệt kê có chạm tới được trong code của bạn không, cũng không cho biết đã có thứ gì bị chiếm quyền chưa, nên một SBOM sinh ra rồi cất đi vẫn chỉ là tài liệu chứ không phải biện pháp kiểm soát.
Tôi có nên đưa lockfile vào repo không? Có, và hãy cài nghiêm ngặt từ nó bằng lệnh báo lỗi khi lockfile không khớp thay vì lệnh tự cập nhật. Cài từ manifest sẽ giải phiên bản mới ngay lúc build, khiến bản build không tái lập được và để một thay đổi từ thượng nguồn lên tới production mà không qua bất kỳ khâu xem lại nào.
Một đội nhỏ nên bắt đầu từ đâu? Đưa secret ra khỏi repo và xoay vòng mọi thứ đã lộ, đưa lockfile vào repo và cài nghiêm ngặt, bật cảnh báo phụ thuộc và phân loại hằng tuần, ghim công cụ build vào tham chiếu bất biến, giảm quyền của pipeline, rồi sinh một SBOM cho mỗi bản phát hành. Mỗi bước tính bằng giờ chứ không phải bằng tuần.
Bình luận