iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Security

《30 天為金融法務部門打造 LLM 個資防護閘:去識別化、可控還原與紅隊驗證》系列 第 20 篇

Day 20|稽核軌跡:能回答「是誰」的那份紀錄

  • 分享至 

  • xImage
  •  

「我們有 log」不等於「可稽核」

多數系統都有日誌。但當事故發生、稽核來問「上個月 3 月 15 日下午,誰還原了張三的資料」,多數系統答不出來。

原因通常是:

  • 日誌記了 API 被呼叫,沒記呼叫的內容
  • 日誌散在五個地方,無法串接
  • 日誌本身可以被有權限的人修改
  • 日誌保存期比稽核要求短
  • 日誌裡有明文個資,所以存取被限制得比業務系統還嚴,反而沒人能查

最後一項很諷刺但很常見。

可稽核性的四個要求

一、完整性:所有敏感操作都要記錄。

哪些算敏感操作?

文件上傳
偵測執行(含 findings 統計,不含原文)
去識別化執行
送出至外部模型
還原請求(成功與失敗都要)
政策變更
權限變更
Vault 清除
金鑰輪替

失敗的還原請求特別重要——連續多次失敗的還原嘗試是攻擊的訊號。

二、不可否認性:能歸屬到具體的人。

服務帳號不算。如果日誌寫的是 service-account-app@project.iam,那你只知道「應用程式做的」。必須把終端使用者身分帶進來:

audit_entry = {
    "actor": {
        "principal": ctx.user_email,        # 終端使用者
        "service_account": ctx.sa_email,     # 執行身分
        "auth_method": ctx.auth_method,      # SSO / MFA
        "source_ip": ctx.source_ip,
    },
    ...
}

三、不可竄改性:寫入後不能改。

用 append-only 的儲存,或者用 Cloud Logging 的 _Required bucket(不可刪除、不可修改、固定保留 400 天)。

更強的做法是把稽核日誌寫到獨立專案,該專案的權限與業務專案完全分離——業務專案的最高權限也不能碰稽核專案的日誌。

四、可串接:一個請求的全流程能被還原。

這需要 trace ID 貫穿全鏈:

def handle_request(req):
    trace_id = req.headers.get("X-Trace-Id") or str(uuid4())
    ctx = Context(trace_id=trace_id, user=req.user)

    audit("document_upload", ctx, {"doc_hash": sha256(req.file)})
    text = extract(req.file, ctx)
    findings = detect(text, ctx)
    audit("detection", ctx, {
        "finding_count": len(findings),
        "types": Counter(f["type"] for f in findings),
    })
    deid = deidentify(text, findings, ctx)
    audit("deidentification", ctx, {"transform_count": len(findings)})
    result = call_llm(deid, ctx)
    audit("external_call", ctx, {
        "model": MODEL_NAME,
        "input_hash": sha256(deid),
    })
    return result

有了 trace_id,稽核可以問「這份摘要是從哪份文件來的、中間經過什麼處理」,然後把整條鏈拉出來。

日誌裡絕對不能有的東西

原文。 不用說。

findings 的 quote。 這是 D7 提過的 include_quote 陷阱。偵測結果如果包含被偵測到的文字,日誌就變成個資清單。

Token 的原值。 可以記 Token 的雜湊,不要記 Token 本身——因為 Token 加上 Vault 存取權就等於明文。

錯誤堆疊裡的變數內容。 這是最陰險的一個。異常處理如果把整個 request body dump 進日誌,前面所有努力都白費。

class SafeFormatter(logging.Formatter):
    SENSITIVE_KEYS = {"text", "content", "value", "quote",
                      "document", "body", "raw"}

    def format(self, record):
        if hasattr(record, "extra_data"):
            record.extra_data = {
                k: ("[REDACTED]" if k in self.SENSITIVE_KEYS else v)
                for k, v in record.extra_data.items()
            }
        return super().format(record)

而且要在例外處理器層級也做一次,因為第三方套件拋出的例外可能夾帶內容:

def safe_exception_handler(exc):
    log.error(
        "處理失敗:%s",
        type(exc).__name__,      # 只記類型
        extra={"trace_id": ctx.trace_id},
    )
    # 不要 log.exception(),它會帶出完整堆疊與區域變數

稽核日誌的結構

{
  "timestamp": "2026-03-15T14:23:07.123+08:00",
  "trace_id": "7f3a9c2e-...",
  "action": "reidentify",
  "outcome": "success",
  "actor": {
    "principal": "lawyer.a@example.com",
    "service_account": "reidentify-svc@project.iam",
    "auth_method": "SSO+MFA",
    "source_ip": "10.20.30.40"
  },
  "target": {
    "scope_id": "case:2026-CV-00123",
    "token_count": 3,
    "token_hashes": ["a3f9...", "b21c...", "c88d..."],
    "entity_types": ["PERSON", "PHONE_NUMBER"]
  },
  "justification": {
    "reason_code": "COURT_FILING",
    "reason_text": "準備民事起訴狀正本",
    "approval_ref": null
  },
  "policy_version": "v2.3.1",
  "key_version": "3"
}

policy_version 和 key_version 這兩個欄位常被漏掉,但它們讓你能回答:「當時用的是哪個版本的偵測規則?」——這在事後檢討「為什麼那筆沒被抓到」時是必要的。

主動偵測,而不是被動查詢

有了結構化日誌,可以做異常偵測。幾個值得設的告警:

訊號 可能意義
單一使用者單日還原量 > 平時 5 倍 批次竊取
非上班時間的還原 憑證盜用
連續多次還原失敗 探測攻擊
某個 scope 被大量不同使用者存取 權限設定錯誤
偵測 findings 數突然歸零 偵測引擎失效(靜默失效!)
同一 Token 短時間被多人還原 資訊擴散

倒數第二列最重要。 如果偵測引擎壞了、規則被誤刪、模型載入失敗,findings 數會變成 0,而系統看起來一切正常——文件照樣處理、照樣送出,只是沒有任何東西被保護。

這是 D3 威脅模型裡沒有列到但同樣致命的情境:不是被攻擊,是自己壞掉。

def check_detection_health(window_hours=1):
    stats = query_audit_logs(action="detection", hours=window_hours)
    if not stats:
        return
    zero_ratio = sum(1 for s in stats if s["finding_count"] == 0) / len(stats)
    baseline = get_baseline_zero_ratio()
    if zero_ratio > baseline * 3:
        alert("偵測引擎可能失效:零命中率異常升高")

保存期限

金融業的稽核紀錄保存要求依業務類型而異,個資相關的處理紀錄通常要留數年。實際年限要跟法遵確認,但有一個原則:

稽核日誌的保存期,應該長於它所記錄的資料的保存期。

因為 Vault 裡的 Token 對應清掉之後,你仍然需要能回答「這筆資料曾經存在、曾經被誰處理過」。日誌比資料活得久,才有意義。


明天談 fail-closed:偵測引擎掛掉的時候該怎麼辦。


關於作者

我是 Fngi,專注在 AI 資安、LLM 紅隊與 AI 治理框架落地。
IG:@aid3fend

有想討論的架構細節或不同意見,留言或私訊都歡迎。



上一篇
Day 19|還原授權:把 D2 的角色矩陣變成 RBAC
下一篇
Day 21|fail-closed:那個平常沒人在意、出事時決定一切的設計
系列文
《30 天為金融法務部門打造 LLM 個資防護閘:去識別化、可控還原與紅隊驗證》 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言