Nhật ký kiểm toán (audit log) là thứ bạn biết ơn khi có sự cố: ai đã đăng nhập, từ đâu, làm gì, lúc mấy giờ. Không có nó thì điều tra một vụ xâm nhập giống như dò trong bóng tối. Nhưng log cũng là con dao hai lưỡi — ghi quá tay thì chính cái log dựng lên để bảo vệ bạn lại trở thành một kho bí mật rò rỉ. Bài này đo cả hai mặt: cái gì cần ghi, và cái gì ghi vào là tự hại mình.

Tôi dựng một bộ ghi log nhỏ, cho nó xử lý một request đăng nhập chứa cả dữ liệu kiểm toán lẫn bí mật, rồi đo xem cái gì lọt vào file log. Ba lần, kết quả giống hệt. Chạy trong môi trường của mình với bí mật giả, không nhắm vào hệ thống của ai khác.

Đo 1: ghi ngây thơ — đẩy nguyên request vào log

Cách viết log phổ biến và tai hại nhất chỉ có một dòng: log nguyên cái request cho "đầy đủ để sau này còn lần". Request đăng nhập của tôi có user, ip, action, result (dữ liệu kiểm toán hợp lệ) nhưng cũng có password, authorization (token), và card (số thẻ). Đo số lần bí mật xuất hiện trong log:

