mcpField
G
gcp-secret-manager

gcp-secret-manager

github.com/bagelholeセキュリティ1.2k344 回インストール23 週間前に更新
リポジトリ

gcp-secret-manager について

Google Cloud Platform で Secret Manager を使ってシークレットを保存、管理するためのスキルです。アクセス制御やバージョン管理に加え、GKE Workload Identity、アプリ SDK、ローテーション自動化、Terraform の例を扱います。

主な機能

  1. 01シークレットとバージョンの作成、管理
  2. 02IAM アクセスバインディングの設定
  3. 03GKE Workload Identity との連携
  4. 04Python、Go、Node.js SDK の例
  5. 05ローテーション自動化と Terraform 設定

活用シーン

  1. 01API キー、データベース認証情報、TLS 証明書の保存
  2. 02GKE ワークロードからのシークレット利用
  3. 03シークレットのバージョン管理とローテーション設定
  4. 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 プロジェクト
  • gcloud CLI がインストールされ、認証済みであること
  • 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 バインディングを使用します

関連スキル

コメント