近黑色卡片上的 Granted 標誌,兩支共用鑰匙環的綠色線條鑰匙圖案,旁邊是白色的 Granted 字樣

用 Granted 把 Windows 上的 AWS 明文金鑰換成 SSO — 小心兩套容易搞混的 AWS 登入

Granted 與 AWS IAM Identity Center 在 Windows 上把靜態 IAM 金鑰換成短效的 SSO 憑證。傳統的 AWS 登入表單完全不接受這組憑證。

更新

本頁目錄

引言

在檢查 AWS 設定檔裡一個與正事無關的區域打字錯誤時,代理程式執行了一個把該檔案印到終端機的指令,結果連緊接在下面的存取金鑰 ID 與密鑰一併以明文印了出來,印進一個原本沒有被當作機密處理的工作階段裡。那把金鑰自 2025 年 2 月起就是這個帳戶唯一的憑證:一把靜態的 IAM 使用者密鑰,放在 ~/.aws/credentials,沒有到期時間,也沒有輪替。我當場決定不再使用它,改用 Granted 這個把 AWS IAM Identity Center 短效 SSO 憑證包裝起來的 CLI,取代一把永久的金鑰。這次遷移最終成功了,但過程中經過了一個看起來像是正確、實際上並不是的登入頁面。本文說明如何在 Windows 上建置 Granted 與 IAM Identity Center,以及真正耗費時間的那個地方:一個絕對不會接受手上這組登入資訊的登入表單。

金鑰以明文印了出來

這個帳戶只有一個 AWS CLI 設定檔 default,背後是一把靜態存取金鑰,屬於一個當初為了 Next.js 部署管線而建立的 IAM 使用者。為了修正一個與正事無關的 region = ap-northeast-1c 打字錯誤(這其實是一個可用區域,不是區域)而讀取 ~/.aws/config~/.aws/credentials,結果把兩個檔案都印到了終端機上。~/.aws/credentials 裡完整存放著存取金鑰 ID 與密鑰。

**一把沒有到期時間的長效金鑰,只要在不該出現的地方出現過一次,就成了常設的風險。**聊天記錄正是這樣的地方。這裡正確的做法不是把金鑰輪替一次然後繼續用下去,而是徹底不在這台機器上留下任何靜態金鑰。

在 Windows 上安裝 Granted

Granted 把 AWS IAM Identity Center 的 SSO 流程包裝在兩個指令背後:用來切換角色的 assume,以及用於設定與帳戶管理的 granted。這樣一來,終端機工作階段持有的就是短效憑證,而不是永久的密鑰。Windows 並不是 Granted 自家文件主要針對的平台,Windows 版安裝過程中有三件事,光看 README 看不出來。

發布的資產是依架構區分的,選錯了不會出現清楚的錯誤,只會安靜地失敗。先確認一下,就不用用猜的:

終端機視窗
echo $env:PROCESSOR_ARCHITECTURE

結果是 AMD64,代表要用 windows_x86_64 版本,而不是 arm64。解壓縮出來的檔案裡有 granted.exeassumego.exe,以及對應每種 shell 的 assume.ps1assume.batassume。把這些全部複製到一個固定的資料夾,再把那個資料夾加進使用者的 PATH,這部分算是簡單的。

assume 不能只是以執行檔的身分放在 PATH 上,因為 assume 一個角色,意味著要改動呼叫它的那個 shell 的環境變數,而子行程永遠沒辦法替父層做到這件事。單純的 .exe 是在自己的行程裡執行後就結束,它設定的任何東西都會隨著行程一起消失。assume.ps1 必須改成在目前的 shell裡面執行,也就是要把它用點號來源執行載入到 PowerShell 設定檔裡,讓每個新的工作階段都自動載入它:

終端機視窗
. "$env:USERPROFILE\granted\assume.ps1"

