Serverless 真不是“免运维”:函数隔离、依赖漏洞和权限坑,一个都不能少
作者:Echo_Wish
很多人第一次接触 Serverless,会有一种特别舒服的感觉:
“代码丢上去就能跑,不用买服务器,不用维护操作系统,也不用半夜起来处理机器宕机。”
听起来确实很香。
但问题也恰恰出在这里。
Serverless 不是没有安全问题,而是安全问题换了个地方。
以前我们做安全,天天盯着服务器、容器、端口、防火墙;到了 Serverless,攻击面开始往函数代码、第三方依赖、身份权限、事件入口这些地方转移。
尤其是下面三个坑,非常容易被忽略:
函数隔离不等于绝对安全;依赖安装不等于依赖可信;函数能调用资源不等于应该拥有全部权限。
这篇文章,我们就聊聊 Serverless 安全里最容易踩的三个坑,以及到底应该怎么做。
一、先别把 Serverless 想得太美:函数也可能成为攻击入口
传统应用一般是:
用户
↓
Nginx
↓
Web服务
↓
数据库
Serverless 则可能变成:
用户
↓
API Gateway
↓
Function A
↓
Function B
↓
数据库 / OSS / MQ / Redis
看起来组件少了,实际上调用链可能更长。
例如一个订单查询函数:
def get_order(event):
order_id = event["order_id"]
return db.query(
f"SELECT * FROM orders WHERE id = {order_id}"
)
很多人看到这里第一反应:
“函数这么短,应该没什么安全问题吧?”
恰恰相反。
这里直接就可能存在 SQL 注入。
攻击者传入:
1 OR 1=1
最后拼出来:
SELECT * FROM orders WHERE id = 1 OR 1=1
Serverless 本身并不会自动帮你解决业务代码里的漏洞。
所以我的一个观点是:
Serverless 解决的是“服务器怎么运维”,不是“代码怎么安全”。
这两个事情一定要分开看。
二、函数隔离:别以为“一个函数一个容器”就万事大吉
Serverless 最大的安全基础之一,就是函数隔离。
一般来说,云平台会通过容器、沙箱、运行时等机制,让不同函数实例之间尽量隔离。
比如:
Function A
┌──────────────┐
│ Runtime │
│ Code │
│ Memory │
└──────────────┘
Function B
┌──────────────┐
│ Runtime │
│ Code │
│ Memory │
└──────────────┘
理论上:
A 不能直接访问 B 的内存
A 不能直接读取 B 的代码
A 不能直接访问 B 的临时文件
但是,这里有一个非常容易被忽略的问题:
隔离 ≠ 安全。
因为真正危险的东西,往往不是函数之间直接“串门”,而是函数本身拥有的资源访问能力。
例如:
攻击者
↓
订单查询函数
↓
函数身份
↓
OSS
↓
所有业务文件
假设这个函数只是负责:
查询订单
结果你给它的身份权限是:
OSS:
读
写
删除
全部 Bucket
那这个函数一旦被攻破,攻击者拿到的就不只是“查询订单”的能力。
而是:
这个函数身份能够访问的所有资源。
这才是 Serverless 安全里非常核心的问题。
三、最小权限:函数需要什么,就给什么
如果只能给 Serverless 安全总结一句话,我会选择:
函数需要什么权限,就给什么权限,不需要的权限一个都别给。
这就是最小权限原则。
比如我们有一个图片处理函数:
Upload Image
↓
Image Function
↓
读取 OSS
↓
压缩图片
↓
写入 OSS
那么它可能只需要:
bucket:image-source
GetObject
bucket:image-result
PutObject
而不是:
OSS:
*
更不要直接:
Resource:
*
Action:
*
因为这相当于告诉云平台:
“这个函数什么都能干。”
一旦函数出现漏洞,攻击者就会非常开心。
四、权限不是越大越方便,而是越大越危险
很多开发人员有一个很典型的习惯:
接口报:
AccessDenied
怎么办?
加权限。
又报:
AccessDenied
继续加。
最后:
{
"Action": "*",
"Resource": "*"
}
好了。
系统不报错了。
开发人员:
“搞定!”
安全人员:
“你这不是搞定,你这是把门拆了。”
😂
这其实是企业里非常常见的权限管理问题。
正确的思路应该是:
函数
↓
明确业务动作
↓
明确资源
↓
明确操作
↓
配置最小权限
例如:
{
"Effect": "Allow",
"Action": [
"oss:GetObject"
],
"Resource": [
"arn:cloud:oss:::order-images/*"
]
}
而不是:
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
前者是:
“我允许你拿订单图片。”
后者是:
“你想干什么随便。”
两者安全级别完全不是一个概念。
五、真正容易被忽略的,是第三方依赖
Serverless 还有一个非常现实的问题:
依赖。
我们写 Python:
import requests
import pandas
import numpy
Node.js:
const express = require("express");
const axios = require("axios");
Java:
<dependency>
<groupId>xxx</groupId>
<artifactId>xxx</artifactId>
</dependency>
开发的时候非常爽。
一句:
pip install xxx
或者:
npm install xxx
依赖就进来了。
但问题来了:
你真的知道这个依赖里面有什么吗?
六、依赖风险,有时候比业务代码漏洞更麻烦
假设我们的项目:
order-function
├── app.py
├── requirements.txt
└── third-party packages
requirements:
requests
pandas
numpy
xxx-package
表面上只有几个依赖。
实际上可能是:
你的代码
↓
requests
↓
urllib3
↓
其他依赖
↓
其他依赖
这就是所谓的依赖链。
你真正部署进去的东西,可能远远超过你写的那几百行代码。
所以 Serverless 的供应链安全非常重要。
七、别只扫描自己的代码,也要扫描依赖
例如 Python 项目,可以在 CI/CD 中加入依赖检查:
pip-audit
Node.js:
npm audit
也可以使用 SCA 工具对整个依赖树进行扫描。
一个比较合理的 CI 流程应该是:
开发
↓
代码提交
↓
SAST
↓
依赖扫描
↓
Secrets扫描
↓
镜像/构建产物扫描
↓
安全门禁
↓
部署
而不是:
开发
↓
git push
↓
直接上线
后面再发现:
“卧槽,这个依赖有高危漏洞。”
那就晚了。
八、依赖版本不要写得太随意
例如:
requests
这种写法太宽松。
今天安装一个版本:
2.x
过几天重新构建:
另一个版本
最终:
开发环境 ≠ 测试环境 ≠ 生产环境
更合理的方式是锁定版本。
例如:
requests==2.32.4
或者使用 lock 文件:
package-lock.json
poetry.lock
uv.lock
这样至少可以保证:
今天构建出来的东西,和明天构建出来的东西尽可能一致。
这对于 Serverless 特别重要。
因为 Serverless 经常会:
代码修改
↓
重新构建
↓
重新发布
构建频率越高,依赖漂移的风险就越明显。
九、Secrets千万别直接写进函数代码
这个坑我见过太多次。
比如:
DB_HOST = "10.10.10.20"
DB_USER = "admin"
DB_PASSWORD = "123456"
然后:
conn = connect(
DB_HOST,
DB_USER,
DB_PASSWORD
)
开发环境觉得:
“反正代码在公司内网。”
问题是:
Serverless 的代码包、日志、Git仓库、构建系统、开发人员电脑,任何一个环节泄露,都可能导致凭证泄露。
更合理的是使用 Secret Manager:
db_password = get_secret("prod/order/db/password")
函数只拿到:
运行时需要的 Secret
而不是:
所有环境的密码
这其实又回到了一个核心思想:
权限最小化,不只是 IAM 权限最小化,凭证也应该最小化。
十、环境变量也不是“保险箱”
很多人会说:
DB_PASSWORD = xxxxx
放环境变量里不就行了?
比写死在代码里好,但千万不要把环境变量当成专业的密钥管理系统。
例如:
print(os.environ)
这一下就可能把一堆敏感配置打进日志。
尤其是 Serverless。
函数执行频率可能非常高:
1分钟
↓
1000次调用
↓
日志
↓
日志平台
↓
长期保存
一次错误日志,很可能变成长期安全事故。
所以生产环境应该尽量使用:
Secret Manager
KMS
密钥轮换
短期凭证
而不是:
代码里写密码
或者:
日志里打印密码
十一、Serverless 最容易犯的另一个错误:函数“万能化”
有些团队喜欢设计一个超级函数:
CommonFunction
然后:
查询订单
创建订单
删除订单
操作库存
发送消息
修改用户
导出数据
全部塞进去。
看起来:
“复用率很高。”
实际上安全边界已经糊了。
一个函数承担的业务越多,它需要的权限就越多。
最后可能变成:
Function
├── DB Read
├── DB Write
├── OSS Read
├── OSS Write
├── MQ Send
├── MQ Consume
├── Redis
└── Admin API
这时候只要函数出现一个漏洞:
漏洞
↓
万能函数
↓
万能权限
↓
整个系统遭殃
所以我更推荐:
函数小一点,职责单一点,权限窄一点。
例如:
OrderQueryFunction
↓
DB Read
OrderCreateFunction
↓
DB Write
ImageProcessFunction
↓
OSS Read/Write
NotificationFunction
↓
MQ Send
这样即使某个函数被打穿:
ImageProcessFunction
↓
只能访问图片
而不是:
ImageProcessFunction
↓
整个生产系统
十二、真正靠谱的 Serverless 安全模型
如果让我设计一套企业级 Serverless 安全体系,我不会只盯着函数代码。
我会把它拆成五层。
┌──────────────────────────┐
│ API / Event │
│ 身份认证 + 限流 + WAF │
├──────────────────────────┤
│ Function │
│ 隔离 + 输入校验 + 防注入 │
├──────────────────────────┤
│ Dependency │
│ SCA + Lock + 漏洞扫描 │
├──────────────────────────┤
│ IAM / Secret │
│ 最小权限 + 密钥管理 │
├──────────────────────────┤
│ Data / Infra │
│ 加密 + 审计 + 监控 │
└──────────────────────────┘
然后再配合:
代码扫描
+
依赖扫描
+
Secret扫描
+
权限审计
+
运行时监控
+
日志审计
形成完整闭环。
十三、最后再聊一个容易被忽略的问题:安全监控
Serverless 有个天然特点:
函数生命周期短。
传统服务器:
服务器
└── 跑30天
Serverless:
实例启动
↓
执行
↓
结束
所以传统那种:
登录服务器
top
ps
netstat
在 Serverless 世界里就不太适用了。
你需要更多依赖:
调用日志
审计日志
Tracing
Metrics
Security Event
比如突然发现:
正常:
API调用 1000次/分钟
异常:
API调用 80000次/分钟
或者:
正常:
Function → OSS:GetObject
异常:
Function → OSS:DeleteObject
Function → IAM
Function → UserAdmin
这时候就应该触发安全告警。
所以:
Serverless 不是不需要运维,而是从“盯服务器”变成了“盯行为”。
十四、写在最后:Serverless真正危险的不是函数,而是“默认信任”
Serverless 很容易让我们产生一种错觉:
“云厂商都帮我隔离好了,安全应该不用太操心。”
这是非常危险的想法。
云厂商负责的是:
基础设施安全
运行环境安全
平台安全
但你的:
代码
依赖
权限
密钥
业务逻辑
数据
依然需要自己负责。
我自己比较认同一句话:
Serverless 不是把安全问题消灭了,而是把安全边界从“服务器”移动到了“函数”。
所以真正成熟的 Serverless 安全,不是把所有权限都关掉,也不是给函数套一堆安全产品。
而是做好三件最朴素的事情:
第一,函数要隔离
一个函数干一件事。
查询归查询
写入归写入
图片处理归图片处理
不要搞一个“超级函数”。
第二,依赖要管住
固定版本
+
依赖扫描
+
漏洞修复
+
构建可追溯
别觉得:
“这是开源库,应该没问题。”
第三,权限要收紧
函数需要:
OSS Read
就给:
OSS Read
不要顺手给:
OSS *
更不要:
*
*
因为安全这件事情,最怕的不是没有防护。
最怕的是“为了方便,什么都允许”。
Serverless 可以让我们少维护很多服务器,但它绝对不会让我们少思考安全。
恰恰相反——
服务器越少,函数越多;基础设施越“无感”,权限、依赖和代码安全就越应该被我们认真对待。
这才是 Serverless 真正成熟之后,运维人员应该面对的问题。
我是 Echo_Wish,我们下篇继续聊 Serverless。