原文作者:Jason Williams-F5 Principal Product Manager Technical

原文链接:Lua 与 NGINX 的复杂协同:能力、陷阱与性能挑战

转载来源:NGINX 中文社区

NGINX 唯一中文官方社区 ,尽在 nginx.org.cn


NGINX 作为一款高性能的 Web 服务器和反向代理,通过 OpenResty 集成 Lua 后,其能力得到了显著扩展。这种强大的组合实现了动态请求处理、灵活的路由以及静态 NGINX 配置本身无法实现的诸多高级功能。然而,将 Lua 脚本嵌入 NGINX 的事件驱动架构中会引入隐形的复杂性和风险,运维人员和开发人员必须理解这些问题,以避免性能下降、系统不稳定以及运维难题。

Lua 如何融入 NGINX 的请求生命周期

NGINX 通过一系列定义明确的阶段处理请求,这些处理阶段包括 rewrite、access、content generation、header filter、body filter 以及logging。OpenResty,通过允许在不同阶段执行 Lua 代码扩展了NGINX。这些指令包括access_by_lua*、rewrite_by_lua*、content_by_lua* 等。

这种集成实现了以下动态行为:

• 自定义身份验证和授权逻辑

• 动态后端选择和负载均衡

• 实时请求和响应处理

• 指标收集和日志增强

必须谨慎管理 Lua 在各阶段的介入,因为:

• 早期阶段终止请求:在早期阶段(例如使用 ngx.exit(status))可能会中断后续的请求处理阶段,但不会停止 header filter、body filter 和 logging 等响应处理阶段。这可能导致日志不一致或意外的 header 泄漏。在早期阶段(例如 access_by_lua*)使用 ngx.exit(status)(其中 status 为 200 或以上)会中断请求处理阶段(如 content 或 balancer),但不会中断响应处理阶段(如 header filter、body filter 和 logging)。这种不彻底的终止可能导致日志不一致或内部 header 泄漏。

• 变量作用域与时序问题:如果对执行时机和作用域理解有误,在某个阶段设置的变量可能无法在后续阶段使用,或者其值已经过期,从而导致错误的路由或访问决策。例如,依赖在不同阶段(例如 set_by_lua* 与 access_by_lua*)设置的变量,可能导致 NGINX 变量($var)为空或仍然使用旧值,从而引发错误的路由、日志记录或访问决策。

• 请求的过早终止:NGINX 核心模块提前终止请求,可能导致后续阶段(如 logging)中的 Lua 处理程序无法运行,从而造成可观测性数据缺失。

共享状态与并发挑战

NGINX 的 worker 进程会在共享的 Lua VM 实例中运行 Lua 代码,这会带来一些并发方面的陷阱:

• 全局变量风险:Lua 模块中的全局变量会在同一个 worker 处理的所有请求之间共享,如果在请求处理过程中对其进行修改,可能引发竞态条件(race condition)和状态不一致。

• 保持严格的非阻塞特性:Lua 代码必须保持严格的非阻塞状态(non-blocking),以维护 NGINX 的事件驱动性能。阻塞操作(例如标准 Lua I/O 或 OS 调用)会使整个 worker 停滞,导致高延迟和请求超时。使用执行阻塞 I/O 的标准 Lua 库或 C 库(如标准的 os.time() 或较慢的文件 I/O)是一个主要陷阱,它会阻塞整个 NGINX worker 进程,导致该 worker 处理的所有并发请求出现严重的性能下降、高延迟和请求超时。

• 不恰当的OpenResty cosocket(非阻塞网络 socket)处理:可能导致资源泄漏或连接失效,进一步降低性能。一个常见问题是在超时或错误发生后,没有将 cosocket 连接释放回 keepalive 连接池,这不仅导致浪费资源,还会导致下一个请求获取到陈旧或断开的连接。

• 协程崩溃风险:虽然 Lua 协程(coroutine)能够隔离请求的执行,但未捕获的错误或VM panic 仍可能导致整个 worker 进程崩溃,从而影响所有并发请求。

