1. 概览
您将修改将服务部署到 Cloud Run 的默认步骤以提高安全性,然后了解如何安全地访问已部署的应用。该应用是 Cymbal Eats 应用的“合作伙伴注册服务”,供与 Cymbal Eats 合作的公司处理食品订单。
学习内容
只需对将应用部署到 Cloud Run 的最少默认步骤进行一些小改,您就可以显著提高应用的安全性。您将使用现有的应用和部署说明,更改部署步骤,以提高部署应用的安全性。
然后,您将了解如何授权访问应用并发出已获授权的请求。
本文并未详尽介绍应用部署安全性,而是介绍了您可以对未来的所有应用部署进行哪些更改,以便只需付出很少的努力即可提高其安全性。
2. 设置和要求
自定进度的环境设置
- 登录 Google Cloud 控制台,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个。
- 项目名称是此项目参与者的显示名称。它是 Google API 尚未使用的字符串。您可以随时更新。
- 项目 ID 在所有 Google Cloud 项目中是唯一的,并且是不可变的(一经设置便无法更改)。Cloud 控制台会自动生成一个唯一字符串;通常情况下,您无需关注该字符串。在大多数 Codelab 中,您都需要引用项目 ID(通常用
PROJECT_ID
标识)。如果您不喜欢生成的 ID,可以再随机生成一个 ID。或者,您也可以尝试自己的项目 ID,看看是否可用。完成此步骤后便无法更改该 ID,并且此 ID 在项目期间会一直保留。 - 此外,还有第三个值,即部分 API 使用的项目编号,供您参考。如需详细了解所有这三个值,请参阅文档。
- 接下来,您需要在 Cloud 控制台中启用结算功能,以便使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。若要关闭资源以避免产生超出本教程范围的结算费用,您可以删除自己创建的资源或删除整个项目。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。
激活 Cloud Shell
- 在 Cloud Console 中,点击激活 Cloud Shell。
如果您以前从未启动过 Cloud Shell,将看到一个中间屏幕(在折叠下面),描述它是什么。如果是这种情况,请点击继续(您将永远不会再看到它)。一次性屏幕如下所示:
预配和连接到 Cloud Shell 只需花几分钟时间。
这个虚拟机已加载了您需要的所有开发工具。它提供了一个持久的 5GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证。只需使用一个浏览器或 Google Chromebook 即可完成本 Codelab 中的大部分(甚至全部)工作。
在连接到 Cloud Shell 后,您应该会看到自己已通过身份验证,并且相关项目已设置为您的项目 ID:
- 在 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`
- 在 Cloud Shell 中运行以下命令,以确认 gcloud 命令了解您的项目:
gcloud config list project
命令输出
[core] project = <PROJECT_ID>
如果不是上述结果,您可以使用以下命令进行设置:
gcloud config set project <PROJECT_ID>
命令输出
Updated property [core/project].
环境设置
在本实验中,您将在 Cloud Shell 命令行中运行命令。通常,您可以复制命令并将其原封不动地粘贴到相应位置,但在某些情况下,您需要将占位符值更改为正确的值。
- 将环境变量设置为项目 ID,以便在后续命令中使用:
export PROJECT_ID=$(gcloud config get-value project)
export REGION=us-central1
export SERVICE_NAME=partner-registration-service
- 启用将运行应用的 Cloud Run 服务 API、将提供 NoSQL 数据存储的 Firestore API、将由部署命令使用的 Cloud Build API,以及将在构建时用于存放应用容器的 Artifact Registry:
gcloud services enable \
run.googleapis.com \
firestore.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com
- 在原生模式下初始化 Firestore 数据库。该命令使用 App Engine API,因此必须先启用该 API。
该命令必须为 App Engine 指定一个区域(我们不会使用该区域,但出于历史原因必须创建该区域),并为数据库指定一个区域。我们将为 App Engine 使用 us-central,为数据库使用 nam5。nam5 是美国多区域位置。多区域位置可最大限度地提高数据库的可用性和耐用性。
gcloud services enable appengine.googleapis.com
gcloud app create --region=us-central
gcloud firestore databases create --region=nam5
- 克隆示例应用代码库并导航到目录
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,并将大多数选项保留为默认值。您将修改此步骤,以通过以下两种主要方式提高部署的安全性:
- 不允许未经身份验证的访问。在探索过程中允许这样做很方便,但这项 Web 服务供商业合作伙伴使用,因此应始终对用户进行身份验证。
- 指定应用必须使用专用服务账号,该账号仅具有必要的权限,而不是默认服务账号,后者可能拥有超出需求的 API 和资源访问权限。这被称为最小权限原则,是应用安全的基本概念。
第 6 步至第 11 步 - 发出示例 Web 请求以验证行为是否正确
由于应用部署现在需要身份验证,因此这些请求现在必须包含请求者的身份证明。您将直接从命令行发出请求,而不是修改这些文件。
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
identity-token 是由受信任的身份验证提供方签发的短期加密编码字符串。在这种情况下,需要使用 Google 签发的有效且未过期的身份令牌。
使用您的用户账号发出请求
Google Cloud CLI 工具可以为默认的已验证用户提供令牌。运行以下命令可获取您自己账号的身份令牌,并将其保存在 ID_TOKEN 环境变量中:
export ID_TOKEN=$(gcloud auth print-identity-token)
默认情况下,Google 签发的身份令牌的有效期为 1 小时。运行以下 curl 命令,以发出之前因未获授权而被拒的请求。此命令将包含必要的标头:
curl -i -X GET $SERVICE_URL/partners \
-H "Authorization: Bearer $ID_TOKEN"
命令输出应以 HTTP/2 200
开头,表示请求可接受且正在执行。(如果您等待一小时后再次尝试此请求,请求将会失败,因为令牌将会过期。)响应正文位于输出末尾的空白行后面:
{"status":"success","data":[]}
尚无合作伙伴。
使用目录中的示例 JSON 数据通过以下两个 curl
命令注册合作伙伴:
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 应用。如有需要,可以撤消该权限,这在正式版应用中是可取的做法。现在,您将创建一个没有分配任何权限或角色的新服务账号,并使用该账号尝试访问已部署的应用,而不是执行上述操作。
- 创建一个名为
tester
的服务账号。
gcloud iam service-accounts create tester
- 您将会获得此新账号的身份令牌,获取方式与之前为默认账号获取身份令牌的方式大同小异。不过,这需要您的默认账号拥有模拟服务账号的权限。向您的账号授予此权限。
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
- 现在,运行以下命令,将此新账号的身份令牌保存到 TEST_IDENTITY 环境变量中。如果该命令显示错误消息,请等待一两分钟,然后重试。
export TEST_TOKEN=$( \
gcloud auth print-identity-token \
--impersonate-service-account \
"tester@$PROJECT_ID.iam.gserviceaccount.com" \
)
- 像之前一样发出经过身份验证的 Web 请求,但使用此身份令牌:
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 设置为允许来自用户 Web 浏览器的未经身份验证的请求。如果应用需要进行用户身份验证,则应用必须自行处理,而不是让 Cloud Run 来处理。该应用可以通过与 Cloud Run 之外的 Web 应用相同的方式执行此操作。具体实现方式不在本 Codelab 的讨论范围之内。
您可能已经注意到,到目前为止,示例请求的响应都是 JSON 对象,而不是网页。这是因为此合作伙伴注册服务供程序使用,而 JSON 是它们方便使用的格式。接下来,您将编写并运行一个程序来使用这些数据。
通过 Python 程序发出的经过身份验证的请求
程序可以通过标准 HTTP Web 请求向安全的 Cloud Run 应用发出经过身份验证的请求,但需要添加 Authorization
标头。这些计划面临的唯一新挑战是获取有效且未过期的身份令牌,以放置在该标头中。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:
- 使用 Eventarc 触发 Cloud 工作流
- 从 Cloud Storage 触发事件处理
- 从 Cloud Run 连接到专用 CloudSQL
- 从 Cloud Run 连接到全代管式数据库
- 使用 Identity-Aware Proxy (IAP) 保护无服务器应用
- 使用 Cloud Scheduler 触发 Cloud Run 作业
- 保护 Cloud Run 入站流量安全
- 从 GKE Autopilot 连接到专用 AlloyDB
清理
为避免因本教程中使用的资源导致您的 Google Cloud 账号产生费用,请删除包含这些资源的项目,或者保留项目但删除各个资源。
删除项目
若要避免产生费用,最简单的方法是删除您为本教程创建的项目。