GSA(GCP Service Account)のキーローテーションをGo言語で実装した
サービスアカウントとは?
サービスアカウントはユーザーではなく、アプリケーションやVMインスタンスで使用するアカウントです。あるサービスアカウントでVMインスタンスを実行する場合などで、必要なリソースへのアクセス権をそのアカウントに付与し、アクセスできるリソースをサービスアカウントの権限によって制御できます。例えば、VMインスタンスで実行されるアプリケーションは、データを格納するように構成されたGCSバケットにアクセスする必要がある。
なぜキーローテーションする必要があるの?
アプリケーションがGCP上で稼働している場合、サービスアカウントキーの管理はGCP側で自動でキーのローテーションが行われていますが、多くのアプリケーションは、開発者のローカルPCやオンプレミス環境などで実行されているので、キーが無防備な状態に置かれることがなく、定期的に変更されるような安全な管理が必要になる。
キーローテーションの実現
キーをローテーションするコードをgolangで実装しCloudFunctionsにデプロイした後、キーを保存するストレージを用意しCloudSchedulerで定期実行するように設定することでキーローテーションが自動化されます。そうすることで安全なサービスアカウントキーの管理を実現でき、開発者にはストレージからダウンロードすることでキーが提供されます。
実際のコード
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
}
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を設定する必要があります。
今回参考にした記事
Discussion