)
前陣子幫一個朋友的公司處理告警排查一臺沒有業(yè)務流量的測試服務器。機器 CPU 莫名其妙長時間跑滿top 里看到幾個陌生進程再翻 Docker 容器列表多了好幾個從未部署過的鏡像。順著一查原因簡單到有點諷刺這臺機器上的 Docker 未授權 API 直接暴露端口 2375 裸奔在公網(wǎng)任何人都能連上來創(chuàng)建容器、執(zhí)行命令。這類問題就是典型的“Docker 未授權 API 逃逸”一句話概括Docker 守護進程的 API 被未授權訪問入侵者通過創(chuàng)建惡意容器或直接操作守護進程最終拿到宿主機上的執(zhí)行能力。這文章我打算從實際運維視角聊透兩件事一是這問題為什么會存在、攻擊面到底在哪二是我自己的巡檢、加固、排障思路。不堆概念重點放在可落地的命令、配置和踩坑經(jīng)驗上。1. 未授權訪問鏈路拆解Docker API 如何變成宿主機入口1.1 Docker 的三條通信通道與最常見的裸奔方式Docker 的命令行工具本身不干活所有docker run、docker exec、docker ps最終都是通過 REST API 與守護進程 dockerd 通信。也就是說只要掌握了 API 的訪問能力就等同于掌握了 Docker 的完整操作權限。守護進程默認提供三種通信通道通道類型默認位置安全性典型說明Unix Socket/var/run/docker.sock依賴文件權限本機進程通過 Socket 訪問最安全TCP 2375無完全沒有加密和認證最常見的不安全暴露方式TCP 2376無需配置 TLS 證書相對安全的遠程訪問方式裸奔的最常見原因是啟動 dockerd 時帶了類似-H tcp://0.0.0.0:2375的參數(shù)或者配置文件里寫了hosts: [tcp://0.0.0.0:2375]同時沒有開啟 TLS 驗證。這種操作在中文互聯(lián)網(wǎng)的老教程里相當常見很多人從測試環(huán)境復制配置到生產環(huán)境順手把外網(wǎng)映射也一起帶了過去。另一個隱患來源是管理節(jié)點的統(tǒng)一部署腳本。有些內部工具為了簡化操作在安全組或防火墻里直接放行了 2375 端口又沒有做來源 IP 限制。測試連得方便了暴露面也跟著擴大。1.2 未認證調用者能獲得什么一旦 2375 端口可訪問攻擊者不需要任何憑證就能調用 Docker API 的大部分能力讀取容器列表、鏡像列表、守護進程信息拉取任意鏡像并運行容器在已運行容器內執(zhí)行命令刪除或停止任意容器修改鏡像標簽、導出容器文件系統(tǒng)這些能力放在普通運維眼里只是“方便”放在攻擊者眼里就是遠程指令執(zhí)行。因為 dockerd 本身以 root 權限運行它創(chuàng)建的容器進程、處理的掛載操作、執(zhí)行的命令全部繼承 root 權限。調用方能不能真正控制宿主機取決于調用方提出什么樣的容器創(chuàng)建請求。打個比方這就好比物業(yè)把整棟樓的總控制臺搬到了門口而且不設門禁。任何一個路過的人都能進去按幾個按鈕把樓里某個房間的門拆了甚至把地下室的設備間打開。1.3 為什么未授權 API 場景下繞不開“逃逸”話題很多人會把容器逃逸理解成容器內攻擊者通過內核漏洞突破隔離。但在 Docker 未授權 API 的語境下逃逸往往不需要漏洞作為前提。攻擊者通過 API 創(chuàng)建容器時可以指定特權模式、宿主網(wǎng)絡、掛載宿主機目錄等參數(shù)。這個操作本身發(fā)生在 Docker API 層合法得不能再合法。只要守護進程接受了這個創(chuàng)建請求一個看似“普通”的容器實際上就獲得了宿主機文件系統(tǒng)的訪問邊界。之后的“逃逸”只是順水推舟——讀一下宿主機掛載的/etc和/root寫一個 SSH 公鑰或者計劃任務回頭再通過正常通道登錄宿主機。所以對運維來說必須把 Docker API 訪問控制當成宿主機安全的第一道大門。門沒關好后面討論再多的 seccomp、AppArmor、Capabilities 都是徒勞。2. 自查巡檢三步定位裸奔的 Docker 服務與其等人來打不如先自己查。下面的步驟不需要額外工具在目標機器上直接執(zhí)行就能有個初步結論。2.1 查監(jiān)聽端口與守護進程啟動參數(shù)第一步看端口和進程參數(shù)ss -lntp | grep -E :(2375|2376)\b ss -lx | grep docker ps -ef | grep dockerd如果ss -lntp沒有任何 2375 或 2376 輸出只剩一個/var/run/docker.sock的 Socket 監(jiān)聽那網(wǎng)絡層風險基本排除。一旦看到tcp://0.0.0.0:2375這樣的啟動參數(shù)不用猶豫這就是高危信號。再順手看一眼配置文件cat /etc/docker/daemon.json 2/dev/null systemctl cat docker.servicedaemon.json里的hosts字段是最容易藏問題的地方。有些時候命令行參數(shù)看著正常但配置文件里的hosts覆蓋了默認行為結果照樣開了一個 TCP 監(jiān)聽口。2.2 不帶證書直接探測 API 響應端口開著不代表一定出問題還要確認是否真的能訪問、是否真的沒啟用 TLS。本地探測幾條命令足夠curl -s http://127.0.0.1:2375/info curl -s https://127.0.0.1:2376/version curl -sk https://127.0.0.1:2376/version第一條請求如果返回類似{ID:...}的 JSON 內容說明 2375 就是一個完全開放的 API連證書都沒有。對 2376 端口分別用帶-k和不帶-k的方式訪問。如果服務端配置了 TLS不帶-k會報證書錯誤帶-k能返回版本信息說明服務端啟用了 HTTPS 但客戶端尚未配置證書。如果兩條請求都失敗端口可能被防火墻攔截或者服務并沒有正常監(jiān)聽。2.3 排查容易被忽略的“偽安全”狀態(tài)有一種情況特別容易騙過直覺Docker 配置了 TLS但證書和來源 IP 控制都沒做或者只對 2375 做了 IP 白名單白名單覆蓋的是整個內網(wǎng)網(wǎng)段。內網(wǎng)一旦被橫向滲透這個口子照樣瞬間被打穿。還有一些隱患藏在環(huán)境變量和登錄配置里env | grep -i docker cat ~/.docker/config.json有些人會把DOCKER_HOST指向局域網(wǎng)內的某臺 Docker 主機還習慣性關掉 TLS 驗證。這種客戶端配置一旦泄露等于替別人準備了訪問憑證。自查時也要把這一類配置一并盤一遍畢竟攻擊者經(jīng)常從配置文件里找突破口。3. 容器逃逸的典型路徑與最小權限防守邏輯就算把 2375 端口封住了容器安全也不能到此為止。理解逃逸路徑的存在形式才能知道該在哪些層面做限制。這里我不展開具體的利用鏈構造方式只從防守視角講清楚風險點和對應的控制策略。3.1 特權容器與宿主機目錄掛載最直接的越界手段Docker 的隔離不是虛擬機級別的隔離。內核是同一個設備是同一批真正起作用的是命名空間、CGroup 和 Capabilities 這套組合機制。一旦創(chuàng)建容器時指定了特權模式就等于告訴內核這個容器不做隔離。典型的危險參數(shù)組合包括--privileged關閉絕大部分隔離限制--pidhost共享宿主進程空間--networkhost共享宿主網(wǎng)絡棧-v /:/host把宿主機根目錄掛進容器在未授權 API 場景下攻擊者不需要交互式登錄只需要發(fā)送一個容器創(chuàng)建請求把這些參數(shù)帶上然后等待容器啟動。對 Docker API 來說這是合理調用沒有任何異常提示。映射到實際危害掛載宿主機根目錄后攻擊者可以直接讀取宿主機/etc/shadow、數(shù)據(jù)庫備份、業(yè)務配置修改/root/.ssh/authorized_keys或者往 crontab 里寫入持久化任務。整個過程可以完全繞開容器逃逸漏洞的概念走的是配置層面的“合法越權”。防守邏輯很簡單容器運行時參數(shù)必須最小化。不允許任何業(yè)務容器使用特權模式不允許掛載宿主機敏感目錄不允許共享宿主網(wǎng)絡和進程空間。這些限制如果只靠人肉審查早晚會漏最好通過鏡像倉庫策略和 Kubernetes 準入控制器一類的自動化門禁來強制。3.2 容器內掛載 Docker Socket 的連鎖反應另一個典型逃生路徑藏在 CI/CD 和監(jiān)控工具里。很多容器化監(jiān)控組件或構建流水線為了讓容器內的程序能管理 Docker直接把/var/run/docker.sock掛載到容器內部。表面上這是給應用開了個“管理口子”實際上等于把宿主機鑰匙交給了容器內進程。如果這個容器存在命令注入、WebShell、反序列化漏洞攻擊者先拿下容器內的執(zhí)行權再通過掛載進去的 Socket 調用宿主機 Docker API。后續(xù)就和未授權 2375 一樣創(chuàng)建特權容器、掛載宿主目錄、獲得宿主機權限。這類問題的根源不是 Socket 本身有問題而是容器獲得了一個超出業(yè)務需求的宿主機控制權。如果一個 Web 應用根本不需要調用 Docker API就不該把 Socket 掛進去。確實需要用的場景也應該通過帶鑒權的代理來轉發(fā)并且限定允許調用的 API 子集和來源 IP。3.3 命名空間與能力機制理解逃逸才能防御把命名空間、Capabilities、Seccomp 這三者放在一起看容器逃逸的本質就很清楚了攻擊者在容器內獲得了某種超出預期的高權限能力然后利用這個能力去操作容器外部的資源。舉個例子普通容器默認使用非特權用戶、有限 Capabilities即便進程拿到了一定權限也無法對宿主機設備進行直接讀寫。但如果你給容器加了CAP_SYS_ADMIN這個能力本身就可能導致掛載、命名空間操作等危險行為如果再疊加一些系統(tǒng)調用上的漏洞逃逸就不只是理論可能。所以防守不能只看 API 端口還要構建一個縱深體系API 層關閉未授權訪問、啟用 TLS、來源白名單運行層去掉特權模式、去掉多余 Capabilities內核層啟用 Seccomp 和 AppArmor 默認策略業(yè)務層鏡像最小化、進程以非 root 用戶運行每一層都可能被繞過但每一層都增加了攻擊成本。安全方案如果有一層沒做問題往往就出在這一層。4. Docker daemon 加固實操從止血到完整 TLS 方案4.1 先止血立刻關閉公網(wǎng)監(jiān)聽端口發(fā)現(xiàn) 2375 對外監(jiān)聽后第一時間是止血而不是研究高級防御。把啟動參數(shù)或配置文件里的 TCP 監(jiān)聽去掉只保留本地 Socket。修改兩個地方之一即可修改/etc/docker/daemon.json{ hosts: [unix:///var/run/docker.sock] }修改 systemd 服務文件/lib/systemd/system/docker.service把啟動參數(shù)里的-H tcp://0.0.0.0:2375去掉只保留ExecStart/usr/bin/dockerd -H fd:// --containerd/run/containerd/containerd.sock保存后執(zhí)行systemctl daemon-reload systemctl restart docker重啟前先確認本地容器存量不大、業(yè)務可接受短暫中斷。如果機器上跑著數(shù)據(jù)庫或核心服務先變更最小化操作或等業(yè)務窗口再重啟。如果業(yè)務確實需要遠程管理 Docker我的建議是優(yōu)先走 SSH 隧道或者堡壘機。在跳板機上執(zhí)行遠程命令不直接開放 Docker 的 TCP 端口這個方案最穩(wěn)。4.2 生成證書并為 Docker API 啟用 TLS 驗證如果遠程調用確實避不開那就必須上 TLS。Docker 官方支持雙向 TLS 認證既能加密通信也能識別客戶端身份。我把生成證書的步驟完整列一遍。證書文件統(tǒng)一放在/opt/docker-certs目錄mkdir -p /opt/docker-certs cd /opt/docker-certs生成 CA 私鑰和 CA 證書openssl genrsa -aes256 -out ca-key.pem 4096 openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem \ -subj /CNDocker-CA生成服務端私鑰和證書簽名請求注意使用服務器的內網(wǎng) IP 或域名openssl genrsa -out server-key.pem 4096 openssl req -subj /CN192.0.2.10 -new -key server-key.pem -out server.csr為了讓客戶端驗證服務端地址還需要在證書里附加 Subject Alternative Name??梢詣?chuàng)建一個擴展文件server-ext.cnfbasicConstraints CA:FALSE keyUsage digitalSignature, keyEncipherment extendedKeyUsage serverAuth subjectAltName IP:192.0.2.10,DNS:docker.local用 CA 簽發(fā)服務端證書openssl x509 -req -in server.csr -CA ca.pem -CAkey ca-key.pem \ -CAcreateserial -out server-cert.pem -days 365 -extfile server-ext.cnf接著生成客戶端證書給運維人員自己用openssl genrsa -out client-key.pem 4096 openssl req -subj /CNdevops-client -new -key client-key.pem -out client.csr echo extendedKeyUsage clientAuth client-ext.cnf openssl x509 -req -in client.csr -CA ca.pem -CAkey ca-key.pem \ -CAcreateserial -out client-cert.pem -days 365 -extfile client-ext.cnf然后把服務端證書配置進daemon.json{ hosts: [unix:///var/run/docker.sock, tcp://0.0.0.0:2376], tlsverify: true, tlscacert: /opt/docker-certs/ca.pem, tlscert: /opt/docker-certs/server-cert.pem, tlskey: /opt/docker-certs/server-key.pem }重啟 Docker 后用客戶端證書驗證連接docker --tlsverify \ --tlscacert/opt/docker-certs/ca.pem \ --tlscert/opt/docker-certs/client-cert.pem \ --tlskey/opt/docker-certs/client-key.pem \ -H tcp://192.0.2.10:2376 version可以正常輸出版本信息說明 TLS 生效。平時建議把客戶端證書放到~/.docker/下并設置DOCKER_HOST和DOCKER_TLS_VERIFY環(huán)境變量日常命令就不用每次都帶一堆證書參數(shù)。有幾個經(jīng)驗供參考CA 私鑰一定要保管好建議離線保存不要放在部署目錄里隨包分發(fā)證書有效期設為 365 天并寫入日歷提醒到期前提前輪換更換證書時先更新客戶端再重啟 dockerd避免中間出現(xiàn)連接空窗4.3 網(wǎng)絡層訪問控制白名單為主即使配了 TLS也不要讓 2376 端口面向全網(wǎng)段開放。TLS 解決的是“通信加密和客戶端身份驗證”但訪問范圍應該收得越窄越好。云環(huán)境下的做法是在安全組里限制來源只允許運維跳板機 IP 訪問 2376只允許內網(wǎng)管理網(wǎng)段訪問明確拒絕公網(wǎng)來源傳統(tǒng)物理機或虛擬機環(huán)境可以用 iptables 控制iptables -A INPUT -p tcp --dport 2376 -s 10.0.0.0/8 -j ACCEPT iptables -A INPUT -p tcp --dport 2376 -j DROP注意規(guī)則順序先放行白名單再執(zhí)行丟棄。如果你在云上配置了安全組同時還在機器內部寫 iptables 規(guī)則要確認安全組優(yōu)先級和 iptables 規(guī)則不會互相沖突。之前踩過坑安全組放行了一大段網(wǎng)段機器內部 iptables 又只擋了 23752376 反而漏掉了。4.4 運行時安全基線daemon.json 與容器默認策略API 層的口子堵住了還得保證容器創(chuàng)建時的默認配置不過分寬松。daemon.json是一個集中收斂配置的地方可以預先定一批安全駝峰{ icc: false, iptables: true, userland-proxy: false, live-restore: true, log-driver: json-file, log-opts: { max-size: 50m, max-file: 3 } }各字段的作用icc: false關閉容器間默認通信業(yè)務需要互訪時單獨放行iptables: true讓 Docker 管理宿主機防火墻規(guī)則同時便于流量控制live-restore: true守護進程升級或短暫重啟時容器不退出日志限制避免容器日志寫滿磁盤容器創(chuàng)建時也應該有一套自己的約束規(guī)范。我建議至少做到進程以非 root 用戶運行鏡像里創(chuàng)建獨立用戶去掉--privileged需要設備權限時逐項添加 Capabilities文件系統(tǒng)盡量只讀必要的目錄單獨掛載默認設置--security-opt no-new-privileges不把宿主機關鍵目錄掛進容器這些限制落在代碼里才有意義。寫成文檔叫“規(guī)范”寫成 CI 檢查叫“落地”。我一般是把檢查邏輯直接放進部署流水線鏡像構建結束后自動掃描危險配置發(fā)現(xiàn)問題直接攔截發(fā)布。5. 入侵后的排查、取證與常態(tài)化監(jiān)控5.1 從 Docker 事件和容器列表還原現(xiàn)場如果機器已經(jīng)疑似被入侵第一件事不是馬上把容器全停掉而是先留證據(jù)、還原現(xiàn)場。Docker 本身自帶事件流很多信息可以從里面翻出來。查看當前容器和鏡像情況docker ps -a --no-trunc docker images docker top container_name -eo pid,cmd查看守護進程日志找容器創(chuàng)建時間、鏡像拉取記錄journalctl -u docker --since 7 days ago | grep -iE container|image|pull|create用 Docker 事件輸出一段時間內的 API 調用docker events --since 7d --until now --filter typecontainer從事件流里通常能看到某臺未知鏡像的 pull 操作、異常容器的 create 時間點。再結合宿主機登錄日志基本上能鎖定入侵開始的時間范圍。5.2 宿主層后門排查要點容器只是切入點攻擊者真正的目標是宿主機持久化。檢查范圍至少覆蓋這幾類SSH 授權文件/root/.ssh/authorized_keysCrontab/var/spool/cron/和/etc/cron.d/系統(tǒng)服務systemctl list-units --staterunning可疑進程ps -ef與/proc目錄下的異常二進制監(jiān)聽端口對比正?;€的陌生外聯(lián)遇到過一種情況攻擊者通過特權容器把宿主機根目錄掛載進去寫了條 crontab 來拉起挖礦進程。單看容器列表覺得“只是跑了個陌生容器”實際宿主機后門已經(jīng)埋好。這類問題的處理重點是清理宿主側而不是只刪容器。清理流程我建議按這個順序走先斷開受影響機器與內網(wǎng)其他機器的非必要連接備份日志和容器、進程快照在隔離網(wǎng)絡里分析確認持久化載體清理 crontab、SSH 公鑰、可疑服務重建容器或重裝系統(tǒng)修改相關賬號密碼與密鑰對具體到已經(jīng)失陷的機器最穩(wěn)妥的方案是備份數(shù)據(jù)后重裝系統(tǒng)。線上業(yè)務寧可切流量重部署也不要在不干凈的底子上繼續(xù)縫縫補補。5.3 常態(tài)化暴露面檢查與告警防住一次不等于永遠安全Docker 端口暴露問題很容易在變更后復發(fā)。我這邊已經(jīng)在用一套定時巡檢腳本每天跑一遍思路是定期執(zhí)行前面提到的自查命令把監(jiān)聽端口和 TLS 配置狀態(tài)發(fā)到統(tǒng)一告警平臺。巡檢的核心檢查點就三個2375 是否監(jiān)聽2376 是否監(jiān)聽且 TLS 是否開啟/var/run/docker.sock是否被掛載進高危容器第三點檢查稍微麻煩一些需要遍歷所有容器的 Mountsdocker ps -q | xargs -I {} docker inspect --format {{.Name}} {{range .Mounts}}{{.Source}} {{end}} {}結果里如果出現(xiàn)/var/run/docker.sock和業(yè)務容器同時出現(xiàn)就要確認是不是必要掛載。不是必要的話盡早改配置。安全本質上是持續(xù)對抗不是一次性通關。從默認關閉 2375到配置完整 TLS再到運行層最小權限、日常巡檢這套鏈路走完Docker 未授權 API 引發(fā)的逃逸風險才算真正壓到了可控范圍。尤其是中小團隊沒有專職安全人員更要靠制度化的巡檢把這類低級風險擋在門外。我把這些經(jīng)驗整理出來也是希望讀到的人能少踩幾個我踩過的坑。