安全地部署到 Cloud Run

1. 概览

您将修改将服务部署到 Cloud Run 的默认步骤以提高安全性,然后了解如何以安全的方式访问已部署的应用。该应用属于“合作伙伴注册服务”Cymbal Eats 应用的功能,供与 Cymbal Eats 合作的公司用来处理食品订单。

学习内容

只需对将应用部署到 Cloud Run 所需的默认步骤进行一些细微更改,即可显著提高其安全性。您将学习现有应用和部署说明,并更改部署步骤,以提高已部署应用的安全性。

然后,您将了解如何授予对应用的访问权限以及如何发出经过授权的请求。

这并不是详尽地介绍应用部署安全性,而是介绍了您可以对未来的所有应用部署做出哪些更改,从而轻松提升其安全性。

2. 设置和要求

自定进度的环境设置

  1. 登录 Google Cloud 控制台,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 项目名称是此项目参与者的显示名称。它是 Google API 尚未使用的字符串。您可以随时对其进行更新。
  • 项目 ID 在所有 Google Cloud 项目中是唯一的,并且是不可变的(一经设置便无法更改)。Cloud 控制台会自动生成一个唯一字符串;通常您不在乎这是什么在大多数 Codelab 中,您都需要引用项目 ID(它通常标识为 PROJECT_ID)。如果您不喜欢生成的 ID,可以再随机生成一个 ID。或者,您也可以尝试自己的项目 ID,看看是否可用。完成此步骤后便无法更改该 ID,并且该 ID 在项目期间会一直保留。
  • 此外,还有第三个值,即某些 API 使用的项目编号,供您参考。如需详细了解所有这三个值,请参阅文档
  1. 接下来,您需要在 Cloud 控制台中启用结算功能,以便使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。如需关停资源,以免产生超出本教程范围的结算费用,您可以删除自己创建的资源或删除整个项目。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。

激活 Cloud Shell

  1. 在 Cloud Console 中,点击激活 Cloud Shell853e55310c205094

55efc1aaa7a4d3ad.png

如果您以前从未启动过 Cloud Shell,系统会显示一个中间屏幕(非首屏)来介绍 Cloud Shell。如果是这种情况,请点击继续(此后您将不会再看到此通知)。一次性屏幕如下所示:

9c92662c6a846a5c

预配和连接到 Cloud Shell 只需花几分钟时间。

9f0e51b578fecce5

这个虚拟机装有您需要的所有开发工具。它提供了一个持久的 5GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证。只需使用一个浏览器或 Google Chromebook 即可完成本 Codelab 中的大部分(甚至全部)工作。

在连接到 Cloud Shell 后,您应该会看到自己已通过身份验证,并且相关项目已设置为您的项目 ID:

  1. 在 Cloud Shell 中运行以下命令以确认您已通过身份验证:
gcloud auth list

命令输出

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. 在 Cloud Shell 中运行以下命令,以确认 gcloud 命令了解您的项目:
gcloud config list project

命令输出

[core]
project = <PROJECT_ID>

如果不是上述结果,您可以使用以下命令进行设置:

gcloud config set project <PROJECT_ID>

命令输出

Updated property [core/project].

环境设置

在本实验中,您将在 Cloud Shell 命令行中运行命令。您通常可以复制命令并按原样粘贴它们,但在某些情况下,您需要将占位值更改为正确的值。

  1. 将环境变量设置为项目 ID,以便在后续命令中使用:
export PROJECT_ID=$(gcloud config get-value project)
export REGION=us-central1
export SERVICE_NAME=partner-registration-service
  1. 启用将运行应用的 Cloud Run Service API、提供 NoSQL 数据存储的 Firestore API、将由部署命令使用的 Cloud Build API,以及在构建时用于保存应用容器的 Artifact Registry:
gcloud services enable \
  run.googleapis.com \
  firestore.googleapis.com \
  cloudbuild.googleapis.com \
  artifactregistry.googleapis.com
  1. 在原生模式下初始化 Firestore 数据库。该命令使用 App Engine API,因此必须先启用它。

该命令必须为 App Engine 指定一个区域(我们不会使用该区域,但出于历史原因必须创建一个),以及一个数据库区域。对于 App Engine,我们将使用 us-central;对于数据库,我们将使用 nam5nam5 是美国多区域位置。多区域位置可以最大限度地提高数据库的可用性和耐用性。

gcloud services enable appengine.googleapis.com

gcloud app create --region=us-central
gcloud firestore databases create --region=nam5
  1. 克隆示例应用代码库并导航到该目录
git clone https://github.com/GoogleCloudPlatform/cymbal-eats.git

cd cymbal-eats/partner-registration-service

3. 查看 README 文件

打开编辑器,查看包含该应用的文件。请查看 README.md,其中介绍了部署此应用所需的步骤。其中一些步骤可能需要考虑隐式或显式安全决策。您将更改其中的几个选项以提高所部署应用的安全性,如下所述:

第 3 步 - 运行 npm install

请务必了解应用中使用的所有第三方软件的出处和完整性。管理软件供应链安全与构建任何软件有关,而不仅仅是部署到 Cloud Run 的应用。本实验侧重于部署,因此不涉及这方面的内容,但您可能需要单独研究该主题。

第 4 步和第 5 步 - 修改并运行 deploy.sh

以下步骤将应用部署到 Cloud Run,大多数选项保留为默认值。您将通过以下两种主要方式修改此步骤,以提高部署的安全性:

  1. 不允许未经身份验证的访问。在探索过程中尝试新内容非常方便,不过,这是商业合作伙伴使用的网络服务,应始终对用户进行身份验证。
  2. 指定应用必须使用仅拥有必要权限定制的专用服务账号,而不是默认服务账号,后者可能拥有比所需更多的 API 和资源访问权限。这称为最小权限原则,也是应用安全性的基本概念。

第 6 步到第 11 步 - 发出示例网络请求以验证正确行为

由于应用部署现在需要身份验证,因此这些请求现在必须包含请求者的身份证明。您将直接从命令行发出请求,而不是更改这些文件。

4. 安全地部署服务

deploy.sh 脚本中根据需要确定了两项更改:不允许未经身份验证的访问和使用具有最小权限的专用服务账号。

您需要先创建一个新的服务账号,然后修改 deploy.sh 脚本以引用该服务账号并禁止未经身份验证的访问,接着运行修改后的脚本来部署服务,然后运行修改后的 deploy.sh 脚本。

创建一个服务账号并向其授予所需的 Firestore/Datastore 访问权限

gcloud iam service-accounts create partner-sa

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:partner-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role=roles/datastore.user

修改deploy.sh

修改 deploy.sh 文件以禁止未经身份验证的访问(–no-allow-unauthenticated),并为已部署的应用指定新的服务账号(–service-account)。请将 GOOGLE_PROJECT_ID 更正为您自己的项目 ID。

您将删除前两行,并更改另外三行,如下所示。

gcloud run deploy $SERVICE_NAME \
  --source . \
  --platform managed \
  --region ${REGION} \
  --no-allow-unauthenticated \
  --project=$PROJECT_ID \
  --service-account=partner-sa@${PROJECT_ID}.iam.gserviceaccount.com

部署该服务

从命令行运行 deploy.sh 脚本:

./deploy.sh

部署完成后,命令输出的最后一行将显示新应用的服务网址。将网址保存在环境变量中:

export SERVICE_URL=<URL from last line of command output>

现在,尝试使用 curl 工具从应用中提取订单:

curl -i -X GET $SERVICE_URL/partners

curl 命令的 -i 标志指示其在输出中包含响应标头。输出的第一行应为:

HTTP/2 403

该应用在部署时提供了禁止未经身份验证的请求的选项。此 curl 命令不包含身份验证信息,因此 Cloud Run 拒绝了该命令。实际部署的应用甚至不会运行或接收来自此请求的任何数据。

5. 发出经过身份验证的请求

系统会通过发出 Web 请求来调用已部署的应用。现在,必须对这类请求进行身份验证,Cloud Run 才允许这类请求。通过添加以下形式的 Authorization 标头对 Web 请求进行身份验证:

Authorization: Bearer identity-token

身份令牌是由受信任的身份验证提供方发放的经过加密签名的短期编码字符串。在这种情况下,您需要 Google 颁发的有效且未过期的身份令牌。

以您的用户账号的身份提出请求

Google Cloud CLI 工具可以为经过身份验证的默认用户提供令牌。运行以下命令,为您自己的账号获取身份令牌,并将其保存在 ID_TOKEN 环境变量中:

export ID_TOKEN=$(gcloud auth print-identity-token)

默认情况下,Google 颁发的身份令牌的有效期为一小时。运行以下 curl 命令,以发出之前因未获授权而被拒绝的请求。此命令将包含必要的标头:

curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $ID_TOKEN"

命令输出应以 HTTP/2 200 开头,表示相应请求可接受并得到采纳。(如果您等待一个小时后再次尝试此请求,则请求将会失败,因为令牌已过期。)响应的正文位于输出末尾的空白行之后:

{"status":"success","data":[]}

还没有任何合作伙伴。

通过两条 curl 命令,在目录中使用示例 JSON 数据注册合作伙伴:

curl -X POST \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d "@example-partner.json" \
  $SERVICE_URL/partner

curl -X POST \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d "@example-partner2.json" \
  $SERVICE_URL/partner

重复前面的 GET 请求,立即查看所有已注册的合作伙伴:

curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $ID_TOKEN"

您应该会看到包含更多内容的 JSON 数据,其中提供了两个注册合作伙伴的相关信息。

以未授权账号的身份发出请求

上一步中发出的经过身份验证的请求成功了,这不仅是因为请求成功通过了身份验证,还因为经过身份验证的用户(您的账号)获得了授权。也就是说,该账号拥有调用该应用的权限。并非所有经过身份验证的账号都有权执行此操作。

上一个请求中使用的默认账号已获得授权,因为该账号创建了包含应用的项目,并且默认授权该账号调用该账号中的任何 Cloud Run 应用。如果需要,可以撤消该权限,这在生产应用中更受欢迎。您不应现在执行此操作,而是会创建一个未分配任何权限或角色的新服务账号,并使用该账号尝试访问已部署的应用。

  1. 创建一个名为 tester 的服务账号。
gcloud iam service-accounts create tester
  1. 您将为此新账号获取身份令牌,方法与您之前为默认账号获取身份令牌大致相同。不过,这需要您的默认账号具有模拟服务账号的权限。向您的账号授予此权限。
export USER_EMAIL=$(gcloud config list account --format "value(core.account)")

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="user:$USER_EMAIL" \
  --role=roles/iam.serviceAccountTokenCreator
  1. 现在运行以下命令,在 TEST_IDENTITY 环境变量中保存此新账号的身份令牌。如果命令显示错误消息,请等待一两分钟,然后重试。
export TEST_TOKEN=$( \
  gcloud auth print-identity-token \
    --impersonate-service-account \
    "tester@$PROJECT_ID.iam.gserviceaccount.com" \
)
  1. 像之前一样发出经过身份验证的网络请求,但使用此身份令牌:
curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $TEST_TOKEN"

命令输出将再次以 HTTP/2 403 开头,因为请求虽然已通过身份验证,却未获得授权。新服务账号无权调用此应用。

向账号授权

用户或服务账号必须具有 Cloud Run 服务的 Cloud Run Invoker 角色才能向其发出请求。使用以下命令向测试人员服务账号授予该角色:

export REGION=us-central1
gcloud run services add-iam-policy-binding ${SERVICE_NAME} \
  --member="serviceAccount:tester@$PROJECT_ID.iam.gserviceaccount.com" \
  --role=roles/run.invoker \
  --region=${REGION}

等待一两分钟,让新角色完成更新,然后重复身份验证的请求。如果新的 TEST_TOKEN 自首次保存后已过一小时或更长时间,则保存该新的 TEST_TOKEN。

curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $TEST_TOKEN"

现在,命令输出以 HTTP/1.1 200 OK 开头,最后一行包含 JSON 响应。Cloud Run 接受了此请求,并由应用处理了此请求。

6. 程序身份验证与对用户进行身份验证

现在,您发出的经过身份验证的请求已使用 curl 命令行工具。您也可以使用其他工具和编程语言。但是,无法使用具有普通网页的网络浏览器发出经过身份验证的 Cloud Run 请求。如果用户点击链接或点击按钮以在网页中提交表单,浏览器不会为经过身份验证的请求添加 Cloud Run 所需的 Authorization 标头。

Cloud Run 的内置身份验证机制适用于程序,而不适用于最终用户。

注意

