3、函数执行环境安全:运行时环境隔离、冷启动与热启动安全、临时磁盘安全、环境变量安全

好,咱们接着聊函数执行环境的安全。这部分内容,说白了就是你的代码在云上跑的时候,那个「房间」到底安不安全。我见过不少团队,花了大把精力搞网络策略、搞身份认证,结果函数执行环境本身却漏成了筛子。嗯,今天咱们就把这个「房间」里里外外翻一遍。

3.1 运行时环境隔离:你的代码和邻居之间隔了几堵墙?

Serverless 函数是共享宿主机的,这是个基本事实。你想想看,你的函数和别人的函数,可能跑在同一个物理机甚至同一个内核上。那怎么保证谁也看不到谁?

这里的关键是「隔离边界」。我个人习惯把隔离分成三个层次:

  • 进程级隔离:每个函数实例本质上是宿主机上的一个独立进程。操作系统天然提供了进程隔离——不同进程的虚拟地址空间是分开的。但光靠这个不够,因为进程间通信(IPC)和共享内存可能成为突破口。
  • 容器级隔离:现在主流平台(AWS Lambda、阿里云函数计算)都用容器来跑函数。每个函数实例跑在一个轻量级容器里,有独立的文件系统、网络栈和进程命名空间。这层隔离比纯进程强得多。
  • 微VM级隔离:这是更狠的做法。比如 AWS 的 Firecracker,每个函数跑在一个微型虚拟机里,有自己的内核。我在项目中遇到过客户问:「容器隔离够不够?」我的回答是:对于大多数场景,容器隔离足够了。但如果你处理的是金融数据或医疗数据,微VM隔离会更让人安心。

关键认知:环境隔离不是「有或没有」的问题,而是「多深」的问题。你需要根据数据敏感度选择合适深度的隔离方案。

3.2 冷启动与热启动安全:两个完全不同的世界

冷启动和热启动,不仅仅是性能差异,安全风险也完全不同。我刚开始接触 Serverless 时,以为安全策略一套走天下,后来发现太天真了。

冷启动的安全风险

冷启动时,平台要为你创建一个全新的执行环境。这个过程包括:

  • 下载函数代码包
  • 初始化运行时(比如启动 Node.js 或 Python 解释器)
  • 执行初始化代码(全局变量、数据库连接等)

这里有个坑:初始化代码中的敏感信息。我曾经见过一个项目,在全局作用域里直接硬编码了数据库密码。冷启动时,这些密码会留在内存里,直到实例被回收。更糟的是,如果平台的内存回收不彻底,下一个函数实例可能读到残留数据。

警告:不要在初始化代码中处理敏感数据!冷启动时执行的初始化逻辑,应该只做无状态的事情。敏感信息请通过环境变量或密钥管理服务获取。

热启动的安全风险

热启动时,函数实例被复用。这听起来很美好——省去了冷启动的延迟。但安全上有个隐患:上下文污染

举个例子:你的函数在处理完一个用户的请求后,在全局变量里留下了某些数据。下一个请求(可能是另一个用户的)复用了同一个实例,如果代码没清理干净,就可能读到上一个用户的数据。

我记得有一次排查线上问题,发现用户 A 偶尔能看到用户 B 的部分信息。查了半天,原来是全局缓存没做好隔离。说白了,就是热启动复用了实例,但代码没处理好「每次请求独立」这件事。

我的建议:每次函数调用都应该被视为「从零开始」。不要依赖全局变量来存储请求级别的数据。如果一定要用缓存,请确保缓存 key 包含用户标识,并且设置合理的 TTL。

3.3 临时磁盘安全:那个 /tmp 目录不是保险箱

每个函数实例都有临时磁盘空间(通常是 /tmp)。很多开发者把它当临时文件存放点,但很少有人想过它的安全性。

临时磁盘的安全问题主要有两个:

  1. 实例复用导致的数据泄露:热启动时,同一个实例的 /tmp 目录可能还残留着上一次调用的文件。如果你的函数写入了敏感数据(比如临时密钥、解密后的文件),下一个调用者可能读到。
  2. 多租户共享存储:虽然 /tmp 是实例级别的隔离,但底层存储可能是共享的。极端情况下,如果宿主机有漏洞,攻击者可能跨实例访问 /tmp 数据。

我曾经在项目中遇到过一个问题:一个图像处理函数把用户上传的图片临时存到 /tmp,处理完就删了。但有一次并发高的时候,用户反馈说看到了别人的图片。查下来发现,函数在删除文件前抛了异常,文件没删掉,下一个请求复用了实例,就读到了残留文件。

最佳实践

  • 每次函数调用开始时,清理 /tmp 中上次残留的文件
  • 敏感数据使用完后立即擦除(覆写或删除)
  • 不要在 /tmp 中存储永久性数据
  • 考虑使用内存而不是磁盘来处理临时数据

3.4 环境变量安全:最容易忽视的「明文密码本」

环境变量是 Serverless 函数配置敏感信息的标准方式。但说实话,很多人对它的安全性过于乐观了。

环境变量的问题在于:

  • 明文存储:在平台控制台或配置文件中,环境变量通常是明文可见的。任何有控制台访问权限的人都能看到。
  • 日志泄露:函数如果打印了环境变量(比如调试日志),这些信息会进入日志系统。我见过不止一次,开发者在排查问题时把整个 process.env 打到了日志里。
  • 子进程继承:如果你的函数启动了子进程,子进程会继承所有环境变量。这意味着子进程里的任何代码都能访问到这些敏感信息。

警告:永远不要在环境变量中存储真正的密钥!环境变量应该只存储「指向密钥的引用」,比如密钥在密钥管理服务中的 ID 或名称。

那正确的做法是什么?我个人习惯用三层策略:

层级 存储内容 示例
环境变量 非敏感配置 DB_HOST=prod-db.example.com
密钥管理服务 敏感密钥 DB_PASSWORD_ARN=arn:aws:secretsmanager:...
运行时获取 动态凭证 函数启动时从 KMS 获取并缓存

代码示例:

// 不推荐:直接把密码放环境变量
const password = process.env.DB_PASSWORD;

// 推荐:环境变量只存密钥的引用
const secretArn = process.env.DB_PASSWORD_ARN;
const password = await getSecretFromKMS(secretArn);

嗯,这里要注意:从密钥管理服务获取密钥时,最好在初始化阶段做一次,然后缓存到内存里。别每次请求都去拉一次,那样性能太差了。

3.5 总结一下

函数执行环境安全,说白了就是管好四个东西:

  • 隔离:选对隔离级别,别让邻居看到你家
  • 冷热启动:冷启动防初始化泄露,热启动防上下文污染
  • 临时磁盘:用完就擦,别留痕迹
  • 环境变量:别当明文密码本用,该上密钥管理就上

这些点看起来简单,但我在实际项目中踩过的坑,十有八九都是这些基础问题。你想想看,攻击者不需要多高深的技术,只要抓住一个没清理干净的 /tmp 文件,或者一个打印到日志的环境变量,就能捅出大篓子。所以,别嫌麻烦,该做的防护一个都不能少。