安全地部署至 Cloud Run

1. 總覽

您將修改將服務部署至 Cloud Run 的預設步驟,以提升安全性,然後瞭解如何以安全的方式存取已部署的應用程式。這個應用程式是 Cymbal Eats 應用程式的「合作夥伴註冊服務」,由與 Cymbal Eats 合作的餐飲公司用於處理食品訂單。

學習目標

只要對部署應用程式至 Cloud Run 的預設步驟進行一些小變更,就能大幅提升應用程式的安全性。您將使用現有的應用程式和部署操作說明,並變更部署步驟,以提升已部署應用程式的安全性。

接著,您將瞭解如何授權存取應用程式,並提出授權要求。

這並非應用程式部署安全性的完整檢視,而是讓您瞭解如何在日後的所有應用程式部署作業中進行變更,以便輕鬆提升安全性。

2. 設定和需求

自學環境設定

  1. 登入 Google Cloud 控制台,然後建立新專案或重複使用現有專案。如果您還沒有 Gmail 或 Google Workspace 帳戶,請務必建立帳戶

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 「Project name」是這個專案參與者的顯示名稱。這是 Google API 不會使用的字元字串。您隨時可以更新。
  • 專案 ID 在所有 Google Cloud 專案中都是不重複的值,且無法變更 (設定後即無法變更)。Cloud 控制台會自動產生一個專屬字串,您通常不需要特別留意。在大多數程式碼研究室中,您都需要參照專案 ID (通常會標示為 PROJECT_ID)。如果您不喜歡系統產生的 ID,可以隨機產生另一組 ID。或者,您也可以自行嘗試,看看是否可用。這項設定在這個步驟後即無法變更,並會在專案期間維持不變。
  • 僅供參考,有些 API 會使用第三個值,也就是「專案編號」。如要進一步瞭解這三個值,請參閱說明文件
  1. 接下來,您需要在 Cloud 控制台中啟用帳單功能,才能使用 Cloud 資源/API。執行本程式碼研究室時,費用應該不會太高,甚至可能不收費。如要關閉資源,避免產生教學課程以外的費用,您可以刪除已建立的資源,或刪除整個專案。Google Cloud 新使用者可享有 $300 美元的免費試用期

啟用 Cloud Shell

  1. 在 Cloud 控制台中,按一下「啟用 Cloud Shell」 圖示 853e55310c205094.png

55efc1aaa7a4d3ad.png

如果您從未啟動 Cloud Shell,系統會顯示中間畫面 (在折疊下方),說明 Cloud Shell 的用途。如果是這種情況,請按一下「Continue」 (您之後就不會再看到這個畫面)。這裡是一次性畫面的外觀:

9c92662c6a846a5c.png

佈建並連線至 Cloud Shell 的作業只需幾分鐘的時間。

9f0e51b578fecce5.png

這個虛擬機器會載入您需要的所有開發工具。提供永久的 5 GB 主目錄,而且在 Google Cloud 中運作,大幅提升網路效能和驗證功能。您可以使用瀏覽器或 Chromebook 完成本編碼實驗室的大部分工作,甚至是全部工作。

連線至 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 服務 API (用於執行應用程式)、Firestore API (用於提供 NoSQL 資料儲存空間)、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,因此必須先啟用該 API。

指令必須指定 App Engine 的區域 (我們不會使用,但基於歷史原因必須建立),以及資料庫的區域。我們會將 us-central 用於 App Engine,並將 nam5 用於資料庫。nam5 是美國多地區位置。多地區位置可盡可能提高資料庫的可用性和耐用性。

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. 進行已驗證的要求

部署的應用程式會透過發出網路要求來叫用,而這類要求現在必須經過驗證,Cloud Run 才會允許。如要驗證網路要求,請加入表單的 Authorization 標頭:

Authorization: Bearer identity-token

身分權杖是指由信任的驗證服務供應器核發的短字串,經過加密簽署並經過編碼。在這種情況下,您必須提供有效且未過期的 Google 核發身分驗證碼。

以使用者帳戶提出要求

Google Cloud CLI 工具可為預設驗證使用者提供權杖。執行這項指令,取得您帳戶的 ID 權杖,並儲存在 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":[]}

目前尚未有合作夥伴。

使用目錄中的 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 應用程式的權限。這項權限可視需要撤銷,這也是正式版應用程式所需的功能。您現在不必這樣做,而是建立新的服務帳戶,並未指派任何權限或角色,然後使用該帳戶嘗試存取已部署的應用程式。

  1. 建立名為 tester 的服務帳戶。
gcloud iam service-accounts create tester
  1. 您會收到新帳戶的 ID 權杖,這與先前為預設帳戶取得 ID 權杖的方式大致相同。不過,您的預設帳戶必須具備模擬服務帳戶的權限。授予帳戶這項權限。
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. 接著執行下列指令,將這個新帳戶的 ID 權杖儲存至 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 叫用者角色,才能向該服務提出要求。使用以下指令,將該角色授予測試人員服務帳戶:

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 可代管面向使用者的網頁應用程式,但這類應用程式必須將 Cloud Run 設為允許使用者網路瀏覽器發出的未經驗證要求。如果應用程式需要使用者驗證,應用程式必須自行處理,而非要求 Cloud Run 執行。應用程式可以以與 Cloud Run 以外的網頁應用程式相同的方式執行這項操作。如何執行這項操作不在本程式碼研究室的範圍內。

您可能已經注意到,範例要求的回應一直是 JSON 物件,而不是網頁。這是因為這項合作夥伴註冊服務是供程式使用,而 JSON 是程式方便取用的格式。接下來,您將編寫並執行程式,以便取用及使用這項資料。

Python 程式中的已驗證要求

程式可以透過標準 HTTP 網頁要求,向安全的 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

如果您使用的是 Cloud 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

您將使用殼層指令執行這個程式。您必須先以預設使用者身分進行驗證,程式才能使用這些憑證。執行下列 gcloud 驗證指令:

gcloud auth application-default login

按照操作說明完成登入程序。然後透過指令列執行程式:

python print_partners.py

輸出內容如下所示:

10102: Zippy food delivery
67292: Foodful

由於程式要求已透過您的身分進行驗證,且您是這個專案的擁有者,因此預設會授權您執行該程式。這類程式通常會以服務帳戶的身分執行。在大多數 Google Cloud 產品 (例如 Cloud Run 或 App Engine) 上執行時,預設身分會是服務帳戶,而非個人帳戶。

7. 恭喜!

恭喜,您已完成程式碼研究室!

下一步:

探索其他 Cymbal Eats 程式碼研究室:

清除所用資源

如要避免系統向您的 Google Cloud 帳戶收取本教學課程所用資源的費用,請刪除含有相關資源的專案,或者保留專案但刪除個別資源。

刪除專案

如要避免付費,最簡單的方法就是刪除您為了本教學課程所建立的專案。