公开的无服务器服务并不天然不安全,但它把自研代码、第三方依赖、运行时身份和云 API 放在了同一条攻击链上。一次目录遍历或命令注入,可能不只暴露容器内的文件,还会让攻击者借助服务账号权限访问 Secret Manager、Cloud Storage,甚至控制整个 Google Cloud 项目。
真正有效的防护不能只依赖修复某一行代码。团队需要同时约束应用输入、运行时身份、网络入口和横向访问路径。
攻击链为什么会越过容器边界
Cloud Run 和函数服务会自动管理底层基础设施,但部署进去的应用仍然要承担传统 Web 应用面对的风险,包括本地或远程文件包含、目录遍历、命令注入、易受攻击的第三方包以及业务逻辑缺陷。
以文件读取接口为例,如果程序直接执行 open(user_input),攻击者就可能读取:
- 应用源码以及
requirements.txt、package.json、go.mod等依赖清单; - 环境变量、配置文件和日志中的令牌或个人数据;
/proc/self/environ等运行时信息;- 暴露内部服务地址、认证方式和后续注入点的业务代码。
命令注入的影响更大。若应用把用户输入交给 subprocess.run(..., shell=True),攻击者获得的通常是容器内远程代码执行能力。随后,他们可能尝试访问云平台元数据服务,取得当前服务账号的短期访问令牌。
短期令牌并不等于低风险。它在有效期内继承服务账号的 IAM 权限。如果 Cloud Run 使用默认 Compute Engine 服务账号,而该账号还保留宽泛的 Editor 权限,攻击者就可能读取密钥、修改资源、部署持久化服务或外泄存储数据。容器只是入口,IAM 权限才决定最终爆炸半径。
先在代码里切断文件读取与命令执行
WAF 可以拦截常见攻击特征,但不能替代输入校验。对于确实需要读取服务端文件的接口,可以这样实践:将访问范围固定在专用目录中,使用业务文件标识而不是任意路径,并在打开文件前验证解析后的真实路径。
下面的示例可直接作为 Functions Framework HTTP 函数运行。它只允许读取 data 目录中的两个公开文件,不接受调用者提供的文件路径。
from pathlib import Path
import functions_framework
from flask import jsonify
DATA_DIR = (Path(__file__).parent / "data").resolve()
ALLOWED_FILES = {
"terms": "terms.txt",
"status": "status.json",
}
@functions_framework.http
def read_public_file(request):
payload = request.get_json(silent=True) or {}
file_id = payload.get("file_id") or request.args.get("file_id")
filename = ALLOWED_FILES.get(file_id)
if filename is None:
return jsonify(error="unknown file_id"), 400
target = (DATA_DIR / filename).resolve()
if DATA_DIR not in target.parents:
return jsonify(error="invalid path"), 400
try:
return target.read_text(encoding="utf-8"), 200
except FileNotFoundError:
return jsonify(error="file not found"), 404
运行前创建如下文件:
.
├── main.py
├── requirements.txt
└── data
├── status.json
└── terms.txt
requirements.txt 内容如下:
functions-framework==3.*
本地启动并测试:
python -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
functions-framework --target=read_public_file --port=8080
curl -i -X POST http://127.0.0.1:8080 \
-H 'Content-Type: application/json' \
-d '{"file_id":"status"}'
如果业务需要调用系统工具,不要把完整命令交给用户,也不要启用 shell=True。应固定可执行文件和参数结构,通过映射表把外部输入转换为有限的内部值。例如图片转换服务可以只接受预定义尺寸,再以参数数组调用工具。若 Python 标准库或 SDK 能完成任务,则应直接使用库 API,进一步移除 shell 攻击面。
密钥同样不能写进源码、镜像层、.env 文件或普通环境变量配置中。应将其放入 Secret Manager,并只授权运行时服务账号读取实际需要的单个密钥。
用独立身份限制失陷后的权限
公开服务不应使用默认 Compute Engine 服务账号。可以这样实践:为每个服务或同类工作负载创建专用服务账号,并把权限限制到具体资源。
下面的命令假设服务只需读取一个 Cloud Storage 存储桶。执行前替换 PROJECT_ID、REGION、BUCKET 和镜像地址:
export PROJECT_ID="my-project"
export REGION="europe-west3"
export BUCKET="my-public-data"
export SERVICE_ACCOUNT="public-reader@${PROJECT_ID}.iam.gserviceaccount.com"
gcloud iam service-accounts create public-reader \
--project="${PROJECT_ID}" \
--display-name="Cloud Run public reader"
gcloud storage buckets add-iam-policy-binding "gs://${BUCKET}" \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role="roles/storage.objectViewer"
gcloud run deploy public-reader \
--project="${PROJECT_ID}" \
--region="${REGION}" \
--image="${REGION}-docker.pkg.dev/${PROJECT_ID}/apps/public-reader:latest" \
--service-account="${SERVICE_ACCOUNT}" \
--ingress="internal-and-cloud-load-balancing" \
--no-allow-unauthenticated
这里有两个关键约束:
roles/storage.objectViewer绑定在指定存储桶,而不是整个项目;- Cloud Run 只接受内部流量和来自 Cloud Load Balancing 的流量,公网入口交给外部七层负载均衡器管理。
如果服务需要读取密钥,应把 roles/secretmanager.secretAccessor 授予具体 Secret,而不是项目级授权。还要检查项目中默认服务账号是否残留 Editor、Owner 或其他历史宽权限。
把公网暴露集中到负载均衡与 WAF
必须公开访问的 Cloud Run 服务,可以放在外部 Layer 7 Application Load Balancer 后面,并限制服务自身的 ingress。这样可以集中管理 TLS、请求头、日志、速率限制和身份策略,同时接入 Cloud Armor。
Cloud Armor 的预配置规则可用于拦截常见 LFI 和 RCE 特征。例如在安全策略中分别加入以下表达式:
evaluatePreconfiguredWaf('lfi-v33-stable', {'sensitivity': 3})
evaluatePreconfiguredWaf('rce-v33-stable', {'sensitivity': 3})
可以这样通过 CLI 创建规则。执行前确认 public-serverless-policy 已经绑定到负载均衡器使用的后端服务:
gcloud compute security-policies rules create 1000 \
--security-policy="public-serverless-policy" \
--expression="evaluatePreconfiguredWaf('lfi-v33-stable', {'sensitivity': 3})" \
--action=deny-403 \
--description="Block common local file inclusion attempts"
gcloud compute security-policies rules create 1010 \
--security-policy="public-serverless-policy" \
--expression="evaluatePreconfiguredWaf('rce-v33-stable', {'sensitivity': 3})" \
--action=deny-403 \
--description="Block common remote code execution attempts"
灵敏度提高后可能出现误报,应先在预发布环境验证正常请求,观察 Cloud Armor 日志,再逐步启用阻断。WAF 规则匹配的是已知模式,经过编码、拆分或业务协议封装的恶意输入仍可能绕过,因此不能把 WAF 当作代码修复方案。
面向内部员工或合作方的服务还可以在负载均衡层接入 Identity-Aware Proxy。真正无需身份的公共 API,则应配置配额、请求大小限制、超时和速率限制,避免漏洞之外的资源消耗攻击。
从单个服务扩展到云架构
当 Cloud Run 通过 Direct VPC egress 或 VPC Access Connector 访问内部资源时,远程代码执行也可能变成横向移动入口。公共工作负载应尽量部署到隔离的服务项目中,并使用 VPC Service Controls、细粒度访问策略和出口控制限制数据外泄。
上线前可以按以下清单审查:
- 公共 Cloud Run 是否与关键生产资源处于隔离项目;
- 是否使用专用服务账号,并移除默认账号的宽权限;
- IAM 是否绑定到具体 Bucket、Secret 或其他资源;
- Cloud Run ingress 是否限制为负载均衡入口;
- 是否启用 Cloud Armor、速率限制、集中日志与告警;
- CI/CD 是否执行依赖扫描、静态分析、密钥扫描和持续安全测试;
- AI 生成代码是否经过人工审查,并在隔离环境中测试;
- VPC 出口和服务边界是否能阻断横向移动与数据外泄;
- 是否通过 Security Command Center 等能力监测凭据访问、侦察、脚本执行和反向 shell 行为。
公开无服务器服务的安全目标不是保证应用永远没有漏洞,而是让单个漏洞难以继续升级。输入校验负责阻止初始利用,最小权限 IAM 限制攻击者能做什么,负载均衡与 WAF 提供外围过滤和可见性,项目隔离与服务边界则负责控制最终影响范围。