Cloud Run 可以托管面向用户的 Web 应用,但此类应用必须将 Cloud Run 设置为允许来自用户网络浏览器。如果应用需要用户身份验证,则必须由应用自行处理,而不是要求 Cloud Run 执行此操作。应用可以采用与 Cloud Run 外部的 Web 应用相同的方式执行此操作。具体如何实现不在此 Codelab 的讨论范围之内。

您可能已经注意到,到目前为止,示例请求的响应都是 JSON 对象,而不是网页。这是因为此合作伙伴注册服务是为计划使用的,而 JSON 格式对他们来说非常方便。接下来,您将编写并运行程序以使用和使用这些数据。

来自 Python 程序的经过身份验证的请求

程序可以通过标准 HTTP Web 请求(但包含 Authorization 标头)向安全的 Cloud Run 应用发出经过身份验证的请求。这些计划面临的唯一新挑战是获取一个有效且未过期的身份令牌以放置在标头中。Cloud Run 将使用 Google Cloud Identity and Access Management (IAM) 验证该令牌,因此该令牌必须由 IAM 认可的机构颁发和签名。一些客户端库支持多种语言,程序可以使用各种语言来请求颁发此类令牌。此示例将使用的客户端库是 Python google.auth 库。一般来说,有几个 Python 库可用于发出网络请求:此示例使用常用的 requests 模块。

第一步是安装两个客户端库:

pip install google-auth
pip install requests

用于为默认用户请求身份令牌的 Python 代码如下:

credentials, _ = google.auth.default()
credentials.refresh(google.auth.transport.requests.Request())
identity_token = credentials.id_token

如果您在自己的计算机上使用命令 shell(如 Cloud Shell 或标准终端 shell),则默认用户是已在相应 shell 中进行身份验证的用户。在 Cloud Shell 中,通常是登录到 Google 的用户。在其他情况下,它是使用 gcloud auth login 或其他 gcloud 命令进行身份验证的任何用户。如果用户从未登录,则不存在默认用户,此代码将会失败。

对于向另一个程序发出请求的程序,通常不应使用请求程序的身份,而是使用请求程序的身份。这就是服务账号可以发挥作用的地方了。您使用专用服务账号部署了 Cloud Run 服务,该账号提供了发出 API 请求(例如向 Cloud Firestore)使用的身份。当程序在 Google Cloud 平台上运行时,客户端库将自动使用分配给它的服务账号作为其默认身份,因此相同的程序代码在两种情况下都有效。

使用添加 Authorization 标头发出请求的 Python 代码如下:

auth_header = {"Authorization": "Bearer " + identity_token}
response = requests.get(url, headers=auth_header)

下面这个完整的 Python 程序将向 Cloud Run 服务发出经过身份验证的请求,以检索所有已注册的合作伙伴,然后输出合作伙伴的名称和分配的 ID。复制并运行以下命令,将此代码保存到文件 print_partners.py

cat > ./print_partners.py << EOF
def print_partners():
    import google.auth
    import google.auth.transport.requests
    import requests

    credentials, _ = google.auth.default()
    credentials.refresh(google.auth.transport.requests.Request())
    identity_token = credentials.id_token

    auth_header = {"Authorization": "Bearer " + identity_token}
    response = requests.get("${SERVICE_URL}/partners", headers=auth_header)

    parsed_response = response.json()
    partners = parsed_response["data"]

    for partner in partners:
        print(f"{partner['partnerId']}: {partner['name']}")


print_partners()
EOF

您将使用 shell 命令运行此程序。您需要先以默认用户身份进行身份验证,以便程序可以使用这些凭据。运行下面的 gcloud auth 命令:

gcloud auth application-default login

按照说明完成登录。然后从命令行运行该程序:

python print_partners.py

输出将如下所示:

10102: Zippy food delivery
67292: Foodful

该程序的请求到达了 Cloud Run 服务,因为它使用您的身份进行了身份验证。您是此项目的所有者,因此有权默认运行此项目。更常见的做法是:以服务账号的身份运行此程序。在大多数 Google Cloud 产品(例如 Cloud Run 或 App Engine)上运行时,默认身份是服务账号,而非个人账号。

7. 恭喜!

恭喜,您已完成此 Codelab!

后续步骤:

探索其他 Cymbal Eats Codelab:

清理

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

删除项目

若要避免产生费用,最简单的方法是删除您为本教程创建的项目。