
作者:Echo_Wish
很多人第一次接触 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=1Serverless 本身并不会自动帮你解决业务代码里的漏洞。
所以我的一个观点是:
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 numpyNode.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 packagesrequirements:
requests
pandas
numpy
xxx-package表面上只有几个依赖。
实际上可能是:
你的代码
↓
requests
↓
urllib3
↓
其他依赖
↓
其他依赖这就是所谓的依赖链。
你真正部署进去的东西,可能远远超过你写的那几百行代码。
所以 Serverless 的供应链安全非常重要。
例如 Python 项目,可以在 CI/CD 中加入依赖检查:
pip-auditNode.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 经常会:
代码修改
↓
重新构建
↓
重新发布构建频率越高,依赖漂移的风险就越明显。
这个坑我见过太多次。
比如:
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
密钥轮换
短期凭证而不是:
代码里写密码或者:
日志里打印密码有些团队喜欢设计一个超级函数:
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 安全体系,我不会只盯着函数代码。
我会把它拆成五层。
┌──────────────────────────┐
│ 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 安全,不是把所有权限都关掉,也不是给函数套一堆安全产品。
而是做好三件最朴素的事情:
一个函数干一件事。
查询归查询
写入归写入
图片处理归图片处理不要搞一个“超级函数”。
固定版本
+
依赖扫描
+
漏洞修复
+
构建可追溯别觉得:
“这是开源库,应该没问题。”
函数需要:
OSS Read就给:
OSS Read不要顺手给:
OSS *更不要:
*
*因为安全这件事情,最怕的不是没有防护。
最怕的是“为了方便,什么都允许”。
Serverless 可以让我们少维护很多服务器,但它绝对不会让我们少思考安全。
恰恰相反——
服务器越少,函数越多;基础设施越“无感”,权限、依赖和代码安全就越应该被我们认真对待。
这才是 Serverless 真正成熟之后,运维人员应该面对的问题。
我是 Echo_Wish,我们下篇继续聊 Serverless。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。