Azure 升級大作戰:告別通行金鑰,擁抱 Managed Identity

這是一篇教大家如何把 Azure Storage 從舊時代的金鑰存取,安全升級到託管身分 Managed Identity 的技術指南。

想像一下,你家大門的鑰匙就被你大大方方放在門口的鞋櫃上,任何走過路過的人只要拿到這把鑰匙就能直接進你家搬東西。這就是以前我們使用存取金鑰 Connection String 連線到 Azure Storage 的驚險狀況!

為了不讓駭客當你家是花園,我們強烈建議改用託管身分 Managed Identity。

不過設定上會和 SQL 連線不太一樣。SQL 只要在連線字串加個 useMsi=true 就能解決,但 Blob Storage 必須搭配 Blob SDK 使用 Entra ID Token,並且在 Azure RBAC 權限控管中,給予你的 Container App 相應的權限。

在目前的測試環境中,我們的後端 App 雖然有了系統分配的身分,但只有拿到拉取映像檔的 AcrPull 權限。至於檔案儲存區,依然把金鑰 AZURE_STORAGE_CONNECTION_STRING 硬塞給後端程式碼。

為了達成最高等級的資安防護,以下是推薦的升級改造方向:

1. 打造安全的基礎架構

  • 私有化儲存區:Blob Container 絕對不能開啟任何人都能看的公開存取存取權限。
  • 後端專屬通道:只有後端 App 可以接觸儲存區,前端完全不拿任何金鑰或 Token。
  • 權限最小化原則:給後端 App 的身分授權 Storage Blob Data Contributor 角色,而且範圍只能限定在 demoproject-attachments 這個容器。這樣它就能讀、寫、刪除檔案。(千萬別偷懶給了一般權限,那樣權限太大了!)

2. 後端 Go 程式碼改版

後端改用 DefaultAzureCredential 搭配 Storage Account 的 Blob 端點。這樣做的好處是,你在本機開發時會自動用你電腦 az login 的身分;一旦部署到 Azure Container Apps 時,就會自動無縫切換成 Managed Identity,完全不用改程式碼!

你的專案已經有 azidentity 套件,不需要另外安裝。請把 config.go 中的連線字串改為帳號名稱 AccountName,並修改 internal/storage/blob.go:

// internal/storage/blob.go
import "github.com/Azure/azure-sdk-for-go/sdk/azidentity"

func NewBlobClient(accountName, containerName string) (*BlobClient, error) {
    if accountName == "" {
        return nil, nil // 如果本地環境停用附件功能就跳過
    }
    if containerName == "" {
        return nil, fmt.Errorf("azure storage container name is required")
    }

    credential, err := azidentity.NewDefaultAzureCredential(nil)
    if err != nil {
        return nil, fmt.Errorf("create azure credential: %w", err)
    }

    serviceURL := fmt.Sprintf("https://%s.blob.core.windows.net/", accountName)
    client, err := azblob.NewClient(serviceURL, credential, nil)
    if err != nil {
        return nil, fmt.Errorf("create blob client: %w", err)
    }

    return &BlobClient{client: client, container: containerName}, nil
}

(別忘了主程式 main.go 也要改為呼叫 NewBlobClient(cfg.AzureStorage.AccountName, cfg.AzureStorage.ContainerName) )

3. 自動化部署腳本更新

部署時不再傳送一長串的金鑰,改為傳送儲存區名稱:

$STORAGE_ACCOUNT_NAME = "demostorageaccount"
$AZURE_STORAGE_CONTAINER_NAME = "demoproject-attachments"

在取得後端的服務身分 ID 後,加入 RBAC 授權:

$storageAccountId = az storage account show `
  --name $STORAGE_ACCOUNT_NAME `
  --resource-group $STORAGE_RG `
  --query id -o tsv
Assert-Success "storage account show"

$storageContainerScope = "$storageAccountId/blobServices/default/containers/$AZURE_STORAGE_CONTAINER_NAME"

az role assignment create `
  --assignee-object-id $backendPrincipalId `
  --assignee-principal-type ServicePrincipal `
  --role "Storage Blob Data Contributor" `
  --scope $storageContainerScope | Out-Null
Assert-Success "Storage Blob Data Contributor role"

最後更新環境變數設定:

@{ name = "AZURE_STORAGE_ACCOUNT_NAME";   value = $STORAGE_ACCOUNT_NAME }
@{ name = "AZURE_STORAGE_CONTAINER_NAME"; value = $AZURE_STORAGE_CONTAINER_NAME }

(小提醒:正式環境的 deploy-production.ps1、.env.example 等檔案也要同步更新,不然正式環境還會傻傻地跟你要金鑰喔!)

4. 警告!附件下載流程會壞掉!

這是大家升級時最容易踩到的超大陷阱!

Managed Identity 的通行證只有「後端 到 儲存區」有效。一旦我們把儲存區設為私有,以前那種直接把網址丟給前端網頁點擊下載的做法就會直接失效,使用者點開只會看到冷酷的 403 Forbidden 拒絕存取畫面!

該怎麼拯救下載功能?

千萬不要因為下載壞掉就把儲存區重新開放給外面的所有人看!

正確的解法是讓後端當「轉接員」,新增一個需要驗證的下載 API 路線:

GET /api/announcements/{id}/attachments/{attachmentId}/download

流程變成:前端跟後端要檔案,後端憑著自己的 Managed Identity 身份去把檔案拿回來,再傳給使用者。這樣既安全又不會斷功能!

改造總結步驟

  1. 建立專屬的容器 Container。
  2. 設定後端身分的 RBAC 授權。
  3. 修改 Go 語言程式碼改走 DefaultAzureCredential。
  4. 更新部署時的環境變數。
  5. 把下載機制改為後端轉接模式(最重要的一步)。
  6. 開始歡樂測試上傳、下載、刪除,確保一切正常!

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *