验证实例的身份

应用可以先使用 Google 签署的实例身份令牌来验证虚拟机的身份,然后再将敏感信息发送给虚拟机实例。每个实例都有一个唯一的 JSON 网络令牌 (JWT),其中包含有关该实例的详情以及 Google 的 RS256 签名。您的应用可以根据 Google 的公共 Oauth2 证书验证签名,以确认已与其建立连接的实例的身份。

仅当实例通过实例元数据请求已签署的实例令牌时,Compute Engine 才会生成这些令牌。实例只能访问自己的唯一令牌,而不能访问任何其他实例的令牌。

在以下情况下,您可能需要验证实例的身份:

  • 当您首次启动实例时,您的应用可能需要确保其连接的实例拥有有效的身份,然后才会向该实例传输敏感信息。
  • 当您的政策要求您在 Compute Engine 环境外存储凭据,并且您需要定期将这些凭据发送给您的实例以便临时使用时。您的应用可以在每次需要传输凭据时确认实例的身份。

Google 的实例身份验证方法具有以下优点:

  • 每次实例请求令牌时,Compute Engine 都会创建一个唯一令牌,并且每个令牌会在一个小时内过期。您可以将应用配置为仅接受一次实例的身份令牌,这样可以降低令牌被未经授权的系统重新使用的风险。
  • Google 的私钥和公共 Oauth2 证书每日都会轮替,以减少针对这些签名的潜在攻击面。
  • 带有签名的元数据令牌采用的是 RFC 7519 开放式行业标准和 OpenID Connect 1.0 身份层,因此现有的工具和库将与身份令牌无缝搭配使用。

准备工作

验证实例的身份

在某些情况下,您的应用必须验证在 Compute Engine 上运行的实例的身份,然后才能向该实例传输敏感数据。在一个典型示例中,有一个在 Compute Engine 之外运行并且名为 “Host1” 的系统和一个名为 “VM1” 的 Compute Engine 实例。其中,VM1 可以连接到 Host1 并使用以下过程验证该实例的身份:

  1. VM1 通过您选择的安全连接协议(如 HTTPS)与 Host1 建立安全连接。

  2. VM1 向元数据服务器请求其唯一的身份令牌,并指定令牌的目标对象。在本例中,对于 Host1,受众群体值是 URI。向元数据服务器发出的请求包含目标对象 URI,以便 Host1 稍后可以在令牌验证步骤中检查相应的值。

  3. Google 会生成一个新的唯一实例身份令牌(使用 JWT 格式),并将其提供给 VM1。令牌的载荷包含有关实例的多项详情,并且还包含目标对象 URI。如需查看令牌内容的完整说明,请参阅令牌内容

  4. VM1 通过现有的安全连接将身份令牌发送给 Host1。

  5. Host1 对身份令牌进行解码以获取令牌标头和载荷值。

  6. Host1 检查目标对象值并根据公共 Google 证书验证证书签名,以确认该令牌是否由 Google 签署

  7. 如果令牌有效,Host1 将继续传输并在传输完成后关闭连接。如果随后还需要与 VM1 建立任何连接,Host1 和任何其他系统应请求一个新的令牌。

获取实例身份令牌

当您的虚拟机实例收到提供其身份令牌的请求时,该实例将使用获取实例元数据的常规流程向元数据服务器请求该令牌。例如,您可以使用以下某种方法:

cURL

创建curl请求,并在 audience 参数中添加一个值。或者,您可以添加 format 参数来指定是否要在载荷中包含项目和实例详细信息。如果使用 full 格式,您可以添加 licenses 参数来指定是否要在载荷中包含许可代码。

$ curl -H "Metadata-Flavor: Google" \
'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=[AUDIENCE]&format=[FORMAT]&licenses=[LICENSES]'

其中:

  • [AUDIENCE] 是由实例和验证实例身份的系统商定的唯一 URI。例如,目标对象可能是两个系统之间的连接的网址。
  • [FORMAT] 是可选参数,用于指定项目和实例详情是否包含在载荷中。指定 full 可将此信息包含在载荷中,指定 standard 可在载荷中省略该信息。默认值为 standard。如需全面了解令牌格式,请参阅身份令牌格式
  • [LICENSES] 是一个可选参数,用于指定是否将与此实例关联的映像的许可代码包含在载荷中。指定 TRUE 可添加此信息;指定 FALSE 可在载荷中省略此信息。默认值为 FALSE。除非 formatfull,否则该值无效

元数据服务器通过使用 RS256 算法签署的 JSON 网络令牌响应此请求。该令牌包含 Google 签名和载荷中的其他信息。您可以将此令牌发送给其他系统和应用,以便其验证令牌并确认您的实例的身份。

Python

您可以使用 Python requests 库中的方法,将实例的简单请求提交到元数据服务器。以下示例向服务器请求了实例身份令牌,然后打印了该令牌。请注意,令牌对于发出此请求的实例是唯一的。

import requests

AUDIENCE_URL = 'http://www.example.com'
METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
FORMAT = 'full'
LICENSES = 'TRUE'

# Construct a URL with the audience and format.
url = 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience={}&format={}&licenses={}'
url = url.format(AUDIENCE_URL, FORMAT, LICENSES)

# Request a token from the metadata server.
r = requests.get(url, headers=METADATA_HEADERS)

# Extract the token from the response.
token = r.text

print(token)

