防禦節點(主機 B)上除了 WAF,還裝了幾個別人寫的系統。「搭配元件篇」這一系列每篇只談一個,不深入它的設定細節,只回答三件事:它是什麼、它在主機 A 與主機 B 之間佔哪一段、少了它整體會缺什麼。本篇是 Portainer,也是本系列最後一篇。主機與位址沿用防禦節點那一套:主機 A(192.168.0.111)是 BeakPlatform,主機 B(192.168.0.112)是防禦節點。
先講結論。 Portainer 是 Docker 的網頁管理介面:主機 B 上的 12 個容器,在它裡面可以看狀態、看紀錄、重啟、進到容器裡下指令。它和防禦這件事完全無關,不在任何一條資料流上,主機 A 不知道它存在;它只是替「不想打 docker 指令的人」準備的畫面。它拿到的權限卻是整台主機裡最大的:它直接掛著 Docker 的控制通道,能登入 Portainer 的人等於主機 B 的 root。
另外有一段過程值得先知道:2026 年 10 月 9 日之前的安裝包內建 2.21.4,連不上 Ubuntu 24.04 目前的 Docker,登入後看得到容器的數量,點進去就是「環境無法連線」,而且出廠沒登記任何環境,要自己開一次精靈頁。現在的安裝包已換成 2.45.2,本機的 Docker 也由安裝腳本登記好。原因、證據與換版對已裝過的人的影響在第四節。
Portainer 是 Portainer.io 公司維護的容器管理介面,分社群版(Community Edition,授權 zlib)與商業版。本安裝包用的是社群版,容器映像 portainer/portainer-ce:2.45.2(2026 年 10 月發布的長期支援版),官方未修改的版本;2026 年 10 月 9 日之前的安裝包是 2.21.4,差別見第四節。它自己也是一個容器,透過 Docker 的 Unix socket(/var/run/docker.sock)跟主機上的 Docker 引擎講話,所以能看到、操作同一台主機上的所有容器,包括它自己。
它在本安裝包裡的樣子:
| 項目 | 內容 |
|---|---|
| 是否安裝 | 預設會裝。安裝時加 --no-ui 就不裝,Grafana 與 EveBox 也一起不裝 |
| 網址 | https://192.168.0.112:9443/,HTTPS,憑證是它自己簽的,瀏覽器會出現安全警告,選「繼續前往」 |
| 誰能連 | 由主機 B 的防火牆限制來源:安裝時 --admin-ips 列出的位址與主機 A。其他來源連線逾時 |
| 帳號 | admin 一個。密碼在安裝時隨機產生,印在安裝結尾,也存在主機 B 的 .env(PORTAINER_ADMIN_PASSWORD) |
| 它管什麼 | 本機的 Docker,安裝腳本在它起來之後用它的 API 登記成一個名為 local 的「環境」。Portainer 出廠時什麼都不管,要開過精靈頁才會登記,安裝腳本替讀者做掉了這一步。見第三節 |
| 它自己的資料 | 帳號、設定、環境清單,存在它自己的一個小資料庫裡,放在名為 portainer-data 的資料卷。實測 256 KB |
| 資源 | 實測一台節點閒置時約佔 20 MB 記憶體;映像檔 201 MB(舊版 2.21.4 是 398 MB) |
密碼為什麼要在安裝時就設好,compose 檔裡有一段註解說明:Portainer 啟動後 5 分鐘內沒有人建立管理員,它會把自己鎖起來,而本節點安裝加下載映像檔不只 5 分鐘。所以安裝腳本先產生密碼、寫進一個檔案,Portainer 第一次啟動時讀這個檔案建帳號。這個檔案只在資料卷是空的那一次有效;之後改 .env 裡的密碼不會生效,要在畫面上改。

(圖01:Portainer 的位置。黑色實線是操作的方向,紅色點線是它掛著的 Docker 控制通道(這條線的意義見第五節),灰色實線是它自己的資料,灰色虛線是它往外的連線。)
圖上有三件事要看。
docker compose 指令,不經過它。在 --admin-ips 列出的電腦上開 https://192.168.0.112:9443/,瀏覽器會警告憑證不受信任,選「進階」再「繼續前往」。帳號 admin,密碼取自主機 B:
# 主機 B
sudo grep '^PORTAINER_ADMIN_PASSWORD=' /opt/integrated-waf/.env
登入後第一頁是 Home,有一張名為 local 的環境卡片。Portainer 把每一個它管的 Docker 叫做一個環境(environment),出廠的環境清單其實是空的,連它自己所在的這台 Docker 也不在裡面,首頁只會寫 No environment available for management,要開過一次精靈頁(#!/wizard)它才會登記。安裝腳本在 Portainer 起來之後用它的 API 替讀者做掉了這一步,只在清單是空的時候做,之後存在它的資料庫裡,重啟不用再做。
卡片上的內容是從 Docker 引擎拿回來的摘要:
local Up Standalone 29.1.3 unix:///var/run/docker.sock
12 of 12 containers running 1 NODES 4 CORES 6.2 GB MEMORY
12 個容器就是本安裝包的全部:WAF 兩個(主站與歡迎頁)、Suricata、CrowdSec、Vector、ClickHouse、od-bridge、cloudflared、歡迎頁的靜態伺服器,加上 Grafana、EveBox、Portainer 自己。點進去是 Dashboard:1 個 stack(compose 檔定義的那一組,名字是 secstack)、12 個容器、14 個映像、21 個資料卷、4 個網路;左側是 Containers、Images、Networks、Volumes 等項目。Containers 頁一列一個容器,有狀態、映像、建立時間、IP 與埠,每一列可以看 Logs、看 Stats、開 Console 進到容器裡。這幾頁的內容就是 docker compose ps、docker compose logs、docker compose exec 這三個指令的畫面版。本系列其他篇裡每一個「主機 B」開頭的指令,都可以在這裡用滑鼠做到;反過來說,沒有 Portainer,那些指令也都還在。
先講舊版。2026 年 10 月 9 日之前的安裝包內建 2.21.4,自己開精靈頁登記好 local 之後,點它的卡片,畫面回到 Home,右上角跳出:
Failed loading environment
The environment named local is unreachable.
卡片上的狀態也從 Up 變成 Down。但 Docker 明明在跑,它剛剛還拿到了容器數量。按 F12 看網路請求,點進環境時它發了這一個:
GET /api/endpoints/2/docker/v1.41/_ping → 400
{"message":"client version 1.41 is too old. Minimum supported API version is 1.44,
please upgrade your client to a newer version"}
Portainer 的前端用 Docker API 的 1.41 版跟引擎打招呼。Ubuntu 24.04 目前套件庫裡的 Docker 是 29.1.3,這個版本把最低支援的 API 版本提高到 1.44,1.41 被拒絕。一被拒絕,Portainer 就把這個環境標成 Down,之後所有的操作頁面都進不去。摘要還拿得到,是因為摘要走的是另一條沒有指定版本的路。
所以這不是防火牆、不是 socket 權限、不是密碼,而是當時安裝包內建的 Portainer 版本太舊、讀者的 Ubuntu 又夠新。兩年前這個版本發布時 Docker 的最低 API 版本還是 1.24,當時不會有這個問題;之後 Ubuntu 24.04 的 docker.io 套件升到了 29,問題就出現了。確認你的主機 B 是不是這種組合:
# 主機 B
docker version --format '{{.Server.Version}} api={{.Server.APIVersion}} min={{.Server.MinAPIVersion}}'
29.1.3 api=1.52 min=1.44
min 大於 1.41 就是這個情況。解法是換新版的 Portainer,安裝包從 2026 年 10 月 9 日起改用社群版 2.45.2(本文寫作時的最新長期支援版)。新裝的讀者直接拿到新版,環境登記後狀態一直是 Up,容器清單正常列出,第三節看到的就是它。已經裝過、用 install.sh --update 升上來的讀者,有三件事會發生:
backups/portainer.db.bak,再一路從 2.21.4 升到 2.45.2,紀錄裡是一連串 migrating data to 2.22.0 … 2.45.0,最後一行 db migrated to 2.45.2。帳號與密碼不變,用原本的密碼登入即可(紀錄裡 instance already has an administrator user defined)。local;舊版時自己開精靈頁登記過的,安裝腳本不動它。舊版留下的那個環境在新版會自己變回 Up,不必刪掉重建。EnableTelemetry),安裝包把它關著;2.45 的設定頁與 API 都沒有這一項了,往外連的只剩查新版、抓公告、載範本三種。Portainer 掛著 /var/run/docker.sock。這條線的意義要講清楚,因為它決定了這個容器該放在誰的手上。
Docker 引擎以 root 執行,透過 socket 收到的指令都以 root 的身分做。能對 socket 說話的人,可以要求引擎建立一個新容器、把主機的 / 掛進去、以 root 跑一個 shell,接著就能讀寫主機上的任何檔案,包括 .env 裡所有的密碼、Suricata 的規則、nftables 的設定,以及其他容器的資料卷。這不是 Portainer 的漏洞,而是 Docker 的設計:socket 的存取權本來就等於主機的 root,主機上的 docker 群組就是為此而存在的。Portainer 只是把這條線接到了一個網頁登入畫面後面。
所以評估它的時候要用的尺是這把:
| 項目 | 說明 |
|---|---|
| 誰能登入 Portainer | 就是誰能當主機 B 的 root。出廠只有 admin 一個帳號、一組密碼,在 .env 裡是明文 |
| 誰能連 9443 埠 | --admin-ips 名單。名單上任何一台電腦被拿下,拿下的人只差一組密碼就到 root |
| 密碼在哪些地方 | 安裝結尾的畫面、.env、它自己的資料庫(雜湊過)。安裝時印出來的那一份,不要留在可以被翻到的地方 |
| 連線是否加密 | 是,9443 是 HTTPS。但憑證自簽,瀏覽器每次都要按「繼續前往」,久了會養成對憑證警告視而不見的習慣,這在其他網站上是危險的 |
| 登入的有效期 | 8 小時。重啟 Portainer 容器會讓所有已登入的瀏覽器回到登入頁,實測如此 |
| 它自己的資料庫 | 沒有加密。啟動紀錄裡寫 encryption key file not present、proceeding without encryption key。但能讀到這個資料卷的人本來就已經是 root 了,這一項不改變什麼 |
Grafana 那一篇說「能登入 Grafana 的人拿到的是 ClickHouse 的管理權」,Portainer 的版本是「能登入 Portainer 的人拿到的是整台主機」。三個管理介面裡它最需要被關掉:不打算用圖形介面管容器的節點,安裝時加 --no-ui,三個一起不裝;只想留 Grafana 的,裝完後把 Portainer 這一個容器停掉。
| 限制 | 說明 |
|---|---|
| 與防禦無關 | 不看事件、不擋攻擊、不參與建案與處置。主機 B 上沒有任何元件呼叫它 |
| 舊版(2.21.4)進不了環境 | 第四節。安裝包已換 2.45.2;用 --update 升上來的資料庫會自動升級 |
| Portainer 出廠沒登記環境 | 安裝腳本用它的 API 登記本機 Docker 為 local。這個動作只做一次,存在資料卷裡 |
| 看不到主機 A | 主機 A 不是容器,Portainer 管不到;主機 A 也不會連它 |
| 不管主機本身 | nftables、cron 的輪替排程、網卡卸載的設定都在容器外,它看不到 |
| 一個帳號 | 要分人,得自己在 Users 裡建。社群版沒有「只能看不能動」這種細緻的角色,每個使用者對它管的環境不是全有就是全無 |
| 會自己往外連 | 查新版、抓公告、載範本,三種都沒有關。舊版還有一項使用統計回報(EnableTelemetry),安裝包關著;2.45 已沒有這個選項 |
| 不會自己更新 | 版本跟著安裝包走。有新版時首頁那行「New version available」只是提醒,按了也不會更新 |
把它停掉會怎樣,實測過一次:停止 Portainer 容器 22 秒。這段期間 9443 埠連不上;對 WAF 送的攻擊請求照常回 403,EveBox 照常回應,其他 11 個容器沒有任何變化。重新啟動後 2 秒就能登入,環境清單與帳號都在。它是整個安裝包裡停掉之後影響最小的一個。
先看它活著沒有。這個端點不必登入,憑證自簽所以要加 -k:
# 主機 B
curl -sk https://127.0.0.1:9443/api/system/status
{"Version":"2.45.2","InstanceID":"9c69ca7c-0d33-4ab3-99c5-755aa6c84762"}
再看環境登記好了沒、連不連得上。這一個要登入,拿 .env 裡的密碼換一個 token,再用 token 問:
# 主機 B
PW=$(sudo grep '^PORTAINER_ADMIN_PASSWORD=' /opt/integrated-waf/.env | cut -d= -f2-)
TOKEN=$(curl -sk -X POST https://127.0.0.1:9443/api/auth -H 'Content-Type: application/json' \
-d "{\"username\":\"admin\",\"password\":\"$PW\"}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["jwt"])')
curl -sk https://127.0.0.1:9443/api/endpoints -H "Authorization: Bearer $TOKEN" | python3 -c '
import sys,json
for e in json.load(sys.stdin): print(e["Id"], e["Name"], e["URL"], "Status", e["Status"])'
3 local unix:///var/run/docker.sock Status 1
沒有任何一列,就是安裝腳本登記失敗(安裝結尾會有一行警告),開一次精靈頁即可。Status 1 是 Up,2 是 Down;舊版配 Docker 29 的話,只要在畫面上點過一次環境,這裡就會變成 2,新版一直是 1。
最後,它管得到的東西,不用它也看得到。本系列其他篇用的都是這三個指令,它們不依賴 Portainer:
# 主機 B
cd /opt/integrated-waf
sudo docker compose ps --format 'table {{.Name}}\t{{.Status}}'
NAME STATUS
secstack-clickhouse-1 Up 35 hours (healthy)
secstack-cloudflared-1 Up 35 hours
secstack-crowdsec-1 Up 35 hours
secstack-evebox-1 Up 35 hours
secstack-grafana-1 Up 35 hours
secstack-od-bridge-1 Up 35 hours
secstack-portainer-1 Up 35 hours
secstack-suricata-1 Up 35 hours
secstack-vector-1 Up 35 hours
secstack-waf-nginx-1 Up 35 hours (healthy)
secstack-waf-welcome-1 Up 35 hours (healthy)
secstack-welcome-1 Up 35 hours
sudo docker compose logs --tail 20 od-bridge # 看某個容器的紀錄
sudo docker compose exec -T clickhouse clickhouse-client --query 'SELECT 1' # 進某個容器下指令
Portainer 能做的就是這三件事的畫面版。它在整體裡的位置到此說完:不在任何一條線上,誰都不需要它,但它握著整台主機的鑰匙。要不要留它,看的不是功能,是誰會拿到那把鑰匙。