如何在 Google Cloud VM 上使用服务账号凭证安全调用自托管服务

8次阅读

如何在 Google Cloud VM 上使用服务账号凭证安全调用自托管服务

本文详解为何基于服务账号的 id Token 认证无法与 compute engine vm 的自托管服务(如 redis api)正常通信,并指出根本原因在于 google 的服务账号身份验证机制不适用于直接面向公网 ip 的非托管服务,同时提供正确实现方案与关键避坑指南。

google Cloud 的 IDTokenCredentials(即 service_account.IDTokenCredentials.from_service_account_file)并非用于向任意公网 IP + 端口发起“带身份认证的 http 请求”,而是专为 Google 托管服务(如 Cloud Run、Cloud Functions、IAP-protected backends)设计的身份验证机制。其核心逻辑是:生成一个短期 JWT ID Token,以 target_audience 作为该 Token 的 aud(受众)声明,由 Google 的身份验证中间件(如 IAP 或 Cloud Run 的内置 Auth)负责校验该 Token 的签名、时效性及 aud 是否匹配其自身服务标识。

然而,你的 Compute Engine VM 上运行的是完全自托管的容器服务(如 flask/fastapi + redis,它不具备任何内置的 Google ID Token 验证能力。你当前的代码:

target_audience = f"https://{ip}:{port}" creds = service_account.IDTokenCredentials.from_service_account_file(     KEY_FILE, target_audience=target_audience)

会生成一个 aud 为 https://:8000 的 Token,但你的 VM 应用既不会解析 Authorization Header 中的 Bearer Token,也不会校验其签名或 aud 字段——它只是静默忽略该 Token,继续等待有效业务请求体。此时,请求看似“发出去了”,但服务端因未收到预期格式的 payload(或因 Token 校验逻辑缺失导致路由/鉴权层阻塞),迟迟不返回响应,最终触发客户端 ReadTimeout。

正确方案:使用服务账号密钥进行服务端身份校验(而非客户端 Token 注入)

  1. 服务端(VM 容器内)需主动验证请求来源
    在你的应用中(例如 python FastAPI),添加中间件,从 Authorization: Bearer 提取 JWT,并使用 Google 的公钥集(https://www.googleapis.com/oauth2/v3/certs)验证其签名、aud、iss(应为 https://accounts.google.com)和有效期。

  2. 客户端仍可使用 IDTokenCredentials,但 target_audience 必须是服务端可识别的逻辑标识(非 IP)
    实际上,target_audience 应设为一个服务标识符(如 my-vm-service),并在服务端硬编码校验该值,而非动态拼接 IP。因为 IP 可能变化,且 Google 不允许 aud 为裸 IP(违反 OIDC 规范)。

示例服务端验证逻辑(Python + PyJWT):

from google.auth import crypt from google.auth.transport import requests as google_requests import jwt import requests  # 获取 Google 公钥(缓存复用) def get_google_public_keys():     resp = requests.get("https://www.googleapis.com/oauth2/v3/certs")     return resp.json()  def verify_id_token(token: str, expected_audience: str = "my-vm-service") -> dict:     keys = get_google_public_keys()     header = jwt.get_unverified_header(token)     key_id = header.get("kid")     signing_key = None     for key in keys["keys"]:         if key["kid"] == key_id:             signing_key = crypt.RSASigner.from_string(key["x5c"][0])             break     if not signing_key:         raise ValueError("Invalid key ID")      payload = jwt.decode(         token,         signing_key,         algorithms=["RS256"],         audience=expected_audience,         issuer="https://accounts.google.com",     )     return payload  # FastAPI 中间件示例 @app.middleware("http") async def auth_middleware(request: Request, call_next):     auth_header = request.headers.get("Authorization")     if not auth_header or not auth_header.startswith("Bearer "):         return jsONResponse({"error": "Unauthorized"}, status_code=401)      token = auth_header.split(" ")[1]     try:         claims = verify_id_token(token, expected_audience="my-vm-service")         request.state.user_email = claims.get("email")     except Exception as e:         return jsonResponse({"error": "Invalid token"}, status_code=401)      return await call_next(request)

客户端保持类似结构,但修正 target_audience:

# ✅ 正确:audience 是逻辑服务名,非 IP target_audience = "my-vm-service"  # 与服务端校验一致 creds = service_account.IDTokenCredentials.from_service_account_file(     "vm-key.json", target_audience=target_audience)  authed_session = AuthorizedSession(creds) response = authed_session.post(     url=f"http://{ip}:8000/get_attributes",  # 注意:服务端若无 HTTPS,此处用 http     json=uuids,     timeout=10  # 增大超时,避免误报 ReadTimeout )

⚠️ 关键注意事项

  • 不要禁用 ssl 验证(verify=False):这会带来严重安全风险;若服务端使用自签名证书,请配置正确的 CA Bundle。
  • VPC 防火墙规则与服务账号无关:0.0.0.0/0 规则仅控制网络可达性,不提供身份认证能力;服务账号认证必须由应用层显式实现。
  • 避免将服务账号密钥(.json)部署到生产 VM:应改用 VM 关联的服务账号(Compute Engine metadata server),通过 google.auth.default() 自动获取凭据,更安全且免密钥管理。

总结:ReadTimeout 的本质不是网络问题,而是服务端未实现 ID Token 校验逻辑导致请求挂起。真正的解决方案是——将服务账号认证从“客户端单向注入”转变为“服务端主动验证”,并确保 audience 语义清晰、传输协议匹配、超时设置合理。

text=ZqhQzanResources