
這個系列寫到第十二篇終于輪到不少讀者催更的 docker secret 了。前幾篇我們把鏡像、容器、網(wǎng)絡、卷和 Compose 都過了一遍看起來一套服務已經(jīng)能跑得像模像樣了但有個問題一直繞不過去數(shù)據(jù)庫密碼、Redis 口令、API Token 這些敏感信息到底怎么安全地交給容器如果你也經(jīng)歷過“密碼直接寫環(huán)境變量結果 docker inspect 一眼看光”“配置文件里藏著數(shù)據(jù)庫賬號傳到代碼倉庫被同事圍觀”之類的尷尬那這篇內(nèi)容應該能幫你把這塊補上。Docker Secret 是 Docker 官方提供的一套敏感信息分發(fā)機制它把密碼這類數(shù)據(jù)從鏡像、環(huán)境變量和普通文件掛載中剝離出來按需、加密、臨時地交給運行中的容器。這篇博文會從設計動機講到 Swarm 和本地兩種模式的底層差異再帶你親手跑通創(chuàng)建、掛載、輪換、刪除的完整鏈路中間穿插我實際踩過的坑。適合已經(jīng)會基礎 Docker 操作、正準備把服務搬上生產(chǎn)的開發(fā)者也適合剛學完 Compose、想給單機部署加點安全感的同學。1. Docker Secret 到底解決了什么痛點1.1 常見傳密方式的隱患我先說一個很典型的場景。假設你部署了一個內(nèi)部系統(tǒng)前端 Nginx 反代后端 API 連接 MySQL 和 Redis。剛開始圖省事很多人會這樣干docker run -d --name api \ -e DB_PASSWORDroot123456 \ -e REDIS_PASSWORDredis123 \ myinternal/api:v1或者把密碼寫到 Compose 文件的 environment 下面??雌饋矸奖銓嶋H上問題不少。首先docker inspect能直接看到容器全部環(huán)境變量。任何一個能碰到 Docker Socket 的人都能把密碼撈出來。其次環(huán)境變量會被子進程繼承如果你的應用里有個打印環(huán)境變量的調(diào)試邏輯或者某個依賴庫異常時把環(huán)境變量帶進日志密碼就隨著日志流出去了。更隱蔽的問題是環(huán)境變量修改起來只能重建容器還很容易出現(xiàn)“開發(fā)機一套密碼、測試機一套密碼、生產(chǎn)機又一套密碼”的混亂局面。還有一種做法是把配置掛進去比如docker run -d -v /host-config/app.conf:/etc/app/app.conf myapp這種方式比環(huán)境變量好一些密碼至少沒進鏡像層但它把“敏感數(shù)據(jù)”和“普通配置”混在了一起。宿主機上配置文件是明文誰拿到這臺機器或者備份包誰就拿到全部密碼。而且一旦掛載卷需要修改權限、屬主、掛載方式都要重新核對維護成本不低。最糟糕的做法是把密碼寫死在鏡像里。比如在 Dockerfile 里寫ENV DB_PASSWORDroot123456或者在構建時 COPY 一個帶密碼的配置文件進去。這個隱患是致命的docker history可以把鏡像每一層的變更記錄翻出來密碼等于直接印在鏡像臉上。而鏡像一旦推送到倉庫相當于把密碼分發(fā)到了所有能拉取這個鏡像的地方。1.2 Secret 的本質與設計目標Docker Secret 就是為了解決上面這三類問題而設計的。它的核心思路是敏感信息不再隨著鏡像、環(huán)境變量、普通掛載文件流轉而是作為一個獨立的“密鑰對象”存在 Docker 里只在服務調(diào)度到某個容器時才把內(nèi)容以只讀文件的形式掛進容器內(nèi)部。你可以把它類比成寫字樓的門禁二維碼物業(yè)不會把整棟樓的鑰匙一串串掛在公共墻上環(huán)境變量更不會把鑰匙直接焊進每間辦公室的門框里鏡像寫死而是訪客到前臺登記后前臺才生成一個臨時二維碼。這個二維碼只對特定樓層、特定時間段有效用完即失效。落到技術上Secret 有幾個關鍵特性Secret 內(nèi)容在創(chuàng)建后不會通過普通 API 回傳docker secret inspect只能看到元數(shù)據(jù)看不到明文。Secret 以文件形式出現(xiàn)在容器中默認路徑是/run/secrets/secret_name它是只讀的 tmpfs 掛載不是持久化磁盤文件。Secret 與鏡像、容器解耦同一個 Secret 可以被多個服務引用也可以隨時切換引用關系。輪換時不用重建整個鏡像只需創(chuàng)建新 Secret 再切換服務引用。我最初用 Secret 之前也懷疑過就一個“掛文件”能比環(huán)境變量安全多少后來真正在 Swarm 集群里跑了一遍才意識到安全感主要來自“最小暴露”密碼不再散落在各種環(huán)境變量、鏡像層、宿主機目錄里了而是有了一個集中管理、按需下發(fā)的出口。2. 理解 Swarm 與本地兩種模式的底層差異2.1 兩種模式對比網(wǎng)上很多教程會告訴你“Docker Secret 是 Swarm 專屬功能”然后手把手教你docker swarm init。這在幾年前是對的但現(xiàn)在已經(jīng)不完全準確了。Docker 在較新的版本里支持了不依賴 Swarm 的本地 Secret 用法docker run --secret和 Docker Compose 的secrets都在往這個方向靠。不過要強調(diào)的是這兩種模式底層機制完全不同別以為本地模式就是 Swarm 模式的簡化版。我在實際使用中發(fā)現(xiàn)很多人切換模式后遇到各種問題根源就是沒搞清楚差異。維度Swarm 模式本地模式run / Compose存儲方式創(chuàng)建時加密后存入 Swarm 集群存儲宿主機文件系統(tǒng)上的明文文件分發(fā)機制管理節(jié)點解密后下發(fā)給工作節(jié)點掛載為 tmpfs容器啟動時直接綁定掛載宿主機文件依賴條件必須先初始化 Swarm 或加入集群無需 Swarm普通 Docker 環(huán)境即可容器內(nèi)形態(tài)/run/secrets/name只讀 tmpfs/run/secrets/name只讀綁定掛載適用場景多節(jié)點生產(chǎn)集群、需要集中管理密鑰單機開發(fā)、CI、簡單部署安全邊界相對高數(shù)據(jù)不落工作節(jié)點持久化磁盤相對低宿主機明文文件即密鑰本身2.2 Swarm 模式的分發(fā)流程Swarm 模式下docker secret create會把內(nèi)容加密后寫入集群的存儲層。當你創(chuàng)建一個 service 并指定某個 secret 時調(diào)度器會在任務分配到某個工作節(jié)點后把解密后的內(nèi)容以 tmpfs 的形式掛到容器的/run/secrets目錄里。這意味著幾個很重要的事實Secret 的內(nèi)容不會因為容器重啟而重新讀取宿主機文件因為它的生命周期跟任務綁定工作節(jié)點的普通磁盤上沒有這個 Secret 的持久化副本容器停下來tmpfs 一卸載內(nèi)容就沒了管理節(jié)點也不能隨便把某個 Secret 的明文導出來這是設計上的單向通道。我在剛開始接觸時犯過一個錯誤以為 Secret 像配置文件一樣宿主機的某個路徑下有個原始文件改了文件容器里就會變。實際完全不是這樣。Secret 是在創(chuàng)建那一刻被“快照”下來的之后它就是一個獨立對象跟來源文件、echo 命令都沒關系了。想改內(nèi)容只能創(chuàng)建一個新的 Secret。2.3 本地模式的安全邊界本地模式的原理簡單很多Docker 把宿主機上一個文件以只讀方式掛到容器里路徑通常還是/run/secrets/name。它不需要 Swarm沒有加密存儲也沒有集群分發(fā)。本質上就是一個“按路徑約定好的只讀文件掛載”。正因為這樣本地模式的安全邊界很清晰宿主機上那個源文件必須是受保護的。如果源文件權限是 644任何能登錄宿主機的用戶都能直接讀如果源文件被提交進 Git 倉庫密碼就進了代碼歷史這就跟環(huán)境變量泄漏沒什么區(qū)別了。我的建議是本地模式可以用于開發(fā)環(huán)境、內(nèi)網(wǎng)單機部署、CI 臨時跑測試但不要因為用起來簡單就把它當成生產(chǎn)環(huán)境的安全方案。多節(jié)點生產(chǎn)還是優(yōu)先走 Swarm 模式或者考慮更專業(yè)的密鑰管理系統(tǒng)。3. 從零跑通一個 Swarm 下的 Secret 實戰(zhàn)3.1 環(huán)境準備與 Swarm 初始化先確認環(huán)境狀態(tài)。Docker 自帶的docker info就能看到 Swarm 狀態(tài)docker info | grep -i swarm如果輸出類似Swarm: inactive說明當前沒開 Swarm。單節(jié)點學習時直接初始化即可docker swarm init --advertise-addr 127.0.0.1這里提一下--advertise-addr它決定了其他節(jié)點怎么訪問當前節(jié)點的管理接口。單機測試用回環(huán)地址沒問題但是如果你后面要加工作節(jié)點這里就得填一個其他節(jié)點能訪問到的 IP。初始化完成后可以看一眼集群狀態(tài)docker node ls看到Leader字樣就說明 Swarm 控制面可用了。這一步不做的話后面的docker secret create會直接報錯因為 Secret 的存儲依賴 Swarm 的集群存儲。3.2 創(chuàng)建 Secret 的三種方式進入正題。創(chuàng)建 Secret 最常用的方式是從標準輸入讀取echo prod-db-pass-2024 | docker secret create db_pass -注意末尾的橫線-它表示從 stdin 讀取內(nèi)容。我推薦配合printf使用避免echo默認追加換行符把密碼弄臟printf root | docker secret create mysql_root_pass -如果密鑰內(nèi)容在文件里也可以直接指定文件名docker secret create app_key ./app_key.txt這種方式適合從已有的密鑰文件遷移。不過要提醒一句文件名里面的內(nèi)容會被完整讀取作為 Secret 的快照之后這個文件本身怎么處理都影響不到已經(jīng)創(chuàng)建的 Secret 了。創(chuàng)建完可以查看列表docker secret ls想確認某個 Secret 的元數(shù)據(jù)用docker secret inspect db_pass注意了這里你會看到名稱、ID、創(chuàng)建時間但看不到內(nèi)容。這不是因為我操作有誤而是設計如此Secret 的內(nèi)容不通過 API 回傳誰都不能事后從 Docker 里倒推明文。所以重要 Secret 一定要在外部有安全備份比如密碼管理器否則丟了只能重新創(chuàng)建。3.3 服務掛載與容器內(nèi)驗證創(chuàng)建完 Secret接下來把它掛給一個服務。傳統(tǒng) Swarm 場景里這不是docker run --secret而是寫在docker service create的參數(shù)里docker service create \ --name demo-web \ --secret db_pass \ --publish 8080:80 \ nginx:alpine容器啟動后Secret 會出現(xiàn)在默認路徑/run/secrets/db_pass。進入容器驗證一下docker service ps demo-web CONTAINER ID$(docker ps --filter namedemo-web -q | head -1) docker exec -it $CONTAINER_ID sh cat /run/secrets/db_pass正常會輸出你剛剛創(chuàng)建的內(nèi)容。再留意一下文件權限ls -l /run/secrets/輸出通常是-r--r--r--之類反正不是普通配置文件那種可寫狀態(tài)。這個只讀屬性就是設計的一部分——防止運行中的程序意外篡改密鑰。如果你不想用默認路徑和文件名service 創(chuàng)建時可以用更精細的語法控制docker service create \ --name demo-web2 \ --secret srcdb_pass,target/etc/demo/pass.txt,mode0400 \ nginx:alpine這里src指定 Secret 名稱target指定容器內(nèi)路徑mode指定權限。這個寫法在“容器以非 root 用戶運行”的場景里特別重要后面我會專門講。4. 容器內(nèi) Secret 文件的變化機制與驗證4.1 /run/secrets 到底是什么在開始調(diào)戲 Secret 之前先搞清楚容器里看到的/run/secrets/db_pass到底是什么。它不是普通的文件也不是你宿主機上某個持久化目錄的映射它是 tmpfs內(nèi)存文件系統(tǒng)上的一份只讀副本。你可以把它理解成快遞柜里的臨時格口快遞密鑰內(nèi)容被放進去柜門鎖上收件人看完取走格口就釋放了。容器重啟、服務滾動更新、任務被調(diào)度到別的節(jié)點舊的格口會銷毀新的任務會在新節(jié)點上重新分配格口。這個設計和卷Volume有本質區(qū)別。卷是持久化存儲改宿主機上的文件容器里馬上能看到Secret 則相反它在創(chuàng)建時快照、調(diào)度時下發(fā)、生命周期綁定容器任務。所以別指望“我在宿主機某個目錄改一下 Secret 內(nèi)容容器里就自動更新”這種操作那不符合它的設計。4.2 更新 Secret 后容器內(nèi)會怎樣很多人第一次用 Secret 時都會遇到一個困惑我改了 Secret為什么容器里的文件沒變這里要澄清一個關鍵點Docker Secret 本身支持修改內(nèi)容的操作嗎不支持。同一個名字的 Secret 創(chuàng)建后內(nèi)容就不可變了。你要換內(nèi)容必須創(chuàng)建一個新的 Secret 對象再讓服務切換引用。切換引用時Swarm 會觸發(fā)服務的滾動更新。命令大概長這樣printf new-password-2025 | docker secret create db_pass_v2 - docker service update --detachfalse \ --secret-rm db_pass \ --secret-add db_pass_v2 \ demo-web執(zhí)行后Swarm 會逐個替換服務任務。新的任務容器里掛載的是db_pass_v2的內(nèi)容舊任務在收斂前可能還在運行很短時間。如果你的應用對數(shù)據(jù)庫密碼特別敏感切換時最好讓新舊密碼在上游系統(tǒng)比如 MySQL 里同時生效一小段時間這就是所謂的“雙密碼窗口”。我最早做輪換時沒考慮這個結果服務更新瞬間出現(xiàn)過連接被拒后來就學乖了。4.3 如何驗證更新生效驗證流程也很簡單。先看服務任務狀態(tài)docker service ps demo-web找到新啟動的任務對應的容器進入容器再cat /run/secrets/db_pass就能確認內(nèi)容已經(jīng)變成新密碼。如果文件名變了比如從 db_pass 換成了 db_pass_v2但 target 沒有指定記得路徑也會變應用的配置文件或啟動腳本要同步調(diào)整。這里也順便提一個最容易忽略的點應用通常不會自動感知 Secret 文件變化。即使 Swarm 已經(jīng)把新 Secret 掛上去了你的進程如果還在讀舊配置或者連接池還握著舊密碼一樣會失敗。所以輪換之后應用程序要能感知文件變化或者配合重啟、信號刷新機制。這個問題在 Nginx、Java 應用里尤其明顯。5. Secret 輪換、更新與刪除的完整鏈路5.1 為什么輪換是個流程而不是一條命令很多教程講 Secret 就停在“創(chuàng)建、掛載、查看”三步等真要換密碼時讀者就卡住了。Secret 是不可變對象這意味著輪換必然是一個多步驟流程不是一個docker secret update就能解決的問題。我總結的輪換順序是這樣的先在上游系統(tǒng)數(shù)據(jù)庫、第三方服務里設置新密碼并保留舊密碼一小段時間。創(chuàng)建新內(nèi)容的 Secret 對象。用docker service update --secret-rm 舊名 --secret-add 新名切換引用。等待服務收斂確認新任務容器里讀取的是新內(nèi)容。確認業(yè)務正常后再清理舊的 Secret 對象。順序很重要。如果先刪舊 Secret 再建新的中間服務一旦滾動更新可能同時引用一個不存在的對象直接導致調(diào)度失敗。如果先切換再在上游改密應用連的還是舊密碼就會被拒絕。5.2 腳本化輪換示例手工執(zhí)行一兩次可以但例行輪換比如三個月一次還是寫成腳本更靠譜。下面是一個簡化但可用的輪換腳本#!/usr/bin/env bash set -euo pipefail SERVICE_NAMEdemo-web OLD_SECRETdb_pass NEW_SECRETdb_pass_$(date %Y%m%d%H%M%S) printf your-new-password-here | docker secret create $NEW_SECRET - docker service update --detachfalse \ --secret-rm $OLD_SECRET \ --secret-add $NEW_SECRET \ $SERVICE_NAME docker secret rm $OLD_SECRET幾個細節(jié)值得說明set -euo pipefail保證腳本在中間出錯時立刻退出避免半輪換狀態(tài)新 Secret 名稱帶時間戳方便回溯--detachfalse讓命令等待服務更新完成再繼續(xù)否則可能在任務收斂前就刪了舊 Secret引發(fā)調(diào)度錯誤。密碼內(nèi)容如果不想寫死在腳本里可以結合openssl rand -base64 24生成隨機內(nèi)容或者從密碼管理器的 CLI 取。不過那些就超出本文范圍了只說一句別把真實密碼直接 echo 到 shell 歷史里。5.3 刪除 Secret 的約束清理過期 Secret 用的是docker secret rm db_pass有個約束很容易踩到正在被某個服務引用的 Secret 刪除會報錯。這其實是一種保護機制防止你把還在用的密鑰刪掉。所以刪除前應該先確認所有服務的--secret引用都已經(jīng)切換走docker service inspect demo-web --format {{json .Spec.TaskTemplate.ContainerSpec.Secrets}}看到輸出里沒有舊 Secret 的引用再刪就比較安全了。另外服務被刪除后它引用的 Secret 不會自動刪除。也就是說docker stack rm或者docker service rm之后舊 Secret 還躺在集群里。這是我見過很多人忽略的點清理服務卻沒清理 Secret結果docker secret ls里一堆過期的“僵尸密鑰”。建議服務下線時把對應的 Secret 也順手清掉。6. 不初始化 Swarm 也能用 Secret本地模式正解6.1 本地模式的開啟條件前幾節(jié)全是 Swarm 場景。但如果你只是單機跑幾個容器或者想在本機寫個 Demo不想為這點事兒初始化 Swarm那也有一條路本地 Secret 模式。這個模式最早跟 Docker Compose 綁定后來 Docker Engine 在較新版本里也支持了docker run --secret的用法。我建議以 Docker 官方文檔為準如果你的版本比較舊就用 Compose 方式最穩(wěn)妥。6.2 Compose secrets 配置示例一個典型的compose.yaml長這樣services: app: image: myinternal/app:v1 secrets: - db_password - app_token environment: - DB_PASSWORD_FILE/run/secrets/db_password secrets: db_password: file: ./secrets/db_password.txt app_token: file: ./secrets/app_token.txt啟動后容器內(nèi)默認路徑就是/run/secrets/db_password和/run/secrets/app_token。如果你的應用只支持從環(huán)境變量讀配置可以在 environment 里指一個文件路徑讓應用自行讀取文件內(nèi)容。這樣既用上了“敏感信息不直接進環(huán)境變量”的好處又兼容了老程序。如果對路徑有特殊要求可以在容器的 secrets 配置里寫 targetservices: app: secrets: - source: db_password target: /etc/app/db_pass uid: 1000 gid: 1000 mode: 0400這個寫法跟 Swarm service create 里--secret src...,target...,uid...,mode...是同一個思路。6.3 本地模式的實際注意點本地模式用起來確實方便但在安全上的定位要清楚Swarm 模式好歹有集群存儲和加密機制本地模式就是“宿主機文件明文 只讀掛載”防的不是宿主機入侵者而是防止這些密鑰不小心進入鏡像層、環(huán)境變量或代碼倉庫。所以使用時有幾條紀律源文件權限盡量收緊一般chmod 600起步源文件路徑別暴露給所有用戶共享的目錄更重要的是絕對不要把secrets:下面的file:指到代碼倉庫目錄里的某個文件然后把整個倉庫提交到遠端。我見過不止一次Compose 寫得挺規(guī)范結果./secrets/password.txt被提交到 Git 歷史里等于白干。開發(fā)調(diào)試階段本地模式完全可以替代環(huán)境變量暴力傳參。生產(chǎn)環(huán)境我建議還是回到 Swarm 模式或者根據(jù)實際情況選擇更完整的密鑰管理方案。7. 生產(chǎn)環(huán)境用 Secret 最容易踩的五個坑7.1 坑點總覽坑現(xiàn)象解法容器以非 root 用戶運行讀不到 Secretcat報 Permission denied創(chuàng)建 Secret 引用時指定 uid / gid / mode把 Secret 內(nèi)容寫進 Compose 內(nèi)聯(lián)配置密碼出現(xiàn)在代碼倉庫對外用file:引用并加入 .gitignore修改宿主機源文件以為容器自動生效應用還是舊值理解 Secret 不可變輪換必須建新對象并切換引用大段配置也塞進 Secret超出大小限制維護混亂敏感短數(shù)據(jù)用 Secret普通配置用 Docker Config 或卷環(huán)境變量傳參的舊代碼沒改容器內(nèi)找不到 /run/secrets 路徑改代碼從文件讀取或在應用層做兼容7.2 非 root 用戶讀取 Secret 這個坑再多說幾句這是我在實際部署中踩過最深的一個坑。很多基礎鏡像出于安全考慮進程以非 root 用戶運行比如 UID 1000。而 Swarm 模式掛載的 Secret 文件默認屬主是 root權限通常是 0444即 root 能讀其他用戶雖然名字上寫著可讀但實際因為目錄權限或 mount 方式的關系非 root 進程可能cat時直接報權限拒絕。我當時排查了很久最后在創(chuàng)建 service 時加了三件套才解決docker service create \ --name demo-web \ --secret srcdb_pass,target/run/secrets/db_pass,uid1000,gid1000,mode0400 \ nginx:alpine或者 Compose 里對應寫secrets: - source: db_pass target: /run/secrets/db_pass uid: 1000 gid: 1000 mode: 0400驗證方法也很簡單進容器后用應用同款 UID 試一下docker exec -u 1000 container cat /run/secrets/db_pass這一步能過說明非 root 進程也能正常讀到密鑰了。如果你用的是自定義鏡像建議把 UID / GID 的數(shù)值跟鏡像里用戶的 UID 保持一致避免看著都是 1000 實際卻對不上的問題。7.3 配置與密鑰要分開最后一個經(jīng)驗總結是Docker Secret 跟 Docker Config 要分清楚別把什么文件都往 Secret 里塞。Secret 適合的是短小的、敏感的、需要輪換的內(nèi)容比如數(shù)據(jù)庫密碼、第三方 Token、私鑰。普通配置比如超時時間、服務地址、業(yè)務開關應該走 Docker Config 或者卷掛載。把它們混在一起的話輪換就很痛苦——你只想換一個 Token結果整個 Secret 里塞了一堆業(yè)務配置不得不全都重新生成一遍風險面反而擴大了。分開管理以后每次輪換的改動范圍、影響面都能控制得清清楚楚。如果你也是從環(huán)境變量遷到 Secret 這邊的我的經(jīng)驗是先做輪換流程再遷數(shù)據(jù)先把新密碼在數(shù)據(jù)庫側配好再創(chuàng)建新 Secret、切換服務、驗證、清理全部走通一遍之后再拆掉舊路徑。這樣遷移過程中任何一步出問題都可以隨時回滾到舊方案不至于整個服務當場不可用。