Kubernetes Ingress-NGINX 与 Lua:动态配置风险

广受欢迎的 Kubernetes ingress controller —— Ingress-NGINX 大量使用 Lua 来实现动态后端更新和路由逻辑。这种动态方式带来了额外的挑战:

• Lua 脚本或共享字典(ngx.shared.DICT)管理中的 bug可能会破坏流量路由,导致请求被发送到不可用或过期的 Pod。如果未对字典中的键实施 TTL(Time-To-Live)或适当的淘汰策略,会导致字典被填满,进而引发内存溢出(Out-of-Memory)错误或缓存抖动(cache thrashing)。

• 虽然 Lua 可以无需完全重载 NGINX的情况下实现动态配置,但部分变更仍然需要重载,这可能引起短暂的连接排空(connection draining)或延迟峰值。

• 由 Lua 驱动的频繁动态更新可能会导致 NGINX master 进程无法正确回收 worker 子进程,进而导致僵尸进程在宿主机操作系统上累积。这些僵尸进程会消耗系统资源,并使进程管理复杂化。

性能与稳定性:重大风险

Lua 的灵活性也伴随着稳定性方面的权衡:

• 内存泄露:Lua 闭包或第三方模块中的内存泄漏,可能导致 worker 进程逐渐消耗更多内存,直到被 Kubernetes 因 OOM 将其终止。worker 进程的内存泄漏(导致 OOM 崩溃)通常由 Lua 闭包泄漏(变量在请求间无意中被持续保留)或第三方 Lua 模块中的错误引起的。

• 阻塞事件循环:未经优化的 Lua 代码或外部调用阻塞事件,会导致严重的延迟峰值和请求超时。

• 基于 Lua 的负载均衡逻辑,尤其是在 Pod 数量较多的时候,可能会导致严重的流量分布不均,一小部分后端 Pod 承担了绝大部分流量,从而产生 “hot pod” 和 “cold pod”这种问题

• 僵尸进程:由 worker 回收不当产生的僵尸进程,会增加运维复杂度并造成资源浪费。当 NGINX master 进程无法正确回收 worker 子进程时,就会出现僵尸进程的累积,这通常由 Lua 驱动的频繁动态端点更新所触发。

运维复杂性与安全隐忧

• 通过 annotation 中的 Lua 代码片段实现的高级功能,会导致配置蔓延、配置漂移以及审计困难。

• 通过用户提供的 annotation 注入 Lua 或 NGINX 配置,历史上曾引发严重的远程代码执行(remote code execution, RCE)漏洞。

• 配置同步问题有时需要人工干预,通过删除并重新创建 Kubernetes Service 和 Ingress 才能解决。

生态系统管理风险

第三方模块的不稳定性与版本控制

Lua 模块生态系统的动态和快速迭代特点,增加了维持系统稳定性的复杂度。源于第三方 Lua 模块中的错误是导致内存消耗持续无限增加并引发 OOM 崩溃的原因之一。如果不对模块版本和依赖项进行严格控制,运维人员将面临难以调试的不易察觉的不稳定风险。

Ingress-NGINX 中 Lua 特有的安全漏洞

将 Lua 脚本集成到 Ingress-NGINX 中虽然提供了强大的扩展能力,但历史上也曾暴露过严重的安全漏洞,这些漏洞可能会危及整个 Kubernetes 集群的安全。它们源于在 NGINX 配置中允许执行 Lua 代码的灵活性与危险性,特别是通过用户控制的 annotation。

Annotation 注入攻击面

Ingress-NGINX 会处理包含用户提供 annotation 的 Kubernetes Ingress 对象,这些 annotation 可以影响 NGINX 和 Lua 配置的生成过程。多个严重漏洞(CVE-2021-25742、CVE-2025-1097、CVE-2025-1098、CVE-2025-24514)都曾利用了该机制实现了越权访问和远程代码执行。