so lan bi mat xuat hien trong log: 3
trich log: {"user":"an","password":"SieuMatKhau!2026","action":"login","authorization":"Bear...

Ba bí mật — mật khẩu, token, số thẻ — nằm nguyên văn trong file log. Đây không phải lỗi hiếm; nó là kết quả mặc định của việc log cả object request, log cả header, hay log cả body của một lời gọi API. Và điều khiến nó nguy hiểm hơn vẻ ngoài là vòng đời của log: log không nằm yên trong một file trên một máy. Nó được gom về một nơi tập trung (ELK, Splunk, CloudWatch), giữ hàng tháng tới hàng năm cho mục đích tuân thủ, và được nhiều người đọc — đội vận hành, đội hỗ trợ, bất kỳ ai có quyền vào dashboard log. Một mật khẩu ghi vào log là một mật khẩu bị lộ ở một kho thứ hai, kho mà bạn còn không nghĩ tới khi lo bảo vệ cơ sở dữ liệu.

Nói cách khác: bí mật vào log thì coi như đã rò. Không có "log nội bộ nên không sao" — log là một bản sao dữ liệu, và mọi bản sao đều là bề mặt tấn công.

Đo 2: bộ lọc che — xoá bí mật, giữ dấu vết

Cách đúng không phải là bỏ log đi, mà là che (redact) theo khoá nhạy cảm trước khi ghi. Tôi thêm một bộ lọc: bất kỳ khoá nào tên là password, authorization, token, secret, card, cookie thì thay giá trị bằng ***, còn lại giữ nguyên. Đo lại:

so lan bi mat xuat hien trong log: 0
truong kiem toan 'user'   con trong log? CO
truong kiem toan 'action' con trong log? CO
truong kiem toan 'ip'     con trong log? CO
truong kiem toan 'result' con trong log? CO
trich log: {"user":"an","password":"***","action":"login","ip":"203.0.113.9","card":"***","result":"success"}

Đây chính là cái đích: 0 bí mật lọt ra, nhưng toàn bộ trường kiểm toán vẫn còn. Bạn vẫn biết an đã đăng nhập từ 203.0.113.9 lúc nào và thành công hay không — đủ để điều tra sau này — mà không giữ lại một ký tự nào của mật khẩu hay số thẻ. Log kiểm toán tốt trả lời được câu "ai làm gì" mà không trả lời câu "mật khẩu của họ là gì".

Điểm mấu chốt của bộ lọc là nó che theo khoá, chạy ngay trước khi ghi, chứ không dựa vào lập trình viên nhớ bỏ bí mật ra ở từng chỗ gọi log. Trông cậy vào trí nhớ thì chỉ cần một dòng log.debug(request) quên sót là rò. Che tập trung một chỗ thì mọi đường ghi log đều đi qua cùng một tấm lưới.

Đo 3: cái bẫy — log lỗi kéo theo dữ liệu

Có một đường rò tinh vi hơn mà bộ lọc trên chưa chắc bắt được: thông báo lỗi. Tôi ném một exception mô phỏng lỗi cơ sở dữ liệu rồi ghi nguyên nó vào log:

ghi nguyen e -> so lan bi mat trong log: 1

Thông báo lỗi từ tầng dưới — cơ sở dữ liệu, thư viện — thường nhúng sẵn dữ liệu gây lỗi vào chuỗi thông báo: "duplicate key: INSERT INTO users(pass) VALUES('...')". Bạn ghi cái lỗi đó để gỡ rối, và vô tình ghi luôn giá trị nhạy cảm nằm trong câu SQL. Đây đúng là loại bẫy mà một dự án dễ vấp: bộ lọc che khoá password không giúp gì, vì bí mật lần này đến trong một câu văn lỗi, không phải một trường có tên. Cách chặn là đừng ghi nguyên str(exception) ra ngoài — ghi loại lỗi và mã, giữ chi tiết đầy đủ ở một nơi riêng quyền chặt, và không bao giờ trả thông báo lỗi thô của tầng dữ liệu ra log chung.

Còn một chuyện: bảo vệ chính cái log

Một nhật ký kiểm toán chỉ có giá trị nếu kẻ tấn công không sửa được nó. Nghịch lý là kẻ đột nhập gần như luôn tìm cách xoá dấu vết — mà dấu vết nằm đúng trong log. Nếu log chỉ là một file mà tài khoản ứng dụng có quyền ghi đè, thì kẻ chiếm được ứng dụng cũng xoá được log ngay sau khi gây án, và bạn mất đúng thứ cần để điều tra.

Vì thế log kiểm toán nghiêm túc thường chỉ được thêm, không được sửa (append-only), và được đẩy sang một nơi khác mà tài khoản ứng dụng không có quyền xoá — một dịch vụ log tập trung, một bucket chỉ-ghi, hay một máy riêng. Ở mức cao hơn, người ta ký hoặc nối chuỗi băm các bản ghi (mỗi dòng chứa băm của dòng trước) để một chỉnh sửa giữa chừng làm gãy cả chuỗi — đúng ý tưởng chữ ký ở bài trước, áp cho nhật ký. Điểm chung: tách quyền ghi khỏi quyền xoá, và đưa log ra khỏi tầm với của thứ mà nó đang giám sát.

Ghép lại

Sơ đồ nhật ký kiểm toán: ghi gì và không ghi gì

Chia đôi rạch ròi:

Cần ghi (để điều tra được): ai (user/id), khi nào (mốc thời gian), từ đâu (IP), làm gì (action), kết quả (thành công/thất bại), và với thao tác nhạy cảm thì cả đối tượng bị tác động. Đây là bộ khung "who/what/when/where/result" của mọi log kiểm toán tử tế.

Không được ghi: mật khẩu, token/khoá phiên, số thẻ đầy đủ, dữ liệu cá nhân nhạy cảm, và nội dung thông báo lỗi thô từ tầng dưới. Những thứ này vào log là nhân bản rủi ro sang một kho sống lâu và đông người đọc.

Mô-típ của Series lại đúng: nguy hiểm không nằm ở chỗ bạn tưởng (cơ sở dữ liệu được canh gác kỹ), mà ở chỗ bạn không để mắt (file log ai cũng đọc được). Và cái ngược trực giác ở đây là ghi nhiều hơn không an toàn hơn — quá một ngưỡng, mỗi trường ghi thêm là thêm một thứ có thể rò. Log kiểm toán tốt là log ghi đủ để điều tra, không phải ghi tất cả cho chắc.

Thử ba mươi giây

Tìm trong log của chính bạn xem có bí mật nào đang nằm đó:

grep -iE 'password|passwd|authorization|bearer |secret|token=|"card"|[0-9]{16}' /duong/dan/toi/app.log | head

Nếu có dòng nào hiện ra kèm một giá trị thật, đó không phải cảnh báo giả — đó là một bí mật đã rò vào log, và có thể đã được gom lên hệ thống log tập trung từ lâu. Việc cần làm gồm hai phần: che nó ở nguồn (bộ lọc theo khoá), và coi mọi bí mật từng xuất hiện trong log như đã lộ — tức là xoay vòng chúng, đúng như bài "Bí mật trong mã nguồn" đã đo.