寫在 Microsoft.PowerShell_profile.ps1 裡。如果跳過這一步,只是直接從 PATH 執行 assume,指令碼確實會執行,但它匯出的任何憑證都會在子行程結束的瞬間消失,父層 shell 仍然和之前一樣完全未驗證。

為什麼 assume 必須用點號來源執行,而不能直接執行左右並排兩張卡片。左側卡片顯示 assume.exe 以一般指令方式執行的情況:它以巢狀在父層 PowerShell 工作階段內的獨立子行程執行,SSO 環境變數設定在該子行程之中,子行程結束的瞬間這些變數就被捨棄,父層 shell 仍然維持未驗證狀態。右側卡片顯示在開頭加上點號載入 assume.ps1 的情況:畫面中沒有巢狀的子行程方框,因為點號來源執行會讓指令碼的指令直接在父層 shell 內執行,因此它設定的環境變數在指令碼結束後仍然保留,父層 shell 最終擁有有效的認證資訊。同一個指令,行程邊界卻不同assume.exe(直接執行)以子行程執行PowerShell 工作階段assume.exe(子行程)設定 SSO 環境變數結束時捨棄父層 shell仍未驗證. assume.ps1(點號來源執行)在目前的 shell 內執行PowerShell 工作階段設定 SSO 環境變數指令碼結束後仍保留父層 shell認證資訊有效
單純的 .exe 會把憑證設定在一個子行程裡,該子行程結束時這些憑證就被捨棄。點號來源執行則完全去掉了子行程,讓同樣的變數直接落在父層 shell 裡並留在那裡。

在某個 shell 裡改動的 PATH,不會傳到已經在執行中的 shell。 透過

終端機視窗
[Environment]::SetEnvironmentVariable("Path", $newPath, "User")

把 Granted 的資料夾加進使用者的 PATH,寫入的是登錄檔,而只有每一個的行程在啟動時才會讀取它。對於變動發生當下已經活著的行程,這完全不會有任何作用。每一個已經開著的終端機都持續出現

granted.exe : The term 'granted.exe' is not recognized as the name of a cmdlet, function, script file, or operable program.

直到那個 shell 從使用者與電腦兩個範圍重新載入 $env:Path,或是開一個全新的終端機為止才恢復正常。同樣的 PATH 陷阱在這次遷移裡又出現了兩次:一次是同一個工作階段裡稍後重新安裝的 pnpm,另一次是在另一種 shell(Bash)裡,同一個工具在 PATH 上有一條完全不同、而且各自獨立壞掉的項目。

不透過 AWS Organization 建置 IAM Identity Center

Granted 的 SSO 流程需要一個實際存在的 IAM Identity Center 執行個體可以對話,所以在設定任何本機設定檔之前,帳戶必須先啟用 Identity Center。AWS 的文件記載了兩種執行個體類型:需要 AWS Organizations 的組織執行個體,以及不需要、僅限單一獨立帳戶使用的帳戶執行個體。

啟用畫面上預設的路徑會建立一個組織,這件事在點下任何東西之前,就有值得先知道的計費後果。 AWS 自家文件講得很直白:在免費方案帳戶上,為了容納預設的組織執行個體而建立 AWS Organization,會讓帳戶立刻轉為隨用隨付計費,並讓剩下的免費方案額度失效。畫面上確實有提供另一條路徑,也就是帳戶執行個體,但它只是一個次要連結,而不是那個按鈕。在點任何東西之前先讀文件,讓這件事在變成計費上的意外之前就先被發現,而不是事後才發現。

啟用帳戶執行個體之後,出現了第二個看起來像設定錯誤、實際上不是的地方:主控台顯示**「IAM Identity Center is currently configured in the US East (N. Virginia) Region」**,而且是在還沒有刻意在那裡設定任何東西之前就出現了。這則橫幅只有在該帳戶在那個區域已經存在一個執行個體時才會出現,而帳戶執行個體會永久鎖定在建立時所在的區域;之後要換區域,代表要刪掉這個執行個體,再從零重建每一個使用者、權限集與指派。很容易把這個區域鎖定誤讀成也限制了帳戶實際資源可以在哪裡執行。事實並非如此:Identity Center 的區域,決定的只是它自己的身分中繼資料放在哪裡,而不是 EC2 執行個體、S3 儲存桶或帳戶裡任何其他東西可以放在哪裡。從維吉尼亞管理登入系統,同時讓所有真正的資源都留在東京,是很正常的事,也沒有什麼值得一提的延遲成本。