CVE-2021-25742

自定义 Snippet 权限提升——在 v1.0.1 和 v0.49.1 之前的版本中,拥有创建或更新 Ingress 对象权限的用户,可以利用自定义 snippet 功能(通过 Lua annotation 实现)注入任意 Lua 代码。这允许攻击者获取 Ingress-NGINX 的 Service Account Token,并访问集群内所有 namespace 中的 secret,从而有效接管整个集群。该漏洞的 CVSS 评分为 8.1(高危),需要通过禁用 allow-snippet-annotations 设置来进行缓解。

CVE-2025-24514

CVE-2025-1097

CVE-2025-1098

Annotation 解析器注入链

2025 年发现的一系列漏洞表明,即使限制了 snippet 之后,基于 Lua 的 annotation 解析器仍然容易受到注入攻击。auth-url、auth-tls-match-cn 以及 mirror UID 解析器在将用户输入并入 NGINX/Lua 配置之前,未能对其进行妥善的清理。攻击者可以构造恶意的 Ingress annotation,当Admission Controller 基于 Lua 的验证逻辑对其进行处理时,会将任意指令注入到 NGINX 配置模板中。

Admission Controller :未认证的攻击面

Ingress-NGINX 的 admission controller 会在 Ingress 对象部署之前,通过生成并测试 NGINX 配置(其中包含 Lua 指令)来对其进行验证。该组件存在若干 Lua 特有的安全隐患:

• 未认证的网络暴露:默认情况下,admission controller 的 webhook endpoint 可在网络上被访问且无需身份验证。任何拥有集群网络访问权限的攻击者,都可以直接向该端点发送精心构造的 AdmissionReview 请求,其中包含能够影响 Lua 执行的恶意 annotation。

• Lua 模板处理漏洞:admission controller 使用基于 Lua 模板的 NGINX 配置。在处理 annotation 时,用户可控的值会**入这些模板,并在 nginx -t 验证阶段进行评估。Lua annotation 解析器中输入清理不充分,使攻击者能够脱离其预期的上下文,并注入任意 Lua 代码或 NGINX 指令。

• 权限上下文风险:admission controller 通常以较高的 Kubernetes 权限运行,包括访问所有 namespace 中 secret 的权限。一旦注入攻击成功,攻击者便可利用这些特权,在该权限上下文中提取敏感信息或执行代码。

更深层的启示:Configuration-as-Code 的攻击面

Ingress-NGINX 的 Lua 漏洞揭示了云原生安全中的一个挑战:当配置通过 Lua 脚本、annotation 或模板变为代码,并且该配置受到用户输入影响时,攻击面会显著扩大。正是这种使 Lua 在动态路由和高级功能上大放异彩的灵活性,同时也创造了可绕过传统安全控制的注入途径。

结论

NGINX 对 Lua 的集成(特别是在像 Ingress-NGINX 这样的 Kubernetes Ingress 控制器中)提供了强大的动态功能,但也引入了一系列复杂的挑战。为了维持稳定的部署,深入理解 NGINX 各阶段的细微差别、Lua 的并发模型、同步和状态管理相关的运维风险,同时避免阻塞事件循环(此乃“首要大忌”),并防止因内存泄漏或僵尸进程导致的资源耗尽是至关重要的。此外,复杂的annotation 扩散所带来的运维开销,以及与配置注入相关的固有安全风险(如远程代码执行漏洞),都需要进行谨慎处理,以确保系统完整性。

为了直观理解将 Lua 集成到 NGINX 中所需的谨慎程度,您可以把它想象成在一辆仍在行驶的赛车上做手术。这辆车(NGINX)为速度而设计,要求一切都是非阻塞且即时的(事件循环,event loop);但如果技师(Lua 脚本)使用了标准但缓慢的工具(阻塞式 I/O),或在发动机舱内遗留了一个多余的部件(共享全局变量或内存泄漏),整辆车就会卡死或撞毁。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