機密(token、金鑰、密碼)的正確放法是 runtime 注入:不烤進 image layer、也不提交進 git(連私有 repo 都不),而是存成 host 側的 gitignored 檔或 secrets manager、在 container 啟動時用環境變數 / --env-file 注入。理由是 image layer 與 git repo 都不是機密容器——它們會被複製、快取、分享、永久留存,機密一旦進去就跟著到處跑、且很難真正抹除。把機密跟可分享的 artifact(image、Dockerfile、設定)分離,是這條原則的核心。這條分離原則跟 Docker named volume 掛載點 owner 同屬 image build 期該決定什麼的範疇。

概念位置

私鑰不外流的同源原則見 SSH 金鑰儲放與 authorized_keys

image layer 不是機密

Dockerfile 裡 ENV TOKEN=...COPY 一個機密檔進去,那顆機密就烤進某一層 image。docker history --no-trunc 讀得到 ENV 值與 build 參數、解開 layer 就拿得到 COPY 進去的檔。image 一旦 push 到 registry、被 CI 快取、被別人 pull,機密就跟著散出去。即使你在後面的 layer 刪掉它,前一層仍留著——layer 是疊加的、刪除不會回溯抹除。

私有 repo 也不是保險箱

私有 repo 降低機密曝露、但不消除:有讀取權的協作者、CI / 整合服務、不小心設成公開、fork、以及 git 歷史永久留存(後來刪掉也還在歷史裡、要 rewrite 且視同已洩)都是曝露面。長效機密(例如有效一年的 token)blast radius 大,不值得為省事賭在「這個 repo 一直是私有」上。這也是 secret-scanning 工具存在的理由——它們專門掃 repo 裡不該出現的機密。

正解:runtime 注入、機密與 artifact 分離

把三件事分開放:

  • image / Dockerfile / 設定 → 進 repo(公開或私有都行、因為不含機密)。
  • 機密 → host 側的 gitignored 檔(如 .env、權限 600)或 secrets manager。
  • 注入docker run --env-file .env(或 -e、Docker secrets)在 runtime 把機密餵進去。

這讓 image 是可重現、可分享的乾淨 artifact、機密則跟著環境走。認證因此也跟 image 生命週期解耦:rebuild image 不影響機密、換機密只改注入的檔。

判讀訊號 / 邊界

  • 想「把 token 寫進 Dockerfile 省事」時就是這條原則該擋下的時刻——改成 .env + 注入。
  • build-time 真的需要機密(例如 build 時要拉私有套件)時,別用 ARG / ENV(會進 layer),用 BuildKit 的 --mount=type=secret——它在 build 期掛載、不寫進 image layer。多數應用機密只在 runtime 用、連這個都不需要。
  • 輪替:機密到期前換、裝置遺失或疑似外洩就撤銷重發。gitignored .env 對單機個人用途夠;多機 / 團隊改用 secrets manager。
  • env 注入自身也有曝露邊界:runtime 注入解掉「進 image / 進 git」的問題,但環境變數不是零曝露——同機上的程序讀得到自己的 /proc/<pid>/environdocker inspect 讀得到用 -e 傳的值。所以「注入」降低的是持久化與散佈風險,不是同機讀取風險;跑在容器裡、有網路的不受信任程式仍可能讀到並外送,長效機密要短期輪替、不長放。