English summary: report vulnerabilities through GitHub private advisories; 90-day coordinated disclosure.
本文适用于边缘节点 edgeweir-node。控制面 edgeweir/edgeweir 遵循同一套信任基线,控制面问题请使用控制面私密安全公告。
请使用 GitHub 私密安全公告。请不要开公开 issue、PR,也不要在讨论区贴细节。
报告中请尽量包含:
- 受影响的版本:
edgeweir-node version的输出(控制面问题请附控制台版本) - 复现步骤、相关配置或 PoC
- 影响:攻击者能做到什么,需要什么前提(例如是否需要已注册的节点、是否需要控制台账号)
- 是否希望署名,以及署名方式
处理流程:
- 我们会尽快确认收到,目标 3 个工作日内。
- 确认漏洞后,和你一起评估严重程度,商定修复计划。
- 采用 90 天协调披露:从收到报告起 90 天内公开。如果修复版本提前发布,会提前披露。
- 安全公告和发布说明中会致谢报告者,除非你希望匿名。
| 版本 | 是否提供安全修复 |
|---|---|
master 最新代码 |
是 |
| 其他版本 | 否,请升级 |
1.0 之前只维护 master 分支的最新代码,修复不回移到旧版本。正式发布时会更新支持周期。
以下原则同时适用于 edgeweir 和 edgeweir-node 两个开源核心仓库。违反其中任何一条,都按安全问题处理。独立商业产品的范围见 LICENSING.md;官方许可证到期或授权服务故障不得停用核心、清除配置或中断已有 CDN 流量,节点不参与官方商业授权。
- 没有任何形式的 phone-home,代码里没有授权或许可证校验。
- 遥测默认关闭,必须由运维者显式开启。节点目前完全没有遥测:状态和统计只发给运维者自己部署的控制台。
- 私钥、DNS API 密钥、源站凭据等敏感数据,由控制面用主密钥做信封加密后才入库;控制面从不保存 SSH 凭据。
- 所有管理操作都写审计日志(控制面)。
- 发布物全部签名,并附校验说明,见下文"验证发布物"。
- CI 构建产物与源码一一对应。构建可复现:使用
-trimpath,模块文件时间戳固定为提交时间,兼容SOURCE_DATE_EPOCH。可复现目标覆盖二进制、安装包和压缩包;容器镜像不逐字节可复现,其中之一是它内置构建当天的 IPinfo Lite 数据库:构建时从 IPinfo 下载,按 IPinfo 公布的 sha256 校验并由 agent 自身的读取代码检查,所含副本由镜像内NOTICE的 sha256 标识。 - 节点的 OpenResty(edgeweir-openresty)从
packaging/openresty/sources.lock固定的源码构建:每个源码包校验 SHA-256,OpenResty、OpenSSL、PCRE2、zlib、Zstandard、ModSecurity、ModSecurity-nginx 与 OWASP CRS 另以固定公钥校验上游的 PGP 签名;工具链来自冻结的 AlmaLinux 9.7 仓库,同一输入构建出的包逐字节相同。OpenSSL、PCRE2、zlib 静态链接,升级它们需要发布新的 edgeweir-openresty。 - 下载数据用的 IPinfo token 只以 BuildKit secret 传给构建,不进入镜像层、构建参数、来源证明或日志;节点运行时不联系 IPinfo。
身份与注册
- 节点私钥(ECDSA P-256)在节点本地生成,保存为
/var/lib/edgeweir-node/node.key,权限0600,从不传输。注册时只发送 CSR。 - 注册 token 一次性有效。节点先完成 TLS 握手,确认控制面内部 CA 与安装命令中的
--ca-sha256一致,之后才发送 token。 - token 通过
EDGEWEIR_TOKEN环境变量(install.sh的做法)或--token-file传给edgeweir-node enroll,不出现在进程列表里;--token仍可用,但同机其他用户能在ps里看到它,使用时会打印警告。读取后环境变量被删除,不传给子进程。 Enroll返回的 CA 证书也必须与同一个 sha256 匹配,否则注册失败。- 注册之后每个 RPC 都走 mTLS,客户端证书 CN 为节点 ID。证书自动续期。
- 区域探针(
edgeweir-node probe)以同样的方式注册:一次性探针 token,私钥在本机生成并保存为probe.key(0600,默认目录/var/lib/edgeweir-probe,与节点的分开),先确认 CA 与--ca-sha256一致再发送 token;之后全程 mTLS,探针证书(O=Edgeweir Probe)不能调用节点 RPC。探针不启动 OpenResty、不监听端口,systemd unit 不授予任何 capability。探针的 HTTPS 探测不校验节点的自签名健康证书,只判断可达,不发送任何机密。 - 控制面绝不保存 SSH 凭据(没有保存的选项),也没有 SSH 远程安装:节点只通过控制台生成的一次性安装命令(或手动安装)接入,由节点主动注册。
本机保存的敏感文件
状态目录 /var/lib/edgeweir-node 权限 0700,属于服务用户。其中:
node.key(0600):节点私钥。credentials.json(0600):当前与前一份同集群 LKG 配置引用的 S3 源站凭据,access key 和 secret key 是明文。它让节点在控制面不可达时重启后仍能为 S3 源站签名;凭据只经 mTLS 的GetOriginCredentials获取,不进入配置和 LKG,两份配置均不再引用时从文件中删除。能读取该文件的人可以访问对应的存储桶,请只授予只读的最小权限。- 访问鉴权(
access-auth-v1)的密钥同样存在credentials.json:Basic 用户的 PBKDF2-HMAC-SHA256 加盐哈希与 URL 鉴权的主备密钥,经GetOriginCredentials获取,不进入配置、LKG 与日志(警告只含网站与规则 ID);它们随站点表进入数据面共享内存,拿到节点 root 权限者可以读取,能据此签发有效的签名 URL 或离线猜测 Basic 密码。 purge.json(0600):清缓存标记和任务时间,不含敏感数据。文件存在但无法读取或解析时,节点在下一次应用配置时给每个站点加一个全站标记(宁可多刷);文件缺失则视为没有标记,已清除的内容会重新可见,所以不要删除它。config/(目录 0700,current.binpb、previous.binpb为 0600):last-known-good 配置及其备份,不含凭据。bans.json(0600):控制台下发的动态封禁(被封禁的地址、范围、到期时间)和已应用的序号。文件无法读取时节点从空集合开始,等控制台重新下发。health.crt、health.key(0600):节点为探针健康端点生成的自签名证书与私钥,只用于 SNIhealth.edgeweir.invalid与无 SNI 的 TLS 握手,与站点证书无关;这样的连接只能访问/.edgeweir/health。session-ticket-keys.json(0600)与 nginx 前缀下的conf/tls-tickets/<id>.key(0600,目录 0700):当前与前一份同集群配置引用的 TLS 会话票据密钥(每把 80 字节,集群内共用,经 mTLS 的GetSessionTicketKeys获取,不进入配置和 LKG)。能读取它们的人可以解密用这些密钥签发的会话票据、恢复别人的 TLS 会话;控制台每 12 小时轮换一次,一把密钥最长约 36 小时后不再使用。challenge-keys.json(0600):当前与前一份同集群配置引用的挑战凭证密钥(HMAC-SHA256,集群内共用)。密钥只经 mTLS 的GetChallengeKeys获取,不进入配置和 LKG;两份配置均不再引用时从文件中删除。能读取该文件的人可以为该集群的站点伪造通行凭证,直到控制台轮换掉这把密钥(每天一次,最长 48 小时后失效)。
访客 TLS
- 从不接受 TLS early data(0-RTT):每个 HTTPS 与 QUIC server 显式
ssl_early_data off。 - 会话按网站隔离:每次握手设置网站的会话 ID 上下文(网站、最低版本、客户端证书设置与证书的哈希),一个网站的会话不能在另一个网站的 SNI 下恢复,设置变化后旧会话失效。四层应用的 TLS 不复用会话。
- 访客客户端证书(mTLS):校验失败或缺失不中止握手,
required的网站在站点逻辑之前返回 403;访客自带的X-Client-Verify、X-Client-Cert-SHA256、X-Client-Cert-Subject、X-Client-Cert-Serial在所有网站上删除,只有节点按网站设置向源站发送它们。不检查客户端证书的吊销。
挑战与通行凭证
- 挑战参数与通行凭证无状态、HMAC-SHA256 签名,绑定站点、客户端网段(IPv4 /24、IPv6 /64)与 User-Agent 哈希;挑战参数 5 分钟内有效且只能兑换一次(节点记录已用 nonce)。验证后只跳转到本站以单个
/开头的路径,不存在开放重定向。 - 挑战页自包含,不引用外部 URL,CSP 使用每个响应独立的 nonce;验证码图片由节点本地生成,答案不离开节点。
- 保留前缀
/.edgeweir/由边缘层直接处理,永不转发源站。 - 访问控制(
access-control-v1)在边缘层、缓存查找之前判定,缓存命中同样生效;防盗链与 UA 名单只看访客可以伪造的请求头,不能代替访问鉴权;WebSocket 来源名单拒绝没有Origin的升级请求。 - 挑战失败封禁(
challenge-v2):verify 的失败按网站与客户端网络(IPv4 地址、IPv6 /64)计数,平台放行名单、本站放行名单与可信代理不计数,避免一个出口后的访客被别人的失败连带封禁。 - 已验证爬虫(
challenge-v2):只认反向解析(PTR)得到搜索引擎官方域名、且该名称的正向解析含访客地址的请求,伪造 PTR 过不了正向检查;查询经节点自己的解析器,结果的可信度取决于该解析器(解析器被劫持时可能放行冒充者,不可用时爬虫照常被挑战)。不下载第三方 IP 列表;结果按地址缓存,同一地址同时只查一次。
源站与回源
- 源站不能指向特殊地址段(回环、链路本地/云元数据、私网、CGNAT、文档、基准测试、组播、保留地址,IPv4 映射与 NAT64 地址按内嵌 IPv4 判断),除非平台管理员把它放进允许清单(
origin_allowed_cidrs)。配置里的 IP 字面量和每个 DNS 解析结果都会检查,租户无法用源站读取云元数据或访问节点所在网络。完整列表见 ARCHITECTURE.md §3.5。 - 发往源站的请求带
CDN-Loop(RFC 8586);带有本节点标识的请求直接返回 508,指向节点自己的源站不会无限递归。 - 回源 HTTPS 默认按源站配置的名称(SNI / Host)用
--trusted-ca或系统 CA 校验证书;关闭了 nginx 1.29.7 起默认开启的、只按地址匹配的上游连接缓存,校验过的连接不会被复用给要校验其他名称的请求。 - agent 的主动健康检查遵守同一地址策略:解析结果按允许清单过滤,只连接检查过的地址,不跟随重定向;HTTPS 探测同样用
--trusted-ca或系统 CA 校验(ARCHITECTURE.md §2.10)。 - 预热与 sitemap 预热只经本机边缘监听(回环地址或 unix socket)请求,源站地址策略照常生效,控制台不发起任何请求;https 预热连接节点自己的 HTTPS 监听,不校验它自己的证书。
- S3 源站收不到客户端的
x-amz-*请求头(节点只签名自己的头),客户端也无法伪造X-Edgeweir-*内部头。 - 带
Authorization的请求默认不查缓存也不存储(RFC 9111 §3.5),只有规则显式设置cache_authorized才缓存。 - 站点、源站、规则 id 只接受
[A-Za-z0-9_-],避免它们在数据面的键里互相冒充。
节点扩展(extensions-v1)
- 扩展是运营者在控制台信任的发布者用 Ed25519 签名的 Lua 扩展包,默认没有受信任的密钥。agent 解包前独立校验 JWS、摘要、大小、发布者与清单(与控制台相同的规则),归档不能含链接、原生代码或字节码,大小与条目数有上限;只经 mTLS 按摘要获取,不从发布者下载,也不向发布者报告。
--extension-keys FILE让节点只接受本地文件里的公钥,即使控制台失陷信任了别的密钥;--no-extensions让节点完全不运行扩展。在意控制台失陷后果的部署应使用其中之一。- 扩展以 nginx worker 的权限在 worker 内运行,能读取并修改开启它的网站的请求与响应(包括 Cookie 与凭据),能访问 worker 能访问的共享内存、套接字与文件,有
http_client时还能连接网络。受限环境只是防误用,不是沙箱。钩子在pcall中按时间预算运行,反复故障熔断,默认失败放行。
本地攻击面
- 本地控制 API 只监听 unix socket,从不监听 TCP。
- OpenResty 内层回源层(
origin.sock、origin-noverify.sock)和给预热用的本地边缘监听(edge.sock)同样只监听 unix socket。 - agent 和 OpenResty 以非特权用户运行:容器内是
edgeweir用户(uid 10001);deb/rpm 包创建系统用户edgeweir,systemd unit 只授予CAP_NET_BIND_SERVICE(用于监听 80/443),开启NoNewPrivileges、ProtectSystem=strict等文件系统和内核保护选项,详见packaging/systemd/edgeweir-node.service中的说明。 - 内核封禁(nftables)需要
CAP_NET_ADMIN,默认不授予:systemd 需要运维者加 drop-in,容器需要--cap-add NET_ADMIN且镜像以NFT_CAPABILITY=true构建(见 ARCHITECTURE.md §5)。授予后 agent 只管理自己的表table inet edgeweir,控制台地址、本机地址、回环和平台allow名单永不丢弃;agent 执行的 nft 脚本只由校验过的地址生成。systemd 的环境能力同样被 OpenResty 子进程继承;容器里nft带文件能力后,容器内任何进程都能用它修改该容器网络命名空间的规则。 - 控制 socket 所在目录权限为
0750,只有服务用户能连接。
配置处理
- 配置在使用前校验
content_hash,无效的部分直接拒绝,不会带病应用。 - 从磁盘加载 last-known-good 配置时同样校验哈希。
- 客户端请求中自带的
X-Edgeweir-*头会被删除,防止伪造内部头。 - 内部头
X-Edgeweir-Waf与X-Edgeweir-Waf-Ex(CRS 的设置与按路径排除的条目)只由边缘层设置,回源前删除。按路径排除由 Lua 按 nginx 规范化后的路径判断,不交给 ModSecurity 的REQUEST_FILENAME(不规范化,/a/../b可绕过前缀匹配);原始路径有歧义或含//(源站可能路由到别处)时不用任何排除条目;条目以内容哈希令牌对应生成的规则,站点表先于 reload 更新时不会把一个条目用到另一个路径上。 - 规则读取请求体(
rules-body-v1)只发生在规则(网站或全局)引用了请求体字段的网站、且表达式真正求值时;只读Content-Length不超过网站上限(至多 1 MiB)的请求体,分块编码、HTTP/2 与 HTTP/3 中没有Content-Length、超过上限、WebSocket 与 gRPC 的请求体一律不读、视为截断,规则不能用来缓冲任意大的请求体。请求体不写日志。 - 访问日志(
access-logs-v2):客户端 IP、User-Agent、Referer(不含查询与片段)与网站开启时的查询串、请求头(从不记录Authorization、Cookie、Proxy-Authorization)、直连对端地址可能属于个人数据,采样后经 mTLS 上报。实时查看(edgeweir-node accesslog)只经本地控制 socket,只在有人查看时采集,记录 10 秒后过期,不含这些可选字段。内部头X-Edgeweir-Upstream(回源地址)只由回源层设置(先删掉源站带来的同名头),边缘层从不转给客户端,缓存命中同样。 - 规则封禁(
waf-v2)的前缀至多 IPv4 /16、IPv6 /48;平台放行名单、可信代理与本地监听的地址从不写入,网站范围不封本站放行名单;同一网络已有封禁时不再写入或上报。 nginx.conf里只写入经过校验的值(端口、zone 名、大小、路径),站点等可变数据走 unix socket 进入 Lua,不拼进配置文件。
每个 release 附带:
checksums.txt:所有发布文件的 sha256checksums.txt.sigstore.json:checksums.txt的 cosign keyless 签名 bundle*.sbom.json:syft 生成的 SBOM- SLSA 构建来源证明(GitHub artifact attestations)
验证步骤:
# 1. 验证 checksums.txt 的签名(cosign v3)
cosign verify-blob \
--bundle checksums.txt.sigstore.json \
--certificate-identity-regexp '^https://github\.com/marvinli001/edgeweir-node/\.github/workflows/release\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
checksums.txt
# 2. 验证下载文件的哈希
sha256sum --ignore-missing -c checksums.txt
# 3. 验证构建来源(GitHub artifact attestations)
gh attestation verify edgeweir-node_<version>_linux_amd64.tar.gz --repo marvinli001/edgeweir-node容器镜像 ghcr.io/marvinli001/edgeweir-node 同样用 cosign keyless 签名:
cosign verify ghcr.io/marvinli001/edgeweir-node:<tag> \
--certificate-identity-regexp '^https://github\.com/marvinli001/edgeweir-node/\.github/workflows/release\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com控制台提供的 install.sh 在执行任何内容之前,都会先完成 sha256 和 cosign 校验,注册 token 经 EDGEWEIR_TOKEN 环境变量交给 edgeweir-node enroll。控制台可以镜像转发二进制(国内访问 GitHub 较慢),但镜像的文件必须通过同样的校验。唯一的例外是 --allow-unsigned:它只供开发使用(例如安装本地快照构建),会跳过 cosign 签名校验,只剩 SHA-256 校验,而 checksums.txt 本身此时未经验证,所以它不能证明发布物来自官方 release;生产环境不要使用。
GoEdge 出过官方二进制投毒事件;它的控制面还保存节点的 SSH root 凭据,2025 年 RingH23 攻击正是借此横向投毒了所有边缘节点。Edgeweir 针对这两点做了设计:发布物可以独立验证签名和来源,控制面不保存 SSH 凭据,节点私钥也从不离开节点。