gcp-secret-manager について
Google Cloud Platform で Secret Manager を使ってシークレットを保存、管理するためのスキルです。アクセス制御やバージョン管理に加え、GKE Workload Identity、アプリ SDK、ローテーション自動化、Terraform の例を扱います。
主な機能
- 01シークレットとバージョンの作成、管理
- 02IAM アクセスバインディングの設定
- 03GKE Workload Identity との連携
- 04Python、Go、Node.js SDK の例
- 05ローテーション自動化と Terraform 設定
活用シーン
- 01API キー、データベース認証情報、TLS 証明書の保存
- 02GKE ワークロードからのシークレット利用
- 03シークレットのバージョン管理とローテーション設定
- 04IAM によるシークレットアクセス制御
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ bagelhole/ devops-security-agent-skills --skill gcp-secret-managerインストール後、Claude Code で/gcp-secret-managerを入力して読み込まれたか確認します。
詳細
GCP Secret Manager
Google Cloud Platform でシークレットを安全に保存・管理します。
このスキルを使用する場面
次のような場合にこのスキルを使用します。
- GCP 環境でシークレットを管理する
- Workload Identity を介して GKE ワークロードとシークレットを連携する
- API キー、データベース認証情報、TLS 証明書を保存する
- シークレットのバージョン管理とローテーションを実装する
- 集中管理されたシークレット管理に関するコンプライアンス要件を満たす
前提条件
- 課金が有効になっている GCP プロジェクト
gcloudCLI がインストールされ、認証済みであること- Secret Manager API が有効であること(
secretmanager.googleapis.com) - IAM 権限:管理には
roles/secretmanager.admin、読み取りにはroles/secretmanager.secretAccessor - GKE の場合:クラスターで Workload Identity が構成されていること
API を有効にする
# Enable Secret Manager API
gcloud services enable secretmanager.googleapis.com
# Verify it's enabled
gcloud services list --enabled --filter="name:secretmanager"
シークレットの作成と管理
# Create a secret (creates the secret resource, not the value)
gcloud secrets create db-password \
--replication-policy="automatic" \
--labels="env=production,team=platform"
# Add the secret value (first version)
echo -n "S3cur3P@ssw0rd!" | gcloud secrets versions add db-password --data-file=-
# Create secret with value in one command
echo -n '{"username":"dbadmin","password":"S3cur3P@ss!","host":"10.0.1.5","port":5432}' | \
gcloud secrets create db-credentials --data-file=- \
--replication-policy="automatic" \
--labels="env=production,team=platform"
# Create with specific region replication
gcloud secrets create regional-secret \
--replication-policy="user-managed" \
--locations="us-central1,us-east1"
# Create with customer-managed encryption key (CMEK)
gcloud secrets create sensitive-secret \
--replication-policy="user-managed" \
--locations="us-central1" \
--kms-key-name="projects/my-project/locations/us-central1/keyRings/my-ring/cryptoKeys/my-key"
# Access the latest version
gcloud secrets versions access latest --secret=db-password
# Access a specific version
gcloud secrets versions access 3 --secret=db-password
# Add a new version (rotation)
echo -n "N3wS3cur3P@ss!" | gcloud secrets versions add db-password --data-file=-
# List all secrets
gcloud secrets list --format="table(name, createTime, labels)"
# List versions of a secret
gcloud secrets versions list db-password --format="table(name, state, createTime)"
# Disable a version (makes it inaccessible but recoverable)
gcloud secrets versions disable 1 --secret=db-password
# Enable a disabled version
gcloud secrets versions enable 1 --secret=db-password
# Destroy a version (permanent)
gcloud secrets versions destroy 1 --secret=db-password
# Delete the entire secret
gcloud secrets delete db-password
# Set expiration on a secret
gcloud secrets update db-password \
--expire-time="2026-06-01T00:00:00Z"
# Set TTL-based expiration
gcloud secrets update temp-token \
--ttl="2592000s" # 30 days
# Update labels
gcloud secrets update db-password \
--update-labels="rotation=enabled,last-rotated=2025-01-15"
# Add version aliases
gcloud secrets versions update 5 --secret=db-password --set-aliases="production"
IAM バインディング
# Grant secret accessor role to a service account
gcloud secrets add-iam-policy-binding db-password \
--member="serviceAccount:[email protected]" \
--role="roles/secretmanager.secretAccessor"
# Grant access to a specific secret version
gcloud secrets add-iam-policy-binding db-password \
--member="serviceAccount:[email protected]" \
--role="roles/secretmanager.secretVersionAccessor" \
--condition='expression=resource.name.endsWith("versions/latest"),title=latest-only'
# Grant admin to security team
gcloud secrets add-iam-policy-binding db-password \
--member="group:[email protected]" \
--role="roles/secretmanager.admin"
# View IAM policy for a secret
gcloud secrets get-iam-policy db-password
# Remove access
gcloud secrets remove-iam-policy-binding db-password \
--member="serviceAccount:[email protected]" \
--role="roles/secretmanager.secretAccessor"
# Project-level IAM for all secrets
gcloud projects add-iam-policy-binding my-project \
--member="serviceAccount:[email protected]" \
--role="roles/secretmanager.secretAccessor" \
--condition='expression=resource.name.startsWith("projects/my-project/secrets/myapp-"),title=myapp-secrets-only'
GKE 用 Workload Identity
# Enable Workload Identity on cluster (if not already)
gcloud container clusters update my-cluster \
--zone us-central1-a \
--workload-pool=my-project.svc.id.goog
# Create GCP service account for the workload
gcloud iam service-accounts create myapp-gke-sa \
--display-name="MyApp GKE Service Account"
# Grant secret accessor role
gcloud secrets add-iam-policy-binding db-password \
--member="serviceAccount:[email protected]" \
--role="roles/secretmanager.secretAccessor"
# Bind Kubernetes SA to GCP SA
gcloud iam service-accounts add-iam-policy-binding \
[email protected] \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:my-project.svc.id.goog[production/myapp-sa]"
Kubernetes マニフェスト
# Kubernetes service account annotated with GCP SA
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp-sa
namespace: production
annotations:
iam.gke.io/gcp-service-account: "[email protected]"
---
# Secrets Store CSI Driver for GCP
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: gcp-secrets
namespace: production
spec:
provider: gcp
parameters:
secrets: |
- resourceName: "projects/my-project/secrets/db-password/versions/latest"
path: "db-password"
- resourceName: "projects/my-project/secrets/db-credentials/versions/latest"
path: "db-credentials"
- resourceName: "projects/my-project/secrets/api-key/versions/latest"
path: "api-key"
secretObjects:
- secretName: myapp-secrets
type: Opaque
data:
- objectName: db-password
key: DB_PASSWORD
- objectName: api-key
key: API_KEY
---
# Deployment using the secrets
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
serviceAccountName: myapp-sa
containers:
- name: myapp
image: gcr.io/my-project/myapp:v1.0.0
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secrets
key: DB_PASSWORD
volumeMounts:
- name: secrets
mountPath: "/var/secrets"
readOnly: true
volumes:
- name: secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "gcp-secrets"
アプリケーション SDK の例
Python
from google.cloud import secretmanager
from google.api_core import exceptions
import json
def get_secret(project_id: str, secret_id: str, version: str = "latest") -> str:
"""Access a secret version from GCP Secret Manager."""
client = secretmanager.SecretManagerServiceClient()
name = f"projects/{project_id}/secrets/{secret_id}/versions/{version}"
try:
response = client.access_secret_version(request={"name": name})
return response.payload.data.decode("UTF-8")
except exceptions.NotFound:
raise ValueError(f"Secret {secret_id} version {version} not found")
except exceptions.PermissionDenied:
raise PermissionError(f"No access to secret {secret_id}")
def get_json_secret(project_id: str, secret_id: str) -> dict:
"""Access and parse a JSON secret."""
raw = get_secret(project_id, secret_id)
return json.loads(raw)
def create_secret(project_id: str, secret_id: str, value: str, labels: dict = None) -> str:
"""Create a new secret with an initial version."""
client = secretmanager.SecretManagerServiceClient()
parent = f"projects/{project_id}"
secret_config = {
"replication": {"automatic": {}},
}
if labels:
secret_config["labels"] = labels
secret = client.create_secret(
request={"parent": parent, "secret_id": secret_id, "secret": secret_config}
)
client.add_secret_version(
request={"parent": secret.name, "payload": {"data": value.encode("UTF-8")}}
)
return secret.name
def rotate_secret(project_id: str, secret_id: str, new_value: str) -> str:
"""Add a new version to rotate the secret."""
client = secretmanager.SecretManagerServiceClient()
parent = f"projects/{project_id}/secrets/{secret_id}"
version = client.add_secret_version(
request={"parent": parent, "payload": {"data": new_value.encode("UTF-8")}}
)
return version.name
def list_secrets(project_id: str, filter_str: str = "") -> list:
"""List all secrets in a project."""
client = secretmanager.SecretManagerServiceClient()
parent = f"projects/{project_id}"
secrets = []
for secret in client.list_secrets(request={"parent": parent, "filter": filter_str}):
secrets.append({
"name": secret.name.split("/")[-1],
"created": secret.create_time.isoformat(),
"labels": dict(secret.labels),
})
return secrets
# Usage
creds = get_json_secret("my-project", "db-credentials")
connection_string = (
f"postgresql://{creds['username']}:{creds['password']}"
f"@{creds['host']}:{creds['port']}/mydb"
)
Go
package main
import (
"context"
"fmt"
"log"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
secretmanagerpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
)
func getSecret(projectID, secretID, version string) (string, error) {
ctx := context.Background()
client, err := secretmanager.NewClient(ctx)
if err != nil {
return "", fmt.Errorf("failed to create client: %w", err)
}
defer client.Close()
name := fmt.Sprintf("projects/%s/secrets/%s/versions/%s", projectID, secretID, version)
result, err := client.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{
Name: name,
})
if err != nil {
return "", fmt.Errorf("failed to access secret: %w", err)
}
return string(result.Payload.Data), nil
}
func main() {
secret, err := getSecret("my-project", "db-password", "latest")
if err != nil {
log.Fatalf("Error: %v", err)
}
fmt.Printf("Secret: %s\n", secret)
}
Node.js
const { SecretManagerServiceClient } = require('@google-cloud/secret-manager');
const client = new SecretManagerServiceClient();
async function getSecret(projectId, secretId, version = 'latest') {
const name = `projects/${projectId}/secrets/${secretId}/versions/${version}`;
const [response] = await client.accessSecretVersion({ name });
return response.payload.data.toString('utf8');
}
async function main() {
const password = await getSecret('my-project', 'db-password');
console.log(`Secret retrieved, length: ${password.length}`);
}
main().catch(console.error);
Cloud Functions によるシークレットのローテーション
"""cloud_function_rotation.py - Triggered by Pub/Sub on secret rotation events."""
import functions_framework
from google.cloud import secretmanager
import secrets
import string
@functions_framework.cloud_event
def rotate_secret(cloud_event):
"""Handle secret rotation events from Pub/Sub."""
data = cloud_event.data
secret_name = data.get("name", "")
if "db-password" not in secret_name:
print(f"Skipping non-DB secret: {secret_name}")
return
client = secretmanager.SecretManagerServiceClient()
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
new_password = ''.join(secrets.choice(alphabet) for _ in range(32))
parent = "/".join(secret_name.split("/")[:4])
client.add_secret_version(
request={
"parent": parent,
"payload": {"data": new_password.encode("UTF-8")},
}
)
print(f"Rotated secret: {parent}")
Cloud Scheduler によるローテーションスケジュール
# Create a Pub/Sub topic for rotation events
gcloud pubsub topics create secret-rotation
# Configure secret to publish rotation events
gcloud secrets update db-password \
--add-topics="projects/my-project/topics/secret-rotation" \
--event-types="SECRET_ROTATE"
# Set up rotation schedule
gcloud secrets update db-password \
--next-rotation-time="2025-04-01T00:00:00Z" \
--rotation-period="2592000s" # 30 days
Terraform の設定
resource "google_secret_manager_secret" "db_password" {
project = var.project_id
secret_id = "db-password"
replication {
auto {}
}
labels = {
env = "production"
team = "platform"
}
rotation {
next_rotation_time = "2025-04-01T00:00:00Z"
rotation_period = "2592000s"
}
topics {
name = google_pubsub_topic.secret_rotation.id
}
}
resource "google_secret_manager_secret_version" "db_password" {
secret = google_secret_manager_secret.db_password.id
secret_data = var.db_password
}
resource "google_secret_manager_secret_iam_member" "app_accessor" {
project = var.project_id
secret_id = google_secret_manager_secret.db_password.secret_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.app.email}"
}
トラブルシューティング
| 問題 | 原因 | 解決策 |
|---|---|---|
| "Secret Manager API not enabled" | プロジェクトで API が有効化されていない | gcloud services enable secretmanager.googleapis.com を実行します |
| アクセス時の "Permission denied" | secretAccessor ロールが不足している | 対象のシークレットに roles/secretmanager.secretAccessor を付与します |
| Workload Identity が機能しない | K8s SA が GCP SA にバインドされていない | K8s SA のアノテーションを確認し、workloadIdentityUser による IAM バインディングを確認します |
| "Secret version is in DISABLED state" | バージョンが無効化されている | gcloud secrets versions enable VERSION --secret=SECRET で有効にします |
| シークレットへのアクセスでレイテンシーが高い | クライアント側のキャッシュがない | TTL を設定してシークレットをメモリにキャッシュします。GKE では CSI ドライバーを使用します |
| CMEK の復号に失敗する | KMS キーの権限が不足している | Secret Manager SA に roles/cloudkms.cryptoKeyEncrypterDecrypter を付与します |
| ローテーション関数がトリガーされない | Pub/Sub トピックが構成されていない | トピックがシークレットに関連付けられていることを確認し、Cloud Function のサブスクリプションを確認します |
ベストプラクティス
- エクスポートしたサービスアカウントキーの代わりに、GKE では Workload Identity を使用します
- プロジェクトレベルではなく、個々のシークレットのレベルで最小権限の IAM を実装します
- すべてのシークレットアクセスについて監査ログ(Cloud Audit Logs)を有効にします
- ローテーション中に問題が発生した場合に安全にロールバックできるよう、シークレットのバージョンを使用します
- 一時的なシークレットには有効期限または TTL を設定します
- Cloud KMS と連携して、顧客管理の暗号化キーを使用します
- 組織化と自動化のためにラベルを一貫して使用します
- Cloud Monitoring でシークレットへのアクセスパターンを監視します
- 長期間使用するすべての認証情報にローテーションスケジュールを実装します
- リソース名のパターンに基づいてアクセスを制限する条件付き IAM バインディングを使用します
関連スキル
- hashicorp-vault - マルチクラウドのシークレット
- gcp-gke - GKE との連携
- aws-secrets-manager - AWS のシークレット管理
- azure-keyvault - Azure のシークレット管理
コメント
Markdown 対応