网络安全

Activepieces 如何隔离出站流量以保护你的内部网络

概述

Activepieces 在两个层面发起出站 HTTP 请求,每个层面都分别进行了加固:

  1. 工作流中的用户代码 — 代码步骤、Piece 动作、工作流作者可编写的任何内容。通过 AP_NETWORK_MODE 加固;请参阅用户代码出口
  2. API 的服务端 HTTP — OAuth 令牌获取/刷新、Vault、Conjur、事件目标 Webhook、On-Call 通知器、MCP 工具验证。始终经过过滤,通过 AP_SSRF_ALLOW_LIST 调优;请参阅服务端出口

两个层面都会阻止相同的 IP 集合(RFC1918 私有地址、回环地址、链路本地/云元数据地址、非单播地址),并共享 AP_SSRF_ALLOW_LIST 白名单。

用户代码出口

每个工作流最终都会运行用户提供的代码——代码步骤、Piece 动作、HTTP 请求。如果没有明确边界,这些代码可以访问宿主能够访问的任何内容:127.0.0.1、Redis、Postgres、Kubernetes API、云元数据端点(169.254.169.254)、VPC。AP_NETWORK_MODE 是控制此边界的开关。

`AP_NETWORK_MODE` 默认为 `UNRESTRICTED`。在生产环境中设置为 `STRICT` 以启用下文所述的完整纵深防御体系。
效果
UNRESTRICTED无出站防护。用户代码可访问 Worker 能访问的任何主机。在加固栈验证完成前的默认值。
STRICT启用下文中所有三层保护。用户代码到私有、回环、链路本地和云元数据 IP 的出站连接被阻止。

相关环境变量:

  • AP_SSRF_ALLOW_LIST — 逗号分隔的 IP 列表,用于绕过阻止(例如内部数据库或 Sidecar)。
  • HTTP_PROXY / HTTPS_PROXY — 如果设置,引擎将通过代理路由所有出站 HTTP(S) 流量。回环代理端口会自动豁免。

隔离工作原理

STRICT 模式下,三层保护叠加。每一层都是一道防线——如果请求通过了一层,下一层会将其拦截。

flowchart LR
    UC["用户代码<br/>(代码步骤 · Piece · fetch)"]

    subgraph L1["第 1 层 — 引擎 SSRF 防护(进程内)"]
        direction TB
        L1check{"IP 在被阻止<br/>范围内?"}
    end

    subgraph L2["第 2 层 — 出口代理(回环)"]
        direction TB
        L2check{"任何 A/AAAA<br/>记录被阻止?"}
    end

    subgraph L3["第 3 层 — iptables 封锁(仅 SANDBOX_PROCESS)"]
        direction TB
        L3check{"目标地址<br/>是代理端口?"}
    end

    NET(["公网"])
    B1["SSRFBlockedError"]
    B2["403 出口被阻止"]
    B3["内核丢弃数据包"]

    UC --> L1check
    L1check -- 是 --> B1
    L1check -- 否 --> L2check
    L2check -- 是 --> B2
    L2check -- 否 --> L3check
    L3check -- 否 --> B3
    L3check -- 是 --> NET

    classDef blocked fill:#fde2e2,stroke:#d33,color:#7a1414
    classDef allowed fill:#e3f7e4,stroke:#2a7,color:#14532d
    class B1,B2,B3 blocked
    class NET allowed
引擎对 Node 的 `dns.lookup`、`Socket.prototype.connect` 和 `undici` 的全局分发器进行猴子补丁。用户在代码解析主机名或打开套接字时,防护程序会立即检查结果 IP 是否在黑名单中(所有非 `unicast` 范围——RFC1918、回环、链路本地、多播、云元数据)。一次覆盖 `axios`、`fetch`、`undici` 以及原始 `http`/`net`。 Worker 启动一个绑定到回环地址的 HTTP(S) CONNECT 代理(`proxy-chain`)。用户代码通过引擎分发器路由到此代理。代理会重新解析每个主机名并检查**每一个** A/AAAA 记录——堵住了多记录绕过(一个 IP 为公网,另一个为私有)的漏洞。 在基于隔离的沙箱中,Worker 在启动时应用 `iptables` 规则,限制沙箱 UID 的出口流量仅到代理端口。即使用户代码找到了直接访问原始套接字层的途径,内核也会丢弃数据包。