元数据服务器通过使用 RS256 算法签署的 JSON 网络令牌响应此请求。该令牌包含 Google 签名和载荷中的其他信息。您可以将此令牌发送给其他系统和应用,以便其验证令牌并确认您的实例的身份。

验证令牌

应用从 Compute Engine 实例收到实例身份令牌后,可以使用以下过程验证令牌。

  1. 从虚拟机实例接收令牌,使用 RS256 JWT 解码器解码令牌,并读取标头内容以获取 kid 值。

  2. 根据公共 Google 证书来检查令牌,以验证其是否由 Google 签署。每个公共证书都有一个 kid 值,对应于令牌标头中的 kid 值。

  3. 如果令牌有效,请将载荷内容与期望值进行比较。如果令牌载荷包含有关实例和项目的详细信息,那么您的应用程序可以检查 instance_idproject_idzone值。这些值是一个全局唯一的元组,用于确认您的应用是否正在与所需的项目中的正确实例进行通信。

您可以使用自己喜欢的工具解码和验证令牌,但常见的方法是使用您选择的语言对应的库。例如,您可以使用 Python 版 Google OAuth 2.0 库中的 verify_token 方法。verify_token 方法可将 kid 值与相应证书匹配、验证签名、检查目标对象声明,以及从令牌返回载荷内容。

# Import libraries for token verification
import google.auth.transport.requests
from google.oauth2 import id_token

# Receive token from VM over an SSL connection
token = …
…

# Verify token signature and store the token payload
request = google.auth.transport.requests.Request()
payload = id_token.verify_token(token, request=request, audience=audience)
…

应用验证令牌及其内容后,可以继续通过安全连接与该实例通信,然后在通信完成后关闭连接。对于后续连接,请向实例请求新令牌并重新验证实例的身份。

令牌内容

实例身份令牌包含以下三个主要部分:

标头中包含 kid,用于标识您在验证签名时必须使用的公共 Oauth2 证书。标头中还包含 alg 值,用于确认使用 RS256 算法生成的签名。

{
  "alg": "RS256",
  "kid": "511a3e85d2452aee960ed557e2666a8c5cedd8ae",
}

载荷

载荷中包含 aud 目标对象声明。如果实例在请求令牌时指定了 format=full,则载荷还会包含有关虚拟机实例及其项目的声明。请求完整格式令牌时,如果指定 licenses=TRUE,则载荷还将包含有关与实例关联的许可的声明。

{
   "iss": "[TOKEN_ISSUER]",
   "iat": [ISSUED_TIME],
   "exp": [EXPIRED_TIME],
   "aud": "[AUDIENCE]",
   "sub": "[SUBJECT]",
   "azp": "[AUTHORIZED_PARTY]",
   "google": {
    "compute_engine": {
      "project_id": "[PROJECT_ID]",
      "project_number": [PROJECT_NUMBER],
      "zone": "[ZONE]",
      "instance_id": "[INSTANCE_ID]",
      "instance_name": "[INSTANCE_NAME]",
      "instance_creation_timestamp": [CREATION_TIMESTAMP],
      "license_id": [
        "[LICENSE_1]",
          ...
        "[LICENSE_N]"
      ]
    }
  }
}

其中:

  • [TOKEN_ISSUER] 是用于标识令牌颁发者的网址。对于 Compute Engine,该值为 https://accounts.google.com
  • [ISSUED_TIME] 是指示令牌颁发时间的 unix 时间戳。每当实例向元数据服务器请求令牌时,此值都会更新。
  • [EXPIRED_TIME] 是指示令牌到期时间的 unix 时间戳。
  • [AUDIENCE] 是由实例和验证实例身份的系统商定的唯一 URI。例如,目标对象可能是两个系统之间的连接的网址。
  • [SUBJECT] 是令牌的主题,它是您用来关联实例的服务帐号的唯一 ID。
  • [AUTHORIZED_PARTY] 是 ID 令牌的颁发对象,它是您用来关联实例的服务帐号的唯一 ID。
  • [PROJECT_ID] 是您在其中创建了实例的项目的 ID。
  • [PROJECT_NUMBER] 是您在其中创建了实例的项目的唯一编号。
  • [ZONE] 是实例所在的地区。
  • [INSTANCE_ID] 是此令牌所属实例的唯一 ID。请注意,这个 ID 是独一无二的,不得重复使用。
  • [INSTANCE_NAME] 是此令牌所属实例的名称。随着时间的推移,这个名称可以被多个实例重复使用,因此请使用 instance_id 值来标识唯一的实例 ID。
  • [CREATION_TIMESTAMP] 是一个 unix 时间戳,指示您创建实例的时间。
  • [LICENSE_1][LICENSE_N] 是与此实例关联的映像的许可代码

您的载荷可能类似如下示例:

{
  "iss": "https://accounts.google.com",
  "iat": 1496953245,
  "exp": 1496956845,
  "aud": "https://www.example.com",
  "sub": "107517467455664443765",
  "azp": "107517467455664443765",
  "google": {
    "compute_engine": {
      "project_id": "my-project",
      "project_number": 739419398126,
      "zone": "us-west1-a",
      "instance_id": "152986662232938449",
      "instance_name": "example",
      "instance_creation_timestamp": 1496952205,
      "license_id": [
        "1000204"
      ]
    }
  }
}

签名

Google 通过对标头和载荷进行 base64url 编码并连接这两个值来生成签名。您可以根据公共 Oauth2 证书检查此值以验证令牌。

后续步骤