看起來像一個的兩套登入

帳戶執行個體建立好之後,設定過程一如預期地繼續進行:建立一個使用者、一個權限集(AdministratorAccess),以及一個把兩者連到帳戶的指派。失敗的是用它登入這一步,而且失敗得完全沒有給出任何有用的錯誤。

登入 AWS 最直覺想到的地方是 signin.aws.amazon.com,每一篇 AWS 教學和每一個書籤都指向這裡。在這裡輸入帳戶 ID、新建立的 Identity Center 使用者名稱與密碼,每次都被拒絕登入,也完全沒有提示原因。傳統登入表單與 Identity Center 登入,是兩套毫無共通之處的獨立系統:沒有共用使用者目錄,沒有共用密碼庫,甚至連「這是同一個帳戶」這個概念都不共用。傳統表單驗證的是在 IAM → Users 底下建立的 IAM 使用者;這個帳戶唯一的 IAM 使用者,是最初的部署使用者 nextjs-deploy,Identity Center 的使用者從來不會被登錄在那裡。不管密碼多正確,Identity Center 身分的密碼永遠無法滿足那個表單。

互不共用任何資料的兩套 AWS 登入系統左右並排兩張卡片。左側卡片是 signin.aws.amazon.com 的傳統登入表單,含帳戶 ID、IAM 使用者名稱與密碼欄位,只能驗證 IAM 使用者 nextjs-deploy。右側卡片是帳戶專屬 SSO 起始網址上的 AWS 存取入口網站,顯示 AWS 帳戶與 AdministratorAccess 權限集的圓角標籤,以及較不顯眼的存取金鑰欄位,只能驗證 Identity Center 使用者 oharu121-dev。兩者之間的虛線標示「沒有共用登入」,中間沒有連接兩張卡片的線條,因為兩個系統並未共用使用者目錄。兩個各自獨立的系統傳統登入signin.aws.amazon.com帳戶 IDIAM 使用者名稱密碼驗證對象nextjs-deploy僅限 IAM 使用者AWS 存取入口網站https://d-xxxxxxxxxx.awsapps.com/startAWS 帳戶123456789012權限集AdministratorAccess存取金鑰僅限 CLI驗證對象oharu121-dev僅限 Identity Center 使用者沒有共用登入
兩張卡片之間刻意沒有畫一條連線。傳統登入表單與 AWS 存取入口網站驗證的是兩個不同的使用者目錄,所以在其中一邊行得通的東西,在另一邊完全行不通。