各沙箱模式的适用情况

网络安全层叠加在沙箱执行模式之上——它们是独立的选择。下表显示了各沙箱在 STRICT 模式下启用的功能:

沙箱模式引擎 SSRF 防护出口代理内核封锁
无沙箱UNSANDBOXED❌ — 无可用的按 UID 隔离
V8 沙箱SANDBOX_CODE_ONLY❌ — 引擎进程与宿主共享 UID
内核命名空间SANDBOX_PROCESS + isolate✅ — iptables 规则限定在沙箱 UID 范围内

请参阅沙箱了解各沙箱模式在进程级别的隔离方式。网络安全是正交的:它隔离沙箱可访问的内容,而不管其运行方式

在 `UNSANDBOXED` 和 V8 模式下没有内核级别的后备保护。进程内 SSRF 防护和出口代理是仅有的两层——这仍然很强,但一旦攻击者攻破了 Node 进程,进程内检查即可被绕过。在多租户部署中请使用 `SANDBOX_PROCESS`。

验证你的配置

在测试工作流的 Code 步骤中尝试:

const res = await fetch('http://169.254.169.254/latest/meta-data/')

设置 AP_NETWORK_MODE=STRICT 后,你应该会看到 SSRFBlockedError(来自引擎防护)或出口代理返回 403(针对通过主机名解析的请求)。设置为 UNRESTRICTED 时,如果宿主允许,请求将成功——确认防护已关闭。

逐步推广

  1. UNRESTRICTED(默认)开始,识别合法工作流需要访问的内部服务(内部 API、代码步骤使用的数据库等)。
  2. 将这些 IP 添加到 AP_SSRF_ALLOW_LIST
  3. 切换到 AP_NETWORK_MODE=STRICT。观察 Worker 日志中的 SSRFBlockedErrorEgress proxy refused request——每条记录要么是攻击、配置错误的工作流,要么是缺少白名单条目。

服务端出口

与流代码不同,API 服务器本身会代表管理员和用户发起出站 HTTP 请求——OAuth 令牌获取/刷新、Hashicorp Vault、CyberArk Conjur、事件目标 Webhook、On-Call 通知器、MCP 工具验证。URL 来自管理员配置(Vault 服务器 URL)或用户输入(Webhook 目标、MCP 服务器 URL),因此同样的 SSRF 风险依然存在。

与用户代码出口不同,这一层始终开启——它不需要 AP_NETWORK_MODE=STRICT。它实现为附加到 @activepieces/server-utils 中共享 axios 实例的 request-filtering-agent 包装器;每个出站请求都流经该包装器。被阻止的范围与引擎防护相同(RFC1918、回环、链路本地/云元数据、非单播)。

`AP_SSRF_ALLOW_LIST` 在两个层面之间共享。添加一个 IP 或 CIDR 一次,即可同时应用于用户代码出口**和**服务端 HTTP。更改值后请重启服务器。

当请求被阻止时,管理 UI 中显示的 axios 错误会包含 AP_SSRF_ALLOW_LIST 提示,以便运维人员在连接测试对话框中直接看到修复方法。

使用私有 IP 的自托管服务提供者

如果 Vault、Conjur、本地 OAuth2 令牌端点或内部 Webhook 解析为私有 IP,服务端过滤器将拒绝连接,直到将目标添加到 AP_SSRF_ALLOW_LIST

AP_SSRF_ALLOW_LIST=10.0.5.12,192.168.10.0/24

宽松 TLS 仍会过滤

接受自签名证书的连接器(例如私有集群中的 CyberArk Conjur)使用 rejectUnauthorized: false。在此设置下 SSRF 过滤器仍然保留——TLS 验证被放宽,但 SSRF 保护不会被绕过。