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