真正行得通的門是 AWS 存取入口網站,要透過帳戶自己的 SSO 起始網址(https://d-xxxxxxxxxx.awsapps.com/start,可以在 Identity Center 主控台的 Settings 頁面找到)才能到達。在那裡登入後,會出現一個列出該帳戶與其指派的權限集 AdministratorAccess 的頁面,權限集本身是一個可以點的連結。要點的是權限集的名稱,而不是旁邊那顆小小的鑰匙圖示,鑰匙圖示只會顯示 CLI 用的憑證。點下權限集名稱,開啟的正是和傳統登入流程一樣完整的 AWS Management Console,只是帶著的是一個暫時的聯合工作階段,而不是永久的 IAM 登入。

重複的電子郵件,以及重新建立使用者

第一個 Identity Center 使用者是用一個次要的電子郵件建立的,從來沒有跨過前面說的那個登入問題。在確認問題出在登入頁面而不是憑證本身之後,重設密碼再試了一次,還是無法通過驗證。與其繼續除錯一個從來沒有成功過的登入,不如刪掉使用者重建看起來更簡單,於是接下來就這麼做了,這次用的是實際上每天在用的那個電子郵件。

第一次嘗試並不簡單:

Errors occurred while adding user "oharu121-dev"
The user couldn't be added
Duplicate unique attribute value for emails.value

Identity Center 在整個目錄範圍內強制每個電子郵件只能對應一個使用者,而第一個、仍然壞掉的那個使用者還佔著那個地址。先刪掉它,再用同一個電子郵件重建 oharu121-dev,這次乾淨地通過了。這次改成透過 AWS 存取入口網站,而不是傳統表單登入,第一次嘗試就成功了,印證了先前的判斷:這個帳戶從來不是密碼壞掉,而是一直對著錯誤的系統嘗試登入

把 Granted 接上新的登入

有了一個能用的 Identity Center 使用者之後,產生本機 AWS CLI 設定檔只需要一個指令:

終端機視窗
granted sso populate --sso-region us-east-1 https://d-xxxxxxxxxx.awsapps.com/start

它會開啟瀏覽器進行一次性的裝置授權,並寫入一個使用 Granted 自己的憑證解析器、而不是靜態金鑰的設定檔:

.aws/config
[profile oharu121/AdministratorAccess]
granted_sso_start_url = https://d-xxxxxxxxxx.awsapps.com/start
granted_sso_region = us-east-1
granted_sso_account_id = 123456789012
granted_sso_role_name = AdministratorAccess
credential_process = granted credential-process --profile oharu121/AdministratorAccess

在真正的終端機視窗裡執行 assume oharu121/AdministratorAccess,確認它可以運作:

{
"UserId": "AROAR6BADHEROZFPENFQ5:oharu121-dev",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_AdministratorAccess_.../oharu121-dev"
}

得到的是一個 assumed-role 的 ARN,而不是舊有的 arn:aws:iam::...:user/nextjs-deploy

assume 沒辦法從非互動式的 shell 驅動,就算把確切的設定檔名稱當成參數傳進去也一樣。 不管怎麼樣它都會開一個方向鍵選單來確認選擇,而那個選單在指令碼或工具驅動的工作階段裡,沒有任何主控台輸入可以讀:

? Please select the profile you would like to assume: [Use arrows to move, type to filter]
[✘] Incorrect function.

因為這個原因,這次遷移裡每一次 assume 呼叫都必須在真正有人盯著的終端機裡執行。credential_process 在底層的 SSO token 快取好之後,設計上就是非互動式的,所以單純的 aws 指令不管有沒有人盯著,到哪裡都能用。把 [default] 設定檔指向同一個解析器,就不必每個指令都輸入 --profile 了:

.aws/config
[default]
region = ap-northeast-1
output = json
credential_process = granted credential-process --profile oharu121/AdministratorAccess

每次開新終端機都會重演選單

光是開一個普通的 PowerShell 視窗,還沒輸入任何指令,每次都會直接掉進互動式選單:

? Please select the profile you would like to assume: [Use arrows to move, type to filter]
> oharu121/AdministratorAccess
default
[✘] interrupt

原因就是本文稍早建議寫進去的那一行設定檔內容。 代理程式重新讀了一次 Microsoft.PowerShell_profile.ps1,發現的正是前面已經寫過的那一行:

終端機視窗
. "$env:USERPROFILE\granted\assume.ps1"

assume.ps1 從來沒有把自己的邏輯包進一個函式裡,而是在那行點號來源執行的當下,直接執行 assumego

終端機視窗
$ASSUME_FLAG, $ASSUME_1, ... = `
$(& (Join-Path $PSScriptRoot -ChildPath "assumego") $args) -split '\s+'

assumego --help 記載的用法是 assume [options][Profile]:沒有設定檔參數就會退回互動式選單,而空的 $args 正好就是這個狀態。把指令碼直接點號來源執行進設定檔,代表這一行會在每次 shell 啟動時立刻執行,而且沒有設定檔參數可以傳,所以選單不只在第一次,而是在每一個終端機都會跳出來。

修正的做法保留了點號來源執行(單純的 .exe 依然無法改動父層 shell 的環境變數),但把它延後包進一個函式裡,讓這一行只在真正輸入 assume 的時候才執行:

終端機視窗
function assume { . "$env:USERPROFILE\granted\assume.ps1" @args }

只定義這個函式的設定檔,在啟動時什麼都不會執行。 之後執行 assume oharu121/AdministratorAccess,運作方式和之前完全一樣:@args 展開會在需要的當下才把設定檔名稱一路帶到 assumego,而不是每次 shell 啟動就先跑一次。

驗證,以及管理員權限帶來的收穫

不加 --profile 旗標執行 aws sts get-caller-identity,確認 default 設定檔現在會透過 SSO 解析。接下來要做的是徹底淘汰那把靜態金鑰:在確認 .github/workflows、AWS SDK,以及金鑰 ID 本身可能出現的每個地方,這個部落格儲存庫的 CI 設定與原始碼裡都沒有任何對它的參照之後,對 nextjs-deploy 使用者執行了 aws iam delete-access-key儲存庫裡完全沒有任何地方參照那把金鑰,這表示這個部落格是透過託管服務自己的 git 整合部署的,而不是透過那把金鑰。

這個管理員工作階段,也讓稽核帳戶裡是否還有其他東西在跑成為可能。沒有正在執行的運算資源,也沒有作用中的 Elastic Beanstalk 環境,只剩下兩個小小的孤立 S3 儲存桶(各只有幾 KB,是早就被放棄的專案留下的部署產物),以及兩筆底下什麼都沒部署的空 Elastic Beanstalk「Application」紀錄。

刪除這些儲存桶時,浮現出 AdministratorAccess 唯一一個並非真正管理員權限的地方:

An error occurred (AccessDenied) when calling the DeleteBucket operation:
User: arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_AdministratorAccess_.../oharu121-dev
is not authorized to perform: s3:DeleteBucket on resource: "arn:aws:s3:::elasticbeanstalk-..."
with an explicit deny in a resource-based policy

這個儲存桶是 Elastic Beanstalk 自動建立的,帶著自己的一份資源型政策,裡面有一條 AWS 預設附加的陳述式:

{
"Sid": "eb-58950a8c-feb6-11e2-89e0-0800277d041b",
"Effect": "Deny",
"Principal": { "AWS": "*" },
"Action": "s3:DeleteBucket",
"Resource": "arn:aws:s3:::elasticbeanstalk-..."
}

明確的 Deny 在 AWS 的政策評估中會凌駕於每一條 Allow 之上,包含萬用字元的 AdministratorAccess 權限集也不例外,因為它是寫給任何人看的,而不是針對某個管理員權限也贏不過的特定身分。先刪掉儲存桶自己的資源型政策,再刪除儲存桶,就解決了。AdministratorAccess 的意思是沒有身分型的政策會擋路,但它完全沒有說資源自己的政策不會擋住這個資源自己。

總結

這個帳戶最終歸零了所有靜態 AWS 金鑰:舊 IAM 使用者的存取金鑰已從 IAM 刪除,~/.aws/credentials 已清空,每一個 aws 指令現在預設都透過 Granted 的 credential_process 解析到 IAM Identity Center 的 SSO。走到這一步,經過了安裝任何東西之前的架構確認、一個單純的 PATH 項目無法取代的 PowerShell 設定檔編輯、一個避開了不想要的 AWS Organization 的帳戶執行個體選擇,以及一次和密碼錯誤完全無關的登入失敗。傳統 AWS 登入表單與 Identity Center 存取入口網站,看起來像是通往同一間房子的兩扇門。但它們其實是兩棟不同的房子,而其中只有一棟聽過 Identity Center 使用者這回事。

參考連結

分享這篇文章