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