Audit log cho agent: ghi cả người dùng lẫn agent làm thay họ
Khi agent làm sai trên hệ thống của khách hàng, một log chỉ ghi tên người dùng sẽ không trả lời được câu hỏi đầu tiên ai cũng đặt ra: ai đã làm việc này.
Tóm tắt nhanh
- OWASP yêu cầu mỗi bản ghi trả lời được khi nào, ở đâu, ai và cái gì. Với agent, phần "ai" phải gồm hai bên: người dùng và agent làm thay họ.
- Delegation theo RFC 8693 giữ cho agent có danh tính riêng, còn impersonation thì xoá ranh giới đó. Token passthrough trong MCP cũng làm log ghi sai danh tính.
- Tool input và output của model là văn bản không đáng tin. Cần làm sạch trước khi ghi, chỉ ghi nội dung khi được bật rõ ràng, và thêm cơ chế phát hiện chỉnh sửa.
Thử hình dung một tình huống: agent hỗ trợ của khách hàng vừa đóng 40 ticket lẽ ra phải để mở. Log của hệ thống ticket ghi người thực hiện là trưởng nhóm hỗ trợ.
Thực tế anh ấy chỉ gõ một câu yêu cầu, còn agent tự chọn tool, tự điền tham số và chạy. Khi khách hàng hỏi ai chịu trách nhiệm, bạn không có bằng chứng nào để trả lời.
Lỗi nằm ở log, không phải ở model. Tài liệu bảo mật của Model Context Protocol mô tả đúng hiện tượng này: khi token bị chuyển thẳng qua (passthrough), log phía sau có thể ghi nhận request như thể đến từ một nguồn khác với một danh tính khác.
Bài hướng dẫn này dựng một audit log trả lời được ba câu hỏi: ai kích hoạt, agent nào hành động thay họ, và đã gọi tool nào với tham số gì.
Để làm theo, bạn chỉ cần Python 3 và một dự án Claude Code thử nghiệm để gắn hook. Các đoạn code dưới đây đã được giản lược để dễ học. Trước khi đưa vào môi trường thật, hãy đối chiếu cú pháp cấu hình hook với tài liệu Claude Code.
Schema đúng bắt đầu từ bốn câu hỏi
OWASP Logging Cheat Sheet đặt ra yêu cầu nền: mỗi bản ghi phải trả lời được khi nào, ở đâu, ai và cái gì, và phải có đủ thông tin cho việc giám sát, phân tích về sau. Với agent, câu hỏi “ai” khó hơn hẳn, vì luôn có ít nhất hai danh tính cùng tham gia vào một hành động.
RFC 8693 (OAuth 2.0 Token Exchange) phân biệt rõ hai cơ chế. Với impersonation, bên hành động nhận toàn bộ quyền của người gốc và log không còn tách được hai bên. Với delegation, agent vẫn giữ danh tính riêng. Vì vậy schema của bạn cần hai trường riêng, on_behalf_of cho người dùng và actor cho agent.
| Câu hỏi | Trường trong bản ghi | Lấy từ đâu |
|---|---|---|
| Khi nào | when (UTC, ISO 8601) |
Đồng hồ máy chủ |
| Ở đâu | host, session_id |
Máy chạy, dữ liệu JSON của hook |
| Ai | on_behalf_of, actor, actor_chain |
Claim sub và act trong token |
| Cái gì | tool_name, tool_use_id, tham số |
Dữ liệu JSON của hook |
Cách kiểm tra: đưa bảng này cho một đồng nghiệp và hỏi họ có trả lời được “agent nào đã đóng 40 ticket” chỉ từ các trường trên không. Nếu không được, schema vẫn còn thiếu.
Bước 1: bắt từng lần gọi tool bằng hook
Hook của Claude Code chạy ở mỗi lần gọi tool và nhận một gói JSON có session_id, tool_name, tool_input và tool_use_id. Bốn trường này đủ để biết tool nào được gọi, với tham số gì và trong phiên nào. Tài liệu hooks có sẵn một ví dụ PreToolUse ghi nối vào file log và dùng matcher để lọc các tool MCP, và đoạn code dưới đây dựa trên mẫu đó.
#!/usr/bin/env python3
# audit_hook.py - bản giản lược để học
import json, sys, os, hashlib, datetime
LOG = os.path.expanduser("~/agent-audit.jsonl")
event = json.load(sys.stdin) # gói JSON hook nhận được
tool_input = event.get("tool_input") or {}
record = {
"when": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"where": {"host": os.uname().nodename,
"session_id": event.get("session_id")},
"what": {"tool_name": event.get("tool_name"),
"tool_use_id": event.get("tool_use_id"),
"input_keys": sorted(tool_input.keys())},
}
Hãy đăng ký script này làm hook PreToolUse. Nếu chỉ muốn theo dõi tool MCP, thêm matcher như ví dụ trong tài liệu. Cách kiểm tra: cho agent gọi một tool bất kỳ rồi mở file log. Bạn phải thấy tool_use_id khớp với lần gọi vừa chạy.
Bước 2: tách người dùng khỏi agent
Hook cho bạn biết cái gì đã xảy ra, nhưng câu trả lời cho “ai” nằm ở token. RFC 8693 định nghĩa claim act trong JWT để thể hiện rằng có delegation: sub vẫn là người dùng, còn act cho biết ai đang hành động. Khi một agent gọi một agent khác, các act lồng nhau thành một chuỗi.
def who_from_claims(claims: dict) -> dict:
chain, act = [], claims.get("act")
while act: # act ngoài cùng = actor hiện tại
chain.append(act.get("sub"))
act = act.get("act") # act lồng bên trong = actor trước đó
return {"on_behalf_of": claims.get("sub"),
"actor": chain[0] if chain else None,
"actor_chain": chain}
Có một chi tiết dễ bỏ qua. RFC 8693 nói các actor trước đó trong chuỗi act lồng nhau chỉ mang tính thông tin, tức token không ép buộc gì với chúng. Muốn sau này truy lại được chuỗi đó thì chính log phải lưu nó. Đó là lý do có trường actor_chain.
Hàm này phải chạy ở phía server nhận request, vì token nằm ở đó. MCP khuyến nghị server chỉ chấp nhận token được cấp cho chính nó, vì token passthrough làm việc điều tra sự cố và kiểm toán khó hơn.
Nếu bạn để token của người dùng đi xuyên qua nhiều tầng, actor sẽ luôn rỗng và bạn quay lại đúng tình huống 40 ticket. Cách kiểm tra: giải mã một token thật mà agent đang dùng để gọi API, rồi xem sub là ai trước khi kết luận.
Nếu sub là người dùng nhưng không có claim act, token không thể hiện delegation nào: agent đang impersonation hoặc chuyển thẳng token của người dùng, và log phía sau sẽ không tách được hai bên. Còn nếu sub chính là agent, nghĩa là agent đang dùng token của riêng nó, thì việc thiếu act là bình thường vì không có ai được làm thay.
Trường hợp sau ghi đúng actor, nhưng token không nói gì về người dùng đã kích hoạt. Khi đó on_behalf_of phải lấy từ phiên làm việc của người dùng và ghi riêng, nếu không bản ghi sẽ chỉ còn một nửa câu trả lời cho câu hỏi “ai”.
Bước 3: coi mọi tham số là văn bản không đáng tin
Tham số của tool do model sinh ra, và model có thể đã bị prompt injection điều khiển. OWASP yêu cầu làm sạch toàn bộ dữ liệu sự kiện trước khi ghi để chống log injection.
Thử hình dung tham số chứa ký tự xuống dòng kèm một dòng JSON giả: nếu bạn ghi thẳng chuỗi đó, kẻ tấn công vừa tự tạo được một bản ghi trong log của bạn.
Cách phòng thủ đơn giản là luôn ghi bằng json.dumps. Hàm này escape ký tự xuống dòng nên mỗi bản ghi chắc chắn nằm trên đúng một dòng. Vấn đề thứ hai là dữ liệu cá nhân. Quy ước GenAI của OpenTelemetry cảnh báo rằng nội dung message nhiều khả năng chứa PII, nên chỉ ghi nội dung khi được bật rõ ràng.
if os.environ.get("AUDIT_LOG_CONTENT") == "1": # opt-in
record["what"]["input"] = {k: str(v)[:500] for k, v in tool_input.items()}
Mặc định, log chỉ giữ tên các khoá tham số. Như vậy vẫn đủ để biết agent gọi tool delete với khoá ticket_id mà không lưu email khách hàng. Giới hạn 500 ký tự là con số minh hoạ, hãy chọn theo nhu cầu của bạn.
Bước 4: làm cho log khó bị sửa lén
Một audit log mà ai đó sửa được thì không còn là bằng chứng. OWASP khuyên tích hợp cơ chế phát hiện khi bản ghi bị sửa hoặc bị xoá, và sao chép log sang kho chỉ đọc càng sớm càng tốt. Một cách đơn giản là nối chuỗi hash: mỗi dòng giữ hash của dòng trước nó.
def last_hash():
try:
with open(LOG, "rb") as f:
return json.loads(f.readlines()[-1])["hash"]
except (FileNotFoundError, IndexError):
return "0" * 64
record["prev"] = last_hash()
body = json.dumps(record, sort_keys=True, ensure_ascii=False)
record["hash"] = hashlib.sha256(body.encode()).hexdigest()
with open(LOG, "a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
Bản giản lược này chưa khoá file khi nhiều hook ghi cùng lúc, và nó chỉ giúp phát hiện chỉnh sửa chứ không ngăn được. Phần ngăn chặn đến từ việc đẩy log sang kho chỉ đọc. Để kiểm tra, bạn cần một script đi lại từ đầu file và tính lại từng hash theo đúng cách hook đã tính.
#!/usr/bin/env python3
# verify_chain.py - tính lại chuỗi hash từ dòng đầu tiên
import json, hashlib, os, sys
LOG = os.path.expanduser("~/agent-audit.jsonl")
prev = "0" * 64
with open(LOG, encoding="utf-8") as f:
for n, line in enumerate(f, 1):
rec = json.loads(line)
h = rec.pop("hash")
body = json.dumps(rec, sort_keys=True, ensure_ascii=False)
if rec.get("prev") != prev or hashlib.sha256(body.encode()).hexdigest() != h:
sys.exit(f"Đứt chuỗi tại dòng {n}")
prev = h
print("Chuỗi nguyên vẹn")
Cách kiểm tra: chạy python3 verify_chain.py khi log còn nguyên, bạn phải thấy “Chuỗi nguyên vẹn”. Sau đó sửa tay một ký tự ở dòng thứ năm và chạy lại: script phải báo đứt chuỗi đúng tại dòng 5. Nếu bạn xoá hẳn một dòng, trường prev của dòng kế tiếp sẽ không khớp và script cũng dừng ở đó.
Bước 5: ghi cả lúc quyền được nới rộng
Hành động nguy hiểm thường đi kèm một lần xin thêm quyền. MCP khuyến nghị ghi các sự kiện nâng scope, gồm scope được yêu cầu và phần scope thực sự được cấp, kèm correlation ID. Tài liệu đó cũng cảnh báo rằng scope gộp quá rộng làm audit trail nhiễu và che mất ý định của người dùng.
Hãy dùng chung một correlation ID cho sự kiện cấp quyền và các lần gọi tool diễn ra sau đó. Khi điều tra, bạn sẽ đọc được trọn câu chuyện: người dùng yêu cầu gì, agent xin quyền gì, được cấp đến đâu, rồi đã dùng quyền đó vào việc gì.
Những lỗi thường gặp
Lỗi phổ biến nhất là ghi sub mà không ghi act, khiến mọi hành động của agent trông như người dùng tự làm. Lỗi thứ hai là bật ghi toàn bộ nội dung prompt “cho dễ debug”, và log trở thành kho PII không ai kiểm soát.
Lỗi thứ ba là để log trên cùng máy với agent, nơi agent có quyền ghi file, nên chính agent cũng có thể sửa được log của mình.
Nếu bạn muốn ghép vào hệ thống observability có sẵn, quy ước GenAI của OpenTelemetry định nghĩa thao tác invoke_agent và một thuộc tính định danh agent. Dùng chung tên trường với quy ước này giúp đội vận hành của khách hàng đọc được log mà không cần bạn giải thích.
Kỹ năng này xuất hiện ở đâu ngoài thực tế?
Thử hình dung đội bảo mật của khách hàng hỏi bạn sẽ truy lại hành động của agent thế nào khi có sự cố.
Nếu bạn mở ra được schema bốn câu hỏi, giải thích chuỗi act và chạy thử script phát hiện chỉnh sửa ngay trong buổi họp, cuộc trao đổi sẽ đi vào chi tiết thay vì dừng ở lời hứa.
Một FDE chỉ nói “chúng tôi có log” thì chưa trả lời được gì.
Khi đọc mô tả công việc FDE, hãy để ý các cụm như “enterprise security review”, “compliance” hay “agent governance”. Trong CV, đừng viết “đã làm logging”. Hãy viết rằng bạn đã thiết kế audit trail tách người dùng khỏi agent theo RFC 8693 và kiểm chứng được tính toàn vẹn bằng chuỗi hash, rồi mang theo file log để cho nhà tuyển dụng xem khi họ hỏi tiếp.
Agent càng được giao nhiều việc thì câu hỏi “ai đã làm việc này” càng hay được đặt ra. Câu trả lời thuyết phục nhất là một file log đã nằm sẵn trong kho chỉ đọc từ trước sự cố.
Bài này có hữu ích không?
Cảm ơn bạn đã góp ý!