👏

GSA(GCP Service Account)のキーローテーションをGo言語で実装した

2021/09/30に公開

サービスアカウントとは?

サービスアカウントはユーザーではなく、アプリケーションやVMインスタンスで使用するアカウントです。あるサービスアカウントでVMインスタンスを実行する場合などで、必要なリソースへのアクセス権をそのアカウントに付与し、アクセスできるリソースをサービスアカウントの権限によって制御できます。例えば、VMインスタンスで実行されるアプリケーションは、データを格納するように構成されたGCSバケットにアクセスする必要がある。

なぜキーローテーションする必要があるの?

アプリケーションがGCP上で稼働している場合、サービスアカウントキーの管理はGCP側で自動でキーのローテーションが行われていますが、多くのアプリケーションは、開発者のローカルPCやオンプレミス環境などで実行されているので、キーが無防備な状態に置かれることがなく、定期的に変更されるような安全な管理が必要になる。

キーローテーションの実現

キーをローテーションするコードをgolangで実装しCloudFunctionsにデプロイした後、キーを保存するストレージを用意しCloudSchedulerで定期実行するように設定することでキーローテーションが自動化されます。そうすることで安全なサービスアカウントキーの管理を実現でき、開発者にはストレージからダウンロードすることでキーが提供されます。

実際のコード

main.go
package main

import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/storage"
	iam "google.golang.org/api/iam/v1" // indirect
)

var (
	serviceAccountEmail = "xxxxxxxx" //キーローテーションするサービスアカウントのメール
	bucket = "xxxxxxxx" //新しく作成したキーをアップロードするGCSバケット名
)

func main() {
	_, w := io.Pipe()
	listKeys(w, serviceAccountEmail)
	createKey(w, serviceAccountEmail)
}

// listKey lists a service account's keys.
func listKeys(w io.Writer, serviceAccountEmail string) ([]*iam.ServiceAccountKey, error) {
	ctx := context.Background()
	service, err := iam.NewService(ctx)
	if err != nil {
		return nil, fmt.Errorf("iam.NewService: %v\n", err)
	}

	resource := "projects/-/serviceAccounts/" + serviceAccountEmail
	response, err := service.Projects.ServiceAccounts.Keys.List(resource).Do()
	if err != nil {
		fmt.Printf("Projects.ServiceAccounts.Keys.List: %v\n", err)
		return nil, fmt.Errorf("Projects.ServiceAccounts.Keys.List: %v\n", err)
	}
	for _, key := range response.Keys {
		fmt.Printf("Listing key: %v\n", key.Name)
		deleteKey(w, key.Name)
		deleteFile(w, bucket, key.Name)
	}
	return response.Keys, nil
}

// deleteKey deletes a service account key.
func deleteKey(w io.Writer, fullKeyName string) error {
	ctx := context.Background()
	service, err := iam.NewService(ctx)
	if err != nil {
		return fmt.Errorf("iam.NewService: %v\n", err)
	}

	_, err = service.Projects.ServiceAccounts.Keys.Delete(fullKeyName).Do()
	if err != nil {
		fmt.Printf("Projects.ServiceAccounts.Keys.Delete: %v\n", err)
		return fmt.Errorf("Projects.ServiceAccounts.Keys.Delete: %v\n", err)
	}
	fmt.Printf("Deleted key: %v\n", fullKeyName)
	return nil
}

// createKey creates a service account key.
func createKey(w io.Writer, serviceAccountEmail string) (*iam.ServiceAccountKey, error) {
	ctx := context.Background()
	service, err := iam.NewService(ctx)
	if err != nil {
		return nil, fmt.Errorf("iam.NewService: %v\n", err)
	}

	resource := "projects/-/serviceAccounts/" + serviceAccountEmail
	request := &iam.CreateServiceAccountKeyRequest{}
	key, err := service.Projects.ServiceAccounts.Keys.Create(resource, request).Do()
	if err != nil {
		fmt.Printf("Projects.ServiceAccounts.Keys.Create: %v\n", err)
		return nil, fmt.Errorf("Projects.ServiceAccounts.Keys.Create: %v\n", err)
	}
	fmt.Printf("Created key: %v\n", key.Name)
	uploadFile(w, bucket, key.Name)
	return key, nil
}

// deleteFile removes specified object.
func deleteFile(w io.Writer, bucket, object string) error {
	ctx := context.Background()
	client, err := storage.NewClient(ctx)
	if err != nil {
		return fmt.Errorf("storage.NewClient: %v\n", err)
	}
	defer client.Close()

	o := client.Bucket(bucket).Object(object)
	if err := o.Delete(ctx); err != nil {
		return fmt.Errorf("Object(%q).Delete: %v\n", object, err)
	}
	fmt.Printf("Blob %v deleted.\n", object)
	return nil
}

// uploadFile uploads an object.
func uploadFile(w io.Writer, bucket, object string) error {

	ctx := context.Background()
	client, err := storage.NewClient(ctx)
	if err != nil {
		return fmt.Errorf("storage.NewClient: %v\n", err)
	}
	defer client.Close()

	// Upload an object with storage.Writer.
	wc := client.Bucket(bucket).Object(object).NewWriter(ctx)

	if err := wc.Close(); err != nil {
		return fmt.Errorf("Writer.Close: %v\n", err)
	}
	fmt.Printf("Blob %v uploaded.\n", object)
	return nil
}
go.mod
module example.com/cloudfuncion

go 1.13

require (
	cloud.google.com/go/storage v1.10.0
	golang.org/x/net v0.0.0-20210917221730-978cfadd31cf // indirect
	google.golang.org/api v0.57.0
)

解説

関数は主に5つあります。

  • listkeys
  • deletekey
  • createkey
  • deleteFile
  • uploadFile

まず、listkeysでserviceAccountEmailの変数として指定したサービスアカウントのキーの情報を全て取得します。
次に、deletekeyとdeleteFileで先ほど取得したキーを第二引数として指定してサービスアカウント・GCSバケットから削除します。
そして、createkeyでserviceAccountEmailの変数として指定したサービスアカウントのキーを新しく作成し、uploadFileにて第二引数にbucketの変数として指定したkeyファイルをアップロードするバケットを指定し、第三引数に新しく作成したキーを指定して、デベロッパーがダウンロードするためのバケットに新しいサービスアカウントキーをアップロードします。
これで、サービスアカウントのキーローテーションができます。

今後 キーローテーションの定期自動実行環境をGCPで構築

GolangでGSAのキーローテーションは実装できましたが、これを自動で定期実行させるために、CloudFunctionsにデプロイしCloudSchedulerを設定する必要があります。

今回参考にした記事

https://cloud.google.com/blog/ja/products/gcp/help-keep-your-google-cloud-service-account-keys-safe

Discussion