容器鏡像的成分分析:比源碼掃描多出來的那幾層
一個常見的困惑
團隊把 SCA 接進了 CI,源碼掃描乾乾淨淨,結果安全團隊用鏡像掃描器一掃,報出幾十個高危漏洞。開發的第一反應通常是「掃描器有問題」。
掃描器沒問題。兩邊掃的根本不是同一個東西。
鏡像裏到底有什麼
一個典型的應用鏡像包含三層性質不同的組件:
第一層:基礎鏡像(OS 層)——你在 Dockerfile 第一行寫的 FROM。它帶來一整個操作系統的用户空間:glibc、openssl、bash、coreutils、包管理器本身。這一層的組件數量通常遠超應用依賴,而且完全不在任何應用清單文件裏。
第二層:系統包——構建過程中 apt-get install 或 apk add 裝進去的東西:curl、git、編譯工具鏈、各種運行時庫。它們經常是為了構建而裝、裝完忘了刪,最終留在生產鏡像裏。
第三層:語言依賴——package.json、pom.xml、requirements.txt 裏聲明的那些。這一層,也只有這一層,是源碼 SCA 能看到的。
所以「源碼掃描乾淨但鏡像有漏洞」根本不矛盾:問題出在你沒寫、但確實在運行的那兩層裏。
為什麼基礎鏡像是風險重災區
基礎鏡像的問題不在於它質量差,而在於三個結構性因素:
體積即攻擊面。 一個完整的 ubuntu:22.04 帶着幾百個包,其中絕大多數你的應用根本用不到——但它們的漏洞照樣會被掃出來,照樣需要你解釋或修復。
更新滯後。 FROM node:18 這樣的寫法看似鎖定了版本,實際上標籤是浮動的,而團隊往往幾個月才重建一次鏡像。基礎鏡像的安全更新不會自動到達你的生產環境。
繼承的不可見性。 你的鏡像 FROM 了某個團隊的中間鏡像,那個鏡像又 FROM 了別的。最終生產鏡像裏的組件,可能來自三四層之外的某個決定,而沒有人完整地看過這條鏈。
三條治理原則
一、選擇精簡且有維護承諾的基礎鏡像。
distroless、官方 slim 變體、alpine——選擇的核心不是「哪個最小」,而是「哪個在滿足運行需求的前提下組件最少,且有明確的安全更新節奏」。少一個包就少一個潛在的 CVE 和一次不必要的解釋。
二、鏡像掃描進 CI,與源碼掃描並行。
兩者互補而非替代:源碼 SCA 在依賴引入時攔截(更早、修復成本更低),鏡像掃描在交付物層面兜底(更全、覆蓋你沒聲明的部分)。理想的門禁位置是鏡像構建之後、推送到倉庫之前——此時還能重建,推上去再發現就是回滾了。
三、基礎鏡像版本全公司統一管理。
不是每個團隊各自選 FROM,而是公司維護一組經過安全審查的基線鏡像,各團隊從基線派生。這樣做的收益在漏洞爆發時最明顯:修一次基線,所有下游重建即可,而不是二十個團隊各查各的。
SBOM 應該覆蓋到哪一層
如果你要給客户或監管提交容器化產品的 SBOM,只導出語言依賴是不夠的——收方期待的是「這個鏡像裏有什麼」,而不是「這個應用聲明瞭什麼」。
完整的容器 SBOM 應當包含:基礎鏡像標識與摘要、系統包清單(含版本)、語言依賴樹、以及各層的來源信息。好在這件事是可自動化的:在 CI 裏對構建產出的鏡像做一次成分分析,導出 SPDX 或 CycloneDX,隨鏡像一起發佈。
需要留意的是層的可追溯性:同一個組件可能出現在多個層,來源不同、修復路徑也不同。基礎鏡像裏的 openssl 要靠換基礎鏡像修,應用依賴裏的要靠改依賴聲明修——SBOM 如果不區分來源,收到的人無法判斷該找誰修。
從「掃描通過」到「成分清楚」
容器化把交付物從「一個 jar 包」變成了「一整個運行環境」,這讓軟件供應鏈的邊界向下擴展了兩層。相應地,成分透明的要求也必須跟着下沉。
值得強調的是:這不只是安全問題,也是許可證問題。基礎鏡像裏的 GPL 組件、系統包的許可證義務,同樣會隨着鏡像分發而傳遞。只看應用依賴的合規審計,在容器化交付面前是不完整的。
---
延伸閲讀:SBOM 字段實操指南 · 在 CI 流水線裏配置 SCA 門禁 · CleanSource SCA 的容器鏡像分析能力
