使用 Istio 授予对 Cloud Run for Anthos 服务的访问权限

本教程演示了如何使用 Istio 授予对您在 Cloud Run for Anthos 上部署的服务的访问权限。

Cloud Run 提供以开发者为中心的体验,方便他们部署和提供应用和功能。Cloud Run 在全托管式环境或 Google Kubernetes Engine (GKE) 集群(称为 Cloud Run for Anthos)中运行服务。

开发者在这两种环境中的体验相同,但底层平台的功能不同。

Cloud Run for Anthos 不使用身份和访问权限管理 (IAM) 授予调用服务的权限。您需要使用 Istio 来实现身份验证和授权。在本教程中,您将使用 Istio 身份验证和授权政策来帮助您保护 Cloud Run for Anthos 示例服务的安全。

身份验证和授权政策指定哪些身份(IAM 服务帐号和用户)可以调用示例服务。

在本教程中,您将为在 GKE 集群外部运行的客户端实现身份验证和授权。

目标

  • 创建已启用 Cloud Run 的 GKE 集群。
  • 将示例服务部署到 Cloud Run for Anthos。
  • 创建 IAM 服务帐号。
  • 创建 Istio 身份验证政策
  • 创建 Istio 授权政策
  • 测试解决方案。

费用

本教程使用 Google Cloud 的以下收费组件:

您可使用价格计算器根据您的预计使用量来估算费用。 Google Cloud 新用户可能有资格申请免费试用

完成本教程后,您可以删除所创建的资源以避免继续计费。如需了解详情,请参阅清理

准备工作

  1. 登录您的 Google Cloud 帐号。如果您是 Google Cloud 新手,请创建一个帐号来评估我们的产品在实际场景中的表现。新客户还可获享 $300 赠金,用于运行、测试和部署工作负载。
  2. 在 Google Cloud Console 的项目选择器页面上,选择或创建一个 Google Cloud 项目。

    转到“项目选择器”

  3. 确保您的 Cloud 项目已启用结算功能。 了解如何确认您的项目是否已启用结算功能

  4. 启用 Google Kubernetes Engine API、Cloud Run API 和 Cloud API。
    启用 API
  5. 在 Cloud Console 中,转到 Cloud Shell。
    转到 Cloud Shell
    在 Cloud Console 底部,系统会打开一个 Cloud Shell 会话并显示命令行提示符。Cloud Shell 是一个已安装 Cloud SDK 的 shell 环境,其中包括 gcloud 命令行工具以及已为当前项目设置的值。该会话可能需要几秒钟来完成初始化。 您可以使用 Cloud Shell 运行本教程中的所有命令。

设置环境

  1. 为您要在本教程中使用的 Compute Engine 区域定义环境变量和 gcloud 工具默认值:

    ZONE=us-central1-f
    gcloud config set compute/zone $ZONE
    

    您可以更改区域

  2. 使用 Cloud Run 插件创建 GKE 集群:

    CLUSTER=cloud-run-gke-invoker-tutorial
    
    gcloud beta container clusters create $CLUSTER \
        --addons HorizontalPodAutoscaling,HttpLoadBalancing,CloudRun \
        --enable-ip-alias \
        --enable-stackdriver-kubernetes \
        --machine-type e2-standard-2 \
        --release-channel regular
    

    本教程需要 GKE 1.15.11-gke.9 及更高版本、1.16.8-gke.7 及更高版本或 1.1.7.4-gke.5 及更高版本。使用 regular 发布版本的新 GKE 集群满足版本限制条件。

部署示例服务

  1. 使用 Cloud Shell 在 GKE 集群中创建一个名为 tutorial 的命名空间:

    kubectl create namespace tutorial
    

    您可以更改命名空间的名称。

  2. 将名为 sample 的服务部署到 tutorial 命名空间中的 Cloud Run for Anthos:

    gcloud run deploy sample \
        --cluster $CLUSTER \
        --cluster-location $ZONE \
        --namespace tutorial \
        --image gcr.io/knative-samples/simple-api \
        --platform gke
    

    此命令会创建一个 Knative Serving 服务对象。

  3. Cloud Run for Anthos 会在 Istio 入站网关的外部 IP 地址上公开服务。请检索该外部 IP 地址并将其存储在名为 EXTERNAL_IP 的环境变量以及名为 external-ip.txt 的文件中:

    export EXTERNAL_IP=$(./get-external-ip.sh | tee external-ip.txt)
    
    echo $EXTERNAL_IP
    
    get_external_ip () {
        external_ip=$(kubectl -n gke-system get svc istio-ingress \
            -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
    }
    get_external_ip
    while [ -z "$external_ip" ]; do
        sleep 2
        get_external_ip
    done
    echo "$external_ip"
  4. 验证示例服务是否以 HTTP/1.1 200 OK 响应:

    curl -siH "Host: sample.tutorial.example.com" $EXTERNAL_IP | head -n1
    

创建服务帐号

  1. 创建一个 IAM 服务帐号,您将在后续步骤中向其授予对示例服务的访问权限。将服务帐号电子邮件地址存储在环境变量中:

    export SERVICE_ACCOUNT_EMAIL=$(gcloud iam service-accounts create \
        cloudrun-gke-invoker-tutorial \
        --display-name "Cloud Run for Anthos authorization tutorial service account" \
        --format "value(email)")
    

    本教程使用服务帐号名称 cloudrun-gke-invoker-tutorial。您可以更改该名称。

配置 Istio 身份验证和授权

  1. 创建 Istio 身份验证政策

    kubectl apply -f authenticationpolicy.yaml
    
    apiVersion: authentication.istio.io/v1alpha1
    kind: Policy
    metadata:
      name: istio-ingress-jwt
      namespace: gke-system
    spec:
      targets:
      - name: istio-ingress
        ports:
        - name: http2
        - name: https
      origins:
      - jwt:
          issuer: https://accounts.google.com
          jwksUri: https://www.googleapis.com/oauth2/v3/certs
      originIsOptional: true # use authorization policy to allow or deny request
      principalBinding: USE_ORIGIN

    此身份验证政策会验证前往 Istio 入站网关 http2https 端口的传入请求的 Authorization 标头中是否有 JSON Web 令牌 (JWT)。为了满足政策要求,JWT 必须是由 Google 签发和签名的 OpenID Connect (OIDC) ID 令牌。如需详细了解 ID 令牌验证,请参阅 Google Identity Platform 文档

    originIsOptional: true 属性意味着身份验证政策接受请求,即使它们不包含满足指定限制条件的 JWT 也是如此。此身份验证政策的目的是将 JWT 的属性用于下一步中创建的授权政策。授权政策决定是接受或拒绝请求。

  2. 创建名为 invoke-tutorial 的 Istio 授权政策

    envsubst < authorizationpolicy-invoker.tmpl.yaml | kubectl apply -f -
    

    对于包含由 Google 签发和签名的 JWT(https://accounts.google.com,该 JWT 授予通过 $SERVICE_ACCOUNT_EMAIL 标识且具有 http://example.com 受众群体 (aud) 声明的服务帐号)的所有请求,此授权政策会提供对主机名与 *.tutorial.example.com 匹配的所有工作负载的访问权限(使用任何 HTTP 方法和网址路径)。

    在您自己的环境中,如果要更改 Cloud Run for Anthos 默认网域,请调整 hosts 字段的值以匹配您的网域。

    apiVersion: security.istio.io/v1beta1
    kind: AuthorizationPolicy
    metadata:
      name: invoke-tutorial
      namespace: gke-system
    spec:
      action: ALLOW
      rules:
      - to:
        - operation:
            hosts:
            - '*.tutorial.example.com'
        when:
        - key: request.auth.claims[aud]
          values:
          - http://example.com
        - key: request.auth.claims[email]
          values:
          - $SERVICE_ACCOUNT_EMAIL
        - key: request.auth.claims[iss]
          values:
          - https://accounts.google.com
      selector:
       matchLabels:
         istio: ingress-gke-system
  3. 身份验证和授权政策可能需要一段时间才能生效。运行以下命令并等待一段时间,直到您在输出中看到 HTTP/1.1 403 Forbidden

    while sleep 2; do
        curl -siH "Host: sample.tutorial.example.com" $EXTERNAL_IP | head -n1
    done
    

    最初,您可能会看到输出交替显示 HTTP/1.1 200 OKHTTP/1.1 403 Forbidden,因为 EnvoyIstio 中的政策更改是最终一致的。

    当您反复看到 HTTP/1.1 403 Forbidden 时,请按 Ctrl+C 停止等待。

访问服务

您创建的身份验证和授权政策需要 Google 签发和签名的 ID 令牌。您可以通过多种不同的方式获取令牌。下面按建议顺序列出了四个选项:

  1. 如果从 Compute Engine 实例、GKE 集群或您有权访问元数据服务器的其他环境中访问受保护的服务,请使用 Compute Engine 元数据服务器
  2. 使用 Google 身份验证客户端库(如果该客户端库可用于您的编程语言)。
  3. 如果您无法使用 Google 身份验证客户端库,请直接使用 IAM Service Account Credentials API
  4. 如果您希望以交互方式(例如在实现期间)测试您的服务,请使用 gcloud 命令行工具

使用 Compute Engine 元数据服务器

  1. 在 Cloud Shell 中,创建 Compute Engine 实例(虚拟机)并关联您之前创建的服务帐号:

    VM=cloudrun-gke-invoker-tutorial-vm
    
    gcloud compute instances create $VM \
        --scopes cloud-platform \
        --service-account $SERVICE_ACCOUNT_EMAIL
    

    本教程使用实例名称 cloudrun-gke-invoker-tutorial-vm。您可以更改该名称。

  2. 将包含 Istio 入站网关的公共 IP 地址的文件复制到虚拟机:

    gcloud compute scp external-ip.txt $VM:~
    
  3. 使用 SSH 连接到虚拟机:

    gcloud compute ssh $VM
    
  4. 在 SSH 会话中,从 Compute Engine 元数据服务器获取 ID 令牌:

    ID_TOKEN=$(curl -s -H Metadata-Flavor:Google \
        --data-urlencode format=full \
        --data-urlencode audience=http://example.com \
        http://metadata/computeMetadata/v1/instance/service-accounts/default/identity)
    
  5. 向示例 Cloud Run for Anthos 服务发送包含 ID 令牌的请求:

    curl -s -w"\n" -H "Host: sample.tutorial.example.com" \
        -H "Authorization: Bearer $ID_TOKEN" $(cat external-ip.txt)
    

    输出为:

    OK
    
  6. 退出 SSH 会话:

    exit
    

使用客户端库

适用于 PythonJavaGoNode.js 的 Google 身份验证客户端库允许您使用便捷的 API 获取 ID 令牌。如需使用 Python 库和 IDTokenCredentials 类,请按以下步骤操作:

  1. 在 Cloud Shell 中,创建并下载服务帐号凭据:

    gcloud iam service-accounts keys create service-account.json \
        --iam-account $SERVICE_ACCOUNT_EMAIL
    
  2. 创建 Python 虚拟环境:

    virtualenv -p python3 .venv
    
  3. 为此终端会话激活 Python 虚拟环境:

    source .venv/bin/activate
    
  4. 安装依赖项:

    pip install -r requirements.txt
    
  5. 获取 ID 令牌并向示例 Cloud Run for Anthos 服务发送请求:

    python id_token_request.py http://$EXTERNAL_IP http://example.com \
        Host:sample.tutorial.example.com
    

    输出如下:

    OK
    
    import os
    import sys
    from google.auth.transport.requests import AuthorizedSession
    from google.oauth2 import service_account
    
    def request(method, url, target_audience=None, service_account_file=None,
                data=None, headers=None, **kwargs):
        """Obtains a Google-issued ID token and uses it to make a HTTP request.
    
        Args:
          method (str): The HTTP request method to use
                ('GET', 'OPTIONS', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE')
          url: The URL where the HTTP request will be sent.
          target_audience (str): Optional, the value to use in the audience
                ('aud') claim of the ID token. Defaults to the value of 'url'
                if not provided.
          service_account_file (str): Optional, the full path to the service
                account credentials JSON file. Defaults to
                '<working directory>/service-account.json'.
          data: Optional dictionary, list of tuples, bytes, or file-like object
                to send in the body of the request.
          headers (dict): Optional dictionary of HTTP headers to send with the
                request.
          **kwargs: Any of the parameters defined for the request function:
                https://github.com/requests/requests/blob/master/requests/api.py
                If no timeout is provided, it is set to 90 seconds.
    
        Returns:
          The page body, or raises an exception if the HTTP request failed.
        """
        # Set target_audience, if missing
        if not target_audience:
            target_audience = url
    
        # Set service_account_file, if missing
        if not service_account_file:
            service_account_file = os.path.join(os.getcwd(),
                                                'service-account.json')
    
        # Set the default timeout, if missing
        if 'timeout' not in kwargs:
            kwargs['timeout'] = 90  # seconds
    
        # Obtain ID token credentials for the specified audience
        creds = service_account.IDTokenCredentials.from_service_account_file(
            service_account_file, target_audience=target_audience)
    
        # Create a session for sending requests with the ID token credentials
        session = AuthorizedSession(creds)
    
        # Send a HTTP request to the provided URL using the Google-issued ID token
        resp = session.request(method, url, data=data, headers=headers, **kwargs)
        if resp.status_code == 403:
            raise Exception('Service account {} does not have permission to '
                            'access the application.'.format(
                                creds.service_account_email))
        elif resp.status_code != 200:
            raise Exception(
                'Bad response from application: {!r} / {!r} / {!r}'.format(
                    resp.status_code, resp.headers, resp.text))
        else:
            return resp.text

Identity-Aware Proxy 文档提供了示例代码,用于演示如何使用其他编程语言获取 Google 签发的 ID 令牌。

使用 IAM Service Account Credentials API

如果没有 Google 身份验证客户端库可用于您的编程语言,从而无法利用 Google 身份验证客户端库来创建 ID 令牌,您可以直接使用 IAM Service Account Credentials API

  1. 创建自定义 IAM 角色,该角色有权为服务帐号创建 ID 令牌:

    gcloud iam roles create serviceAccountIdTokenCreator \
        --project $GOOGLE_CLOUD_PROJECT \
        --description "Impersonate service accounts to create OpenID Connect ID tokens" \
        --permissions "iam.serviceAccounts.getOpenIdToken" \
        --stage GA \
        --title "Service Account ID Token Creator"
    

    您可以使用预定义的 Service Account Token Creator 角色 (iam.serviceAccountTokenCreator),而不是创建自定义角色,但此角色可以提供额外的权限,这些权限在您使用 Service Account Credentials API 创建 ID 令牌时不需要用到。

  2. 创建政策绑定,以向您自己授予之前创建的服务帐号的自定义角色:

    gcloud iam service-accounts add-iam-policy-binding $SERVICE_ACCOUNT_EMAIL \
        --member user:$(gcloud config get-value account) \
        --role projects/$GOOGLE_CLOUD_PROJECT/roles/serviceAccountIdTokenCreator
    

    政策绑定可能需要一段时间才能生效。以下命令每五秒钟检查一次政策绑定是否有效。如果政策绑定有效,则该命令将停止检查,并让您返回到终端提示符:

    status_code=""
    while [ "$status_code" != "200" ] ; do
        sleep 5
        echo "Checking permissions"
        status_code=$(curl -s -w "%{http_code}" -o /dev/null -X POST \
            -H "Authorization: Bearer $(gcloud auth print-access-token)" \
            --data-urlencode audience=http://example.com \
            --data-urlencode includeEmail=true \
            "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SERVICE_ACCOUNT_EMAIL}:generateIdToken")
    done
    
  3. 使用 IAM Service Account Credentials API 的 generateIdToken 方法为服务帐号生成 ID 令牌:

    ID_TOKEN=$(curl -s -X POST \
        -H "Authorization: Bearer $(gcloud auth print-access-token)" \
        --data-urlencode audience=http://example.com \
        --data-urlencode includeEmail=true \
        "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SERVICE_ACCOUNT_EMAIL}:generateIdToken" | jq -r '.token')
    
  4. 向示例 Cloud Run for Anthos 服务发送包含 ID 令牌的请求:

    curl -s -w"\n" -H "Host: sample.tutorial.example.com" \
        -H "Authorization: Bearer $ID_TOKEN" $EXTERNAL_IP
    

    输出为:

    OK
    

使用 gcloud 命令行工具

如需获取 Google 签发的 ID 令牌,您可以使用 gcloud 命令行工具。这些令牌用于标识您自己或服务帐号,这对于以交互方式测试服务很有用。

  1. 向您自己授予之前创建的服务帐号的预定义 Service Account Token Creator 角色

    gcloud iam service-accounts add-iam-policy-binding $SERVICE_ACCOUNT_EMAIL \
        --member user:$(gcloud config get-value account) \
        --role roles/iam.serviceAccountTokenCreator
    

    政策绑定可能需要一段时间才能生效。以下命令每五秒钟检查一次政策绑定是否有效。如果政策绑定有效,则该命令将停止检查,并让您返回到终端提示符:

    status_code=""
    while [ "$status_code" != "200" ] ; do
        sleep 3
        echo "Checking permissions"
        status_code=$(curl -s -w "%{http_code}" -o /dev/null -X POST \
            -H "Authorization: Bearer $(gcloud auth print-access-token)" \
            -H "Content-Type: application/json" \
            -d '{"payload": "{}"}' \
            "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SERVICE_ACCOUNT_EMAIL}:signJwt")
    done
    
  2. 模拟服务帐号并获取 ID 令牌:

    ID_TOKEN=$(gcloud auth print-identity-token \
        --audiences http://example.com \
        --impersonate-service-account $SERVICE_ACCOUNT_EMAIL \
        --include-email)
    
  3. 向示例 Cloud Run for Anthos 服务发送包含 ID 令牌的请求:

    curl -s -w"\n" -H "Host: sample.tutorial.example.com" \
        -H "Authorization: Bearer $ID_TOKEN" $EXTERNAL_IP
    

    输出为:

    OK
    

清理

为避免因本教程中使用的资源导致您的 Google Cloud 帐号产生费用,请删除包含这些资源的项目,或者保留项目但删除各个资源。

删除项目

  1. 在 Cloud Console 中,转到管理资源页面。

    转到“管理资源”

  2. 在项目列表中,选择要删除的项目,然后点击删除
  3. 在对话框中输入项目 ID,然后点击关闭以删除项目。

删除资源

如果您希望保留在本教程中使用的 Google Cloud 项目,请删除单个资源:

  1. 删除 GKE 集群:

    gcloud container clusters delete $CLUSTER --async --quiet
    
  2. 删除服务帐号:

    gcloud iam service-accounts delete $SERVICE_ACCOUNT_EMAIL --quiet
    
  3. 删除服务帐号凭据文件:

    rm -f service-account.json
    
  4. 删除 Compute Engine 实例:

    gcloud compute instances delete $VM --quiet
    

后续步骤