CodesWhat/portwing

GitHub: CodesWhat/portwing

Portwing 是一个安全优先的远程 Docker API 代理,通过 Ed25519 每请求认证、socket 级过滤和加固容器运行时,安全地远程管控分布在多台主机上的容器。

Stars: 3 | Forks: 0

Portwing

Portwing

**安全优先的远程 Docker 代理 —— 随时随地安全地管控你的容器。** Status: Alpha

Release GHCR
Multi-arch Image size License AGPL-3.0

Stars Forks Issues Last commit Commit activity
Repo size Repo views

CI Vulnerability Scan Nightly fuzz
Go Report Card Go Reference OpenSSF Scorecard Monitored by Snyk (placeholder)


📑 目录

- [🚀 快速开始](#quick-start) - [🆕 最近更新](#recent-updates) - [✨ 功能](#features) - [🔐 身份验证](#authentication) - [🔌 连接模式](#connection-modes) - [🖥️ 独立模式](#standalone-generic-mode) - [⚙️ 配置](#configuration) - [📡 API 参考](#api-reference) - [🔑 Token 安全](#token-security) - [✅ 验证发布版本](#verify-a-release) - [🛡️ 安全](#security) - [📋 审计日志](#audit-logging) - [⭐ Star 历史](#star-history) - [🛠️ 构建工具](#built-with) - [🤝 社区与支持](#community--support)
``` flowchart LR subgraph server ["Your server"] DD["Drydock
(controller + UI)"] end subgraph hostA ["Remote host A"] direction LR LA["Portwing
(agent)"] SGA["sockguard
(socket filter)"] DA["Docker Engine"] LA -- "filtered socket" --> SGA --> DA end subgraph hostB ["Remote host B"] direction LR LB["Portwing
(agent)"] SGB["sockguard
(socket filter)"] DB["Docker Engine"] LB -- "filtered socket" --> SGB --> DB end DD -- "HTTPS + SSE · X-Dd-Agent-Secret" --> LA DD -- "HTTPS + SSE · X-Dd-Agent-Secret" --> LB ```

🚀 快速开始

### 推荐部署(已强化) 最强大的安全态势结合了三项控制措施:**sockguard**(socket 级别的请求过滤,使 Portwing 永远不会直接接触原始的 Docker socket)、**Ed25519 每请求身份验证**(签名请求、防重放保护、无需共享密钥)以及**强化容器运行时**(`read_only`、`cap_drop: ALL`、`no-new-privileges`、密钥挂载的 token)。在**严格位于 TLS 反向代理之后的标准模式**下运行 Portwing —— Drydock 控制器将向其发起入站连接。(对于没有入站端口的主机,出站 [边缘模式](#connection-modes) 可以在 Drydock 1.5 上实现端到端可用。) **第 1 步 —— 生成 token 并拉取示例:** ``` openssl rand -hex 32 > portwing_token.txt # 下载加固的 compose 文件及其 sockguard 策略 curl -fsSLO https://raw.githubusercontent.com/CodesWhat/portwing/main/examples/docker-compose.with-sockguard.yml curl -fsSLO https://raw.githubusercontent.com/CodesWhat/portwing/main/examples/sockguard.yaml ``` **第 2 步 —— 启动强化堆栈:** ``` docker compose -f docker-compose.with-sockguard.yml up -d ``` 这会将 sockguard 和 Portwing 作为共享已过滤 socket 卷的独立容器运行。两个容器都没有直接挂载原始 Docker socket;sockguard 会在 socket 级别强制执行允许的 Docker API 操作白名单。完整的 compose 文件(`examples/docker-compose.with-sockguard.yml`): ``` # Portwing + sockguard — 两层防御。 # # Sockguard 位于 Portwing 和宿主机的 Docker socket 之间,并将一个 # 过滤后的 unix socket 写入到共享的 named volume 中。Portwing 与该 # 过滤后的 socket 通信,而不是直接挂载 /var/run/docker.sock,因此即使 # agent 被完全攻陷,也会被限制在 # sockguard.yaml 中明确的 API allowlist 内。 # # 首先生成一个 token: # openssl rand -hex 32 > portwing_token.txt services: sockguard: image: ghcr.io/codeswhat/sockguard:latest restart: unless-stopped read_only: true cap_drop: - ALL security_opt: - no-new-privileges:true volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./sockguard.yaml:/etc/sockguard/sockguard.yaml:ro - sockguard-socket:/var/run/sockguard environment: - SOCKGUARD_LISTEN_SOCKET=/var/run/sockguard/sockguard.sock portwing: image: ghcr.io/codeswhat/portwing:latest restart: unless-stopped depends_on: - sockguard read_only: true cap_drop: - ALL security_opt: - no-new-privileges:true tmpfs: - /tmp ports: - "3000:3000" volumes: - sockguard-socket:/var/run/sockguard:ro - portwing-stacks:/data/stacks environment: - DOCKER_SOCKET=/var/run/sockguard/sockguard.sock - TOKEN_FILE=/run/secrets/portwing_token secrets: - portwing_token secrets: portwing_token: file: ./portwing_token.txt volumes: sockguard-socket: portwing-stacks: ``` **升级到 Ed25519 密钥身份验证(零共享密钥):** 使用 `portwing keygen` 生成密钥对,挂载 `authorized_keys` 文件,并设置 `AUTHORIZED_KEYS=/etc/portwing/authorized_keys` —— 参见 [身份验证](#authentication)。使用 `PRIVATE_KEY_FILE` 进行签名的边缘模式握手。
边缘模式变体(出站 WebSocket —— 抢先体验) 对于位于 NAT 或防火墙之后的主机,[`examples/docker-compose.edge.yml`](examples/docker-compose.edge.yml) 会让 Portwing 拨号连接到你的 Drydock 控制器的边缘端点(`DRYDOCK_URL` + `/api/portwing/ws`);远程主机上不会发布任何端口。 ``` services: portwing: image: ghcr.io/codeswhat/portwing:latest restart: unless-stopped read_only: true cap_drop: - ALL security_opt: - no-new-privileges:true tmpfs: - /tmp volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - portwing-stacks:/data/stacks environment: - DRYDOCK_URL=https://drydock.example.com - TOKEN_FILE=/run/secrets/portwing_token - AGENT_NAME=edge-host-01 # Key-based hello instead of a shared token: # portwing keygen → PRIVATE_KEY_FILE=/run/secrets/portwing_key secrets: - portwing_token secrets: portwing_token: file: ./portwing_token.txt volumes: portwing-stacks: ```
快速开始(仅供评估 —— 不可用于生产环境) ``` docker run -d \ --name portwing \ -v /var/run/docker.sock:/var/run/docker.sock \ -p 3000:3000 \ -e TOKEN=$(openssl rand -hex 24) \ ghcr.io/codeswhat/portwing:latest ``` 如果没有设置 `TOKEN`(或 `TOKEN_HASH`/`AUTHORIZED_KEYS`),API 将是**未经验证**的 —— 任何可以访问该端口的人都可以控制你的 Docker daemon。
二进制安装 (install.sh) ``` curl -fsSL https://raw.githubusercontent.com/codeswhat/portwing/main/scripts/install.sh | bash ```

🆕 最近更新

最新版本亮点 - **v0.2.0 于 2026-06-12 发布** —— Ed25519 每请求身份验证,通过 `X-Portwing-Key-ID` / `X-Portwing-Timestamp` / `X-Portwing-Nonce` / `X-Portwing-Signature` 标头进行签名请求,并根据 `authorized_keys` 文件进行验证。通过 nonce LRU 和时间戳窗口实现防重放保护,支持使用 SIGHUP 热重载密钥文件,包含 `portwing keygen` CLI 子命令,并在 401 响应中提供 `X-Portwing-Reason` 诊断标头。通过 `PRIVATE_KEY_FILE` 实现签名的边缘模式握手。 - **密钥注册** —— 可选的一次性 `ENROLLMENT_TOKEN`(`POST /api/portwing/enroll`),用于引导配置首个 Ed25519 密钥 —— 首次使用后即销毁,受速率限制,并记录在审计日志中。 - **Argon2id token 哈希** —— 使用 OWASP 推荐参数的 `TOKEN_HASH` / `TOKEN_HASH_FILE`;SHA-256 成功缓存使每次请求的开销保持平稳。 - **MCP server** —— 位于 `/_portwing/mcp` 的只读 Model Context Protocol 端点(可流式传输的 HTTP,协议版本 2025-11-25),适用于 AI 助手(Claude、Cursor、Windsurf)。工具包含:`list_containers`、`inspect_container`、`container_logs`、`host_metrics`、`container_stats`。 - **Prometheus 指标** —— `/metrics` 和 `/_portwing/metrics` 暴露 `portwing_build_info`、容器数量以及主机资源指标。 - **结构化审计日志** —— `AUDIT_LOG` 环境变量以 JSON 行格式记录身份验证事件、Compose 操作和 exec 会话。 - **通用 REST 适配器** —— 无需 Drydock 平台连接的独立模式无头 REST + SSE 管理 API(`ADAPTER=generic`)。 - **强化的 CI 与供应链** —— SHA 固定的 actions,五个 Go fuzz 目标(CI 60秒 / 夜间5分钟),针对真实 Docker daemon 的集成测试套件,每周漏洞扫描(govulncheck/grype/gosec),每月变异测试,OpenSSF Scorecard,CodeQL,以及在每次发布时进行 cosign 无密钥签名 + CycloneDX SBOM + SLSA 来源验证。 - **v0.1.0 于 2025-06-01 发布** —— 初始版本:透明的 Docker API 代理、边缘模式 WebSocket 隧道、Drydock 适配器、SSE 事件流、token 身份验证、速率限制、多架构镜像。 完整的历史记录请参见 [CHANGELOG.md](CHANGELOG.md)。

✨ 功能

| | 功能 | 描述 | |---|---|---| | 🔄 | **连接模式** | 标准模式(Drydock 控制器通过 HTTP/SSE 进行入站连接)是主要的集成方式。边缘模式(代理通过 WebSocket 拨号出站,适用于 NAT/防火墙后的主机)自 Drydock 1.5 + Portwing 0.2.2 起即可端到端使用(两者均为预发布版本)。 | | 🐳 | **透明 Docker API 代理** | 所有 Docker Engine API 路径均转发至本地 daemon —— 包括流式端点、exec 会话劫持以及长连接。 | | 🔑 | **Ed25519 每客户端身份验证** | 使用各客户端密钥进行逐请求签名,通过 nonce LRU 和时间戳窗口实现防重放保护,通过 SIGHUP 进行 `authorized_keys` 式轮换,零共享密钥。 | | 🔐 | **Argon2id Token 哈希** | 使用 OWASP 推荐的 Argon2id 参数对静态 token 进行哈希处理;`TOKEN_HASH_FILE` 支持 Docker secrets;SHA-256 成功缓存使每次请求的开销保持平稳。 | | 🤖 | **MCP Server** | AI 助手连接至 `/_portwing/mcp`(可流式传输的 HTTP,协议版本 2025-11-25)。只读工具:`list_containers`、`inspect_container`、`container_logs`、`host_metrics`、`container_stats`。环境变量的值绝不会被传输。 | | 📦 | **容器清单** | 包含 `dd.*` 标签解析和 SSE 广播的完整容器元数据,包括用于 Drydock 兼容性的 `dd:watcher-snapshot` 事件。 | | 📈 | **Prometheus 指标** | 在 `/_portwing/metrics` 以 cAdvisor 兼容格式提供主机及各容器的 CPU/内存/网络指标。零外部依赖。 | | 📋 | **审计日志** | 将每个 API 调用、身份验证事件、exec 会话和 Compose 操作结构化为 JSON。默认禁用(关闭时仅有单次 nil 检查的开销)。 | | 🖥️ | **主机指标** | 收集 CPU、内存、磁盘、网络和运行时间。 | | ⚡ | **交互式 Exec** | 通过 WebSocket 或 HTTP 劫持进行终端会话,最大支持 100 个并发会话。 | | 🗂️ | **Docker Compose** | 具备安全强化的全生命周期管理 —— 路径遍历保护、环境变量黑名单、服务名称注入预防。 | | 📡 | **SSE 兼容性** | 可直接替换现有的 Drydock 代理,包括连接时发送的 `dd:watcher-snapshot` 完整清单。 | | ✍️ | **已签名供应链** | 每次发布均提供 Cosign 无密钥签名、CycloneDX SBOM 和 SLSA 来源证明。无需管理签名密钥即可验证。 | | 🛡️ | **双层防御** | 与 [sockguard](https://github.com/codeswhat/sockguard) 配合使用,使代理永远不会直接接触原始 Docker socket。 | | 🪶 | **最小占用空间** | 静态 Go 二进制文件,约 10 MB 的 Wolfi (Chainguard) 容器镜像。禁用 CGO,已剥离,无外部运行时依赖。 | | 🔌 | **独立模式** | `ADAPTER=generic` 在 `/api/v1/*` 上提供由本地 Docker daemon 支持的纯净 REST + SSE API —— 无需 Drydock 账户。 |

🔐 身份验证

Token 身份验证(快速开始) 将 `TOKEN` 设置为随机密钥。所有请求必须通过 `Authorization: Bearer`、`X-Portwing-Token` 或 `X-Dd-Agent-Secret` 提供。 ``` TOKEN=$(openssl rand -hex 32) docker run -d --name portwing \ -v /var/run/docker.sock:/var/run/docker.sock \ -e TOKEN="$TOKEN" \ -p 3000:3000 \ ghcr.io/codeswhat/portwing:latest ```
Ed25519 每客户端密钥身份验证(推荐) Ed25519 密钥对通过逐请求签名和防重放保护提供客户端身份。无共享密钥。 **生成密钥对:** ``` # 将私钥(PEM PKCS#8)和 authorized_keys 行写入 stdout。 portwing keygen -comment "my-platform:prod" ``` **将 `authorized_keys` 行复制到代理主机:** ``` # /etc/portwing/authorized_keys (mode 0600) ed25519 AAAA... my-platform:prod ``` **启动带有 Ed25519 身份验证的代理:** ``` docker run -d --name portwing \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /etc/portwing/authorized_keys:/etc/portwing/authorized_keys:ro \ -e AUTHORIZED_KEYS=/etc/portwing/authorized_keys \ -p 3000:3000 \ ghcr.io/codeswhat/portwing:latest ``` **密钥轮换(零停机):** 1. 生成新密钥对:`portwing keygen -comment "my-platform:prod:2026-07"` 2. 将新的公钥行追加到代理主机的 authorized_keys 文件中。 3. 发送 `SIGHUP` 以重新加载:`kill -HUP $(pidof portwing)` 或 `docker kill --signal HUP portwing`。旧密钥和新密钥现在均处于活动状态。 4. 更新平台以使用新的私钥。 5. 从文件中删除旧密钥,并再次发送 `SIGHUP`。 Token 身份验证(`TOKEN`/`TOKEN_HASH`)可与 Ed25519 并行工作 —— 在迁移期间可以同时设置两者。中间件会首先检查 `X-Portwing-Signature`;如果不存在,则回退到 token 检查。

🔌 连接模式

标准模式与边缘模式 ### 标准模式 —— 已实现 Portwing 运行 HTTP(S) server;**Drydock 控制器进行入站连接**并从中拉取数据。这是目前可用的集成方式。 - 在未配置 `DRYDOCK_URL` 时设置 - Drydock 使用 `X-Dd-Agent-Secret` 共享密钥进行身份验证(可选 mTLS) - 在 `GET /api/containers` · `/api/watchers` · `/api/triggers` 上进行握手,然后在 `GET /api/events` 上建立长生命周期的 **SSE** 流 - 所有路径上的透明 Docker API 代理;代理端点位于 `/_portwing/*` 下 - 支持带有现代密码套件的可选 TLS (TLS 1.2+) 边缘模式 —— 抢先体验 Portwing 向控制器的边缘端点(`DRYDOCK_URL` + `/api/portwing/ws`)发起出站 WebSocket,适用于没有入站端口的主机。双方均已实现 —— Drydock 1.5 提供了控制器端点,Portwing 对 Ed25519 握手进行了签名 —— 因此边缘模式**可以端到端使用**。Drydock 1.5 和 Portwing 0.2.2 为预发布版本;在 Portwing 0.2.2 中实现了完全的负载下的 exec 稳健性。该端点为**仅限 Ed25519**:设置 `PRIVATE_KEY_FILE` 并在 Drydock 注册公钥。 - 在配置了 `DRYDOCK_URL` 以及 `TOKEN`、`AUTHORIZED_KEYS` 或 `PRIVATE_KEY_FILE` 时设置 - 面向位于 NAT、防火墙和动态 IP 后的主机 - 带有指数退避 + 抖动的自动重连;通过 `PRIVATE_KEY_FILE` 进行签名握手 ``` DRYDOCK_URL set + (TOKEN or AUTHORIZED_KEYS or PRIVATE_KEY_FILE) set → Edge Mode (outbound WebSocket) Otherwise → Standard Mode (inbound HTTP server) ```

🖥️ 独立(通用)模式

无需 Drydock 平台连接运行 通过设置 `ADAPTER=generic` 在没有任何外部控制器的情况下运行 Portwing。 你将获得直接由本地 Docker daemon 支持的在 `/api/v1/*` 上的纯净 REST + SSE API —— 无需 Drydock 账户。 ``` docker run -d \ --name portwing \ -v /var/run/docker.sock:/var/run/docker.sock \ -e ADAPTER=generic \ -e TOKEN=my-secret \ -p 3000:3000 \ ghcr.io/codeswhat/portwing:latest ``` ### 端点 | 端点 | 描述 | |----------|-------------| | `GET /api/v1/version` | 代理版本,协议信息 | | `GET /api/v1/containers` | 缓存的容器清单 | | `GET /api/v1/containers/{id}/logs` | 容器日志(`tail`、`since`、`until`、`follow`) | | `GET /api/v1/events` | Docker 生命周期事件的 SSE 流 | ### curl 示例 ``` TOKEN=my-secret # Agent 版本 curl -s -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/v1/version | jq . # Container 清单 curl -s -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/v1/containers | jq . # 来自某个 container 的最后 50 行日志 curl -s -H "Authorization: Bearer $TOKEN" \ "http://localhost:3000/api/v1/containers/my-container/logs?tail=50" # 实时流式传输 container 日志 curl -sN -H "Authorization: Bearer $TOKEN" \ "http://localhost:3000/api/v1/containers/my-container/logs?follow=1" # 流式传输 Docker 生命周期事件 (SSE) curl -sN -H "Authorization: Bearer $TOKEN" \ http://localhost:3000/api/v1/events ``` 每个 SSE 事件都是一个 JSON 对象: ``` { "ts": "2026-06-11T10:00:00Z", "type": "container", "action": "start", "containerId": "abc123def456", "name": "my-container", "image": "nginx:latest", "labels": { "app": "web" } } ``` 每 30 秒会写入一条注释心跳行(`: heartbeat`),以保持连接穿过代理存活。

⚙️ 配置

环境变量参考 ### 连接 | 变量 | 默认值 | 描述 | |----------|---------|-------------| | `DRYDOCK_URL` | -- | 边缘模式的 WebSocket URL (`wss://...`) | | `TOKEN` | -- | 身份验证 token(明文) | | `TOKEN_FILE` | -- | 包含 token 的文件路径 | | `TOKEN_HASH` | -- | token 的 Argon2id 哈希(使用 `portwing hash-token` 生成) | | `TOKEN_HASH_FILE` | -- | 包含 Argon2id 哈希的文件路径 | | `AUTHORIZED_KEYS` | -- | Ed25519 authorized_keys 文件路径(每客户端非对称身份验证) | | `AUTHORIZED_KEYS_FILE` | -- | `AUTHORIZED_KEYS` 的别名 | | `MAX_CLOCK_SKEW_SECONDS` | `60` | Ed25519 请求时间戳允许的最大时钟偏差 | | `NONCE_LRU_SIZE` | `10000` | 用于防重放保护的内存 nonce 缓存容量 | | `ENROLLMENT_TOKEN` | -- | 用于 Model C 密钥注册的一次性引导 token | | `ENROLLMENT_TOKEN_FILE` | -- | 包含注册 token 的文件 | | `PRIVATE_KEY_FILE` | -- | 用于签名边缘模式握手的 Ed25519 私钥 (PEM PKCS#8) | | `CA_CERT` | -- | 边缘模式的自定义 CA 证书 | | `TLS_SKIP_VERIFY` | `false` | 跳过 TLS 验证(仅供测试) | | `PORT` | `3000` | HTTP server 端口 | | `BIND_ADDRESS` | `0.0.0.0` | 绑定地址 | | `TLS_CERT` | -- | Server TLS 证书(标准模式) | | `TLS_KEY` | -- | Server TLS 密钥(标准模式) | | `TRUSTED_PROXIES` | -- | 以逗号分隔的反向代理 CIDR 列表,其 `X-Forwarded-For` 是受信任的;未设置表示忽略转发标头 | ### Docker | 变量 | 默认值 | 描述 | |----------|---------|-------------| | `DOCKER_SOCKET` | 自动检测 | Docker socket 路径 | | `DOCKER_HOST` | -- | Docker TCP host(替代方案) | | `STACKS_DIR` | `/data/stacks` | Compose 堆栈文件目录 | ### 代理身份 | 变量 | 默认值 | 描述 | |----------|---------|-------------| | `AGENT_ID` | UUID v4 | 唯一代理标识符 | | `AGENT_NAME` | 主机名 | 可读名称 | ### 运营 | 变量 | 默认值 | 描述 | |----------|---------|-------------| | `HEARTBEAT_INTERVAL` | `30` | Ping 间隔(秒) | | `WELCOME_TIMEOUT` | `30` | 在边缘模式下等待 Drydock 欢迎消息的秒数 | | `REQUEST_TIMEOUT` | `30` | Docker API 请求超时(秒) | | `RECONNECT_DELAY` | `1` | 初始重连延迟(秒) | | `MAX_RECONNECT_DELAY` | `60` | 最大重连延迟(秒) | | `LOG_LEVEL` | `info` | `debug`、`info`、`warn`、`error` | | `SKIP_DF_COLLECTION` | -- | 禁用磁盘指标 | | `AUDIT_LOG` | -- | 审计日志输出:`stdout`、`stderr` 或文件路径;未设置则禁用审计 | ### 适配器 | 变量 | 默认值 | 描述 | |----------|---------|-------------| | `ADAPTER` | `drydock` | 要使用的适配器:`drydock`(兼容 Drydock)或 `generic`(独立 REST/SSE) | ### Drydock 兼容性 | 变量 | 默认值 | 描述 | |----------|---------|-------------| | `DD_AGENT_SECRET` | -- | Drydock 代理密钥 token | | `DD_AGENT_SECRET_FILE` | -- | Drydock 代理密钥 token 文件 | | `DD_POLL_INTERVAL` | `300` | 容器清单刷新(秒) |

📡 API 参考

健康、代理、MCP、兼容 Drydock 及代理端点 ### 健康端点 | 端点 | 方法 | 身份验证 | 描述 | |----------|--------|------|-------------| | `/health` | GET | 否 | 简单健康检查 —— `{"status":"ok"}` | | `/_portwing/health` | GET | 否 | 健康检查 + Docker 连通性 | 当无法连接 Docker daemon 时,`/_portwing/health` 将返回 HTTP 503。 两个端点均无需身份验证,可安全用于负载均衡器探针和 Docker HEALTHCHECK 指令。 ### 代理端点 | 端点 | 方法 | 身份验证 | 描述 | |----------|--------|------|-------------| | `/_portwing/info` | GET | 是 | 代理版本、模式、功能 | | `/_portwing/compose` | POST | 是 | Docker Compose 操作 | | `/_portwing/metrics` | GET | 是 | Prometheus 指标(代理范围) | | `/metrics` | GET | 是 | Prometheus 指标(Drydock 代理密钥) | | `/_portwing/mcp` | POST | 是 | MCP server(JSON-RPC 2.0,协议版本 2025-11-25) | ### MCP —— AI 助手集成 Portwing 在 `POST /_portwing/mcp` 暴露了一个只读的 [Model Context Protocol](https://modelcontextprotocol.io/) 端点。 AI 助手(Claude、Cursor、Windsurf 或任何 MCP 客户端)可以通过此端点使用其标准的工具调用流程查询实时的容器状态。 **协议:** MCP 2025-11-25 —— 可流式传输的 HTTP,无状态单请求模式,`Content-Type: application/json`。 **可用工具:** | 工具 | 描述 | |------|-------------| | `list_containers` | 所有容器 —— id、名称、镜像、状态、状况、标签 | | `inspect_container(id)` | 状态、镜像、环境变量数量(绝不暴露值)、挂载、网络、重启策略 | | `container_logs(id, tail)` | stdout/stderr 的最后 N 行(最多 500 行) | | `host_metrics` | CPU、内存、磁盘、网络、运行时间快照 | | `container_stats(id)` | 针对容器的一次性 CPU/内存/网络统计信息 | **凭证清理:** `inspect_container` 仅返回环境变量的*数量* —— 绝不会传输值,防止意外泄露密钥。 #### 添加至 Claude Desktop (claude_desktop_config.json) ``` { "mcpServers": { "portwing": { "command": "curl", "args": ["-s", "-X", "POST", "-H", "Content-Type: application/json", "-H", "Authorization: Bearer YOUR_PORTWING_TOKEN", "http://your-host:3000/_portwing/mcp"], "type": "http", "url": "http://your-host:3000/_portwing/mcp", "headers": { "Authorization": "Bearer YOUR_PORTWING_TOKEN" } } } } ``` #### 通过 claude mcp add 添加 (CLI) ``` claude mcp add --transport http \ --header "Authorization: Bearer YOUR_PORTWING_TOKEN" \ portwing http://your-host:3000/_portwing/mcp ``` #### .mcp.json(项目级,Cursor / Windsurf / 任何客户端) ``` { "mcpServers": { "portwing": { "type": "http", "url": "http://your-host:3000/_portwing/mcp", "headers": { "Authorization": "Bearer YOUR_PORTWING_TOKEN" } } } } ``` 将 `YOUR_PORTWING_TOKEN` 替换为你在 `TOKEN` / `TOKEN_FILE` / `TOKEN_HASH` 中设置的值。 ### 兼容 Drydock 的端点 | 端点 | 方法 | 描述 | |----------|--------|-------------| | `/api/events` | GET | SSE 事件流(`dd:ack`、容器事件) | | `/api/containers` | GET | 容器清单 | | `/api/containers/:id/logs` | GET | 容器日志 | | `/api/containers/:id` | DELETE | 移除容器 | | `/api/watchers` | GET | Watcher 组件 | | `/api/triggers` | GET | Trigger 组件 | ### Docker API 代理 所有其他路径(`/*`)均透明代理至 Docker Engine API,包括流式端点和 exec 会话劫持。 ### 指标 Portwing 在 `/_portwing/metrics`(以及别名 `/metrics`)暴露 Prometheus 指标。两者均需要 bearer 身份验证。 Prometheus 抓取配置: ``` scrape_configs: - job_name: portwing scheme: https # or http if TLS not configured static_configs: - targets: ["your-host:3000"] authorization: type: Bearer credentials: YOUR_PORTWING_TOKEN tls_config: # ca_file: /etc/prometheus/portwing-ca.crt # if using custom CA insecure_skip_verify: false ```

🔑 Token 安全

明文、基于文件和静态哈希的 token 选项 ### 明文 token(仅供测试) ``` # 生成一个强 token TOKEN=$(openssl rand -hex 32) docker run -e TOKEN="$TOKEN" ... ghcr.io/codeswhat/portwing:latest ``` ### 基于文件的 token(生产环境) ``` TOKEN=$(openssl rand -hex 32) printf '%s' "$TOKEN" > /run/secrets/portwing-token chmod 600 /run/secrets/portwing-token docker run -e TOKEN_FILE=/run/secrets/portwing-token \ -v /run/secrets/portwing-token:/run/secrets/portwing-token:ro \ ... ghcr.io/codeswhat/portwing:latest ``` ### 使用 TOKEN_HASH 进行静态哈希 仅存储 Argon2id 哈希,确保明文 token 永远不会出现在环境转储或配置文件中: ``` # 生成 hash(token 从 stdin 读取,绝不经过 argv) HASH=$(printf '%s' "$TOKEN" | portwing hash-token) # $argon2id$v=19$m=19456,t=2,p=1$$ # 使用 hash 替代明文 docker run -e TOKEN_HASH="$HASH" ... ghcr.io/codeswhat/portwing:latest ``` 或者将哈希写入文件并使用 `TOKEN_HASH_FILE`: ``` printf '%s' "$TOKEN" | portwing hash-token > /run/secrets/portwing-token-hash docker run -e TOKEN_HASH_FILE=/run/secrets/portwing-token-hash ... ```

✅ 验证发布版本

校验和与容器镜像的 Cosign 验证 Portwing 发布版本通过 GitHub Actions 无密钥签名使用 [Sigstore cosign](https://github.com/sigstore/cosign) 进行签名。无需管理签名密钥即可验证校验和与容器镜像。 ### 验证校验和文件 ``` TAG=v0.1.0 cosign verify-blob \ --certificate-identity-regexp "https://github.com/CodesWhat/portwing/.github/workflows/.*" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ --bundle "checksums.txt.bundle" \ "checksums.txt" ``` ### 验证容器镜像 ``` TAG=v0.1.0 cosign verify \ --certificate-identity-regexp "https://github.com/CodesWhat/portwing/.github/workflows/.*" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ "ghcr.io/codeswhat/portwing:${TAG}" ``` ### SBOM 每次发布都包含一个作为发布资产附加的 CycloneDX SBOM (`portwing-${TAG}-sbom.cdx.json`)。使用任何兼容 CycloneDX 的工具下载并检查它,或者像校验和文件一样使用 cosign 对其进行验证。

🛡️ 安全

安全模型摘要 - **身份验证**:基于 token,采用时序安全比较(`crypto/subtle`);通过 `TOKEN_HASH` 实现静态哈希;Ed25519 各客户端密钥对,带有逐请求签名和防重放保护 - **速率限制**:每个 IP 每分钟允许 10 次失败的身份验证尝试 - **TLS**:带有现代 AEAD 密码套件的 TLS 1.2+ - **Compose 安全**:路径遍历保护、环境变量黑名单、防止服务名称注入 - **资源限制**:WebSocket (16 MB)、响应主体 (100 MB)、exec 会话(100 个并发) 完整的可引用规范和 CVE 映射请参见 [docs/security-model.md](docs/security-model.md)。

📋 审计日志

针对每个安全相关操作的结构化 JSON 审计跟踪 Portwing每个安全相关操作提供了结构化的 JSON 审计日志 —— 这是商业容器管理平台通常锁定在付费层级之后的功能。 ### 启用 ``` # 写入文件(以 append-only 方式打开,mode 0600) docker run -e AUDIT_LOG=/var/log/portwing-audit.log ... # 或者输出到 stdout/stderr(与 log aggregators 配合使用时很有用) docker run -e AUDIT_LOG=stdout ... ``` 默认情况下禁用审计(未设置 `AUDIT_LOG`)。禁用时,开销仅为每个请求进行一次 nil 指针检查。 ### 事件 | `event` | 触发条件 | |---------|---------------| | `api_request` | 任何经过身份验证的 API 调用完成时 | | `auth_failure` | 出现无效 token 时 | | `rate_limited` | IP 被速率限制器拦截时 | | `compose_op` | 运行 Docker Compose 操作时 | | `exec_start` | 打开交互式 exec 隧道时 | ### 示例 JSON 行 ``` {"time":"2026-01-15T10:23:45.123456789Z","level":"INFO","msg":"","event":"api_request","actor":"203.0.113.42","method":"POST","path":"/_portwing/compose","outcome":"allowed","status":200,"duration_ms":3.14} ``` Compose 操作包含附加字段: ``` {"time":"2026-01-15T10:23:45.200Z","level":"INFO","msg":"","event":"compose_op","actor":"203.0.113.42","operation":"up","stack":"nginx-stack","outcome":"allowed"} ``` Exec 隧道事件: ``` {"time":"2026-01-15T10:24:01.500Z","level":"INFO","msg":"","event":"exec_start","actor":"203.0.113.42","container":"abc123def456","exec_id":"e7f8a9b1","outcome":"allowed"} ```

📖 文档

| 资源 | 链接 | | --- | --- | | 安全模型 | [`docs/security-model.md`](docs/security-model.md) | | Ed25519 身份验证设计 | [`docs/design/ed25519-auth.md`](docs/design/ed25519-auth.md) | | Watchtower 迁移 | [`docs/migrating-from-watchtower.md`](docs/migrating-from-watchtower.md) | | Drydock 集成 | [`docs/drydock-integration.md`](docs/drydock-integration.md) | | OpenAPI 规范 | [`api/openapi.yaml`](api/openapi.yaml) | | 更新日志 | [`CHANGELOG.md`](CHANGELOG.md) | | 贡献指南 | [`CONTRIBUTING.md`](CONTRIBUTING.md) | | 行为准则 | [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) | | 安全策略 | [`SECURITY.md`](SECURITY.md) | | 发布流程 | [`RELEASING.md`](RELEASING.md) | | 示例 | [`examples/`](examples/) | | 议题 | [GitHub Issues](https://github.com/CodesWhat/portwing/issues) | | 讨论 | [GitHub Discussions](https://github.com/CodesWhat/portwing/discussions) |
Star History Chart
[![SemVer](https://img.shields.io/badge/semver-2.0.0-blue)](https://semver.org/) [![Conventional Commits](https://img.shields.io/badge/commits-conventional-fe5196?logo=conventionalcommits&logoColor=fff)](https://www.conventionalcommits.org/) [![Keep a Changelog](https://img.shields.io/badge/changelog-Keep%20a%20Changelog-E05735)](https://keepachangelog.com/) ### 构建工具 [![Go 1.26](https://img.shields.io/badge/Go_1.26-00ADD8?logo=go&logoColor=fff)](https://go.dev/) [![gorilla/websocket](https://img.shields.io/badge/gorilla%2Fwebsocket-00ADD8?logo=go&logoColor=fff)](https://github.com/gorilla/websocket) [![google/uuid](https://img.shields.io/badge/google%2Fuuid-00ADD8?logo=go&logoColor=fff)](https://github.com/google/uuid) [![golang.org/x/crypto](https://img.shields.io/badge/x%2Fcrypto-00ADD8?logo=go&logoColor=fff)](https://pkg.go.dev/golang.org/x/crypto) [![Sigstore](https://img.shields.io/badge/Sigstore-FFC107?logo=sigstore&logoColor=000)](https://www.sigstore.dev/) [![Wolfi](https://img.shields.io/badge/Wolfi-4A4A55?logo=chainguard&logoColor=fff)](https://edu.chainguard.dev/open-source/wolfi/overview/) [![Docker](https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=fff)](https://www.docker.com/) [![GoReleaser](https://img.shields.io/badge/GoReleaser-00ADD8?logo=go&logoColor=fff)](https://goreleaser.com/) ### 社区与支持 欢迎提交议题、想法和 pull request。请从 [CONTRIBUTING.md](CONTRIBUTING.md) 开始,使用 [SECURITY.md](SECURITY.md) 进行私下漏洞披露,并使用 [GitHub Discussions](https://github.com/CodesWhat/portwing/discussions) 讨论设计问题。 每个发布的镜像都通过 GitHub Actions OIDC 进行了 cosign 签名。在生产环境中运行 Portwing 镜像之前,请使用上方 [验证发布版本](#verify-a-release) 部分中的规范调用进行验证。 **[AGPL-3.0 许可证](LICENSE)** 由 CodesWhat 构建 [![Ko-fi](https://img.shields.io/badge/Ko--fi-Support-ff5e5b?logo=kofi&logoColor=white)](https://ko-fi.com/codeswhat) [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?logo=buymeacoffee&logoColor=black)](https://buymeacoffee.com/codeswhat) [![Sponsor](https://img.shields.io/badge/Sponsor-ea4aaa?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/CodesWhat) 返回顶部
标签:API代理, Docker, EVTX分析, Web截图, 安全防御评估, 容器安全, 日志审计, 自定义请求头, 请求拦截, 远程管理, 零信任网络