{"id":165706,"date":"2026-04-11T09:18:19","date_gmt":"2026-04-11T06:18:19","guid":{"rendered":"https:\/\/computingforgeeks.com\/google-cloud-secret-manager-tutorial\/"},"modified":"2026-04-11T09:18:19","modified_gmt":"2026-04-11T06:18:19","slug":"google-cloud-secret-manager-tutorial","status":"publish","type":"post","link":"https:\/\/computingforgeeks.com\/google-cloud-secret-manager-tutorial\/","title":{"rendered":"Google Cloud Secret Manager: Complete Tutorial (2026)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">The path from &#8220;we need to store this database password somewhere&#8221; to &#8220;we accidentally committed the database password to Git&#8221; is alarmingly short. Google Secret Manager is the managed store that closes that loop on GCP. Secrets live in an API-accessible vault, access is granted through IAM, versions are immutable, audit logs show exactly who read what and when, and the price per secret is low enough that nobody needs to cut corners. This guide walks through what Secret Manager actually is, how pricing really works (including the free tier most articles skip), the four IAM roles you need to know, how versioning and the <code>latest<\/code> alias actually behave under rollback (with a gotcha that surprises most teams), rotation via Pub\/Sub, regional vs global secrets, CMEK with the automatic-replication trap, and a tested end-to-end External Secrets Operator integration on GKE that pulls secrets into Kubernetes without a single JSON key.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Every command was run on a live GCP project in <code>europe-west1<\/code> with the real output captured, including a behavior check on the <code>latest<\/code> alias that contradicts what most blog posts claim. If you are coming from AWS, the final comparison section is a fast map from AWS Secrets Manager to GCP Secret Manager so you can skim the differences in thirty seconds.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Tested April 2026 on Google Cloud Secret Manager with gcloud 521, GKE Autopilot 1.35.1-gke.1396002, and External Secrets Operator 0.18<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What Secret Manager Actually Is<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Secret Manager is a managed key-value store scoped specifically to application secrets. Each secret is a resource with metadata (name, labels, replication policy, rotation config) and zero or more immutable <strong>versions<\/strong> holding the actual payload. The payload is binary, up to 64 KiB per version, and encrypted at rest with Google-managed keys by default or a CMEK key from Cloud KMS if you bring your own. Access is granted through IAM the same way you grant access to any other GCP resource, which means you can bind directly to a Kubernetes ServiceAccount via <a href=\"https:\/\/computingforgeeks.com\/gke-workload-identity-federation-complete-guide\/\">GKE Workload Identity<\/a>, to a GitHub Actions pipeline via Workload Identity Federation, or to a Google Service Account for a Compute Engine VM.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Secrets are designed around four constraints that make them useful for production. First, versions are immutable. Once you create version 3 you cannot edit it, only disable or destroy it. Second, every access is logged via Cloud Audit Logs Data Access logs (opt-in but free for many workloads). Third, replication is configured at creation time and cannot be changed. Fourth, IAM is granular enough to grant different teams different roles on the same secret without duplicating the secret itself. If you think of it as &#8220;etcd for production credentials with IAM in front,&#8221; the mental model is accurate.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Pricing: What It Actually Costs<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Secret Manager pricing is one of the cheapest line items on a real GCP bill, but the structure has a few sharp edges worth knowing.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead><tr><th>Item<\/th><th>Price<\/th><th>Free tier (always)<\/th><\/tr><\/thead>\n<tbody>\n<tr><td>Active secret versions<\/td><td>$0.06 per version per location per month<\/td><td>6 versions<\/td><\/tr>\n<tr><td>Destroyed secret versions<\/td><td>Free<\/td><td>Unlimited<\/td><\/tr>\n<tr><td>Access operations<\/td><td>$0.03 per 10,000 operations<\/td><td>10,000 operations<\/td><\/tr>\n<tr><td>Management operations<\/td><td>Free<\/td><td>Unlimited<\/td><\/tr>\n<tr><td>Rotation notifications<\/td><td>$0.05 per rotation<\/td><td>3 rotations<\/td><\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The &#8220;per location&#8221; qualifier is where people get caught out. A secret with automatic replication bills as one location regardless of how many regions Google actually replicates it to. A secret with user-managed replication explicitly listing three regions bills as three locations, so a single version costs <code>$0.18<\/code> per month instead of <code>$0.06<\/code>. If you need data residency in specific regions, user-managed replication is the right call. If you just want multi-region durability without thinking about it, automatic wins on cost.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The free tier covers six active versions and ten thousand access operations per month across the entire billing account. A small project with a handful of secrets that gets queried a few times an hour sits inside the free tier permanently. A production workload with dozens of secrets and continuous reads will pay a few dollars per month. It is almost never a meaningful line item unless somebody builds a bad loop that queries the same secret on every HTTP request instead of caching it in memory.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Small set of prerequisites. Nothing exotic.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A GCP project with billing enabled<\/li>\n<li><code>gcloud<\/code> CLI 520+ authenticated with an account that can manage Secret Manager (tested on v521.0.0)<\/li>\n<li>Secret Manager API enabled: <code>gcloud services enable secretmanager.googleapis.com<\/code><\/li>\n<li>For the GKE\/ESO integration section: an existing GKE cluster with Workload Identity enabled. Autopilot clusters have it on by default<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Create, Read, and Version a Secret<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Create a secret with a first version in one shot. The <code>--data-file=-<\/code> flag reads the value from stdin, which keeps the plaintext off your shell history if you pipe from a generator:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>echo -n \"super-secret-value-42\" | gcloud secrets create demo-secret \\\n  --data-file=- \\\n  --replication-policy=automatic<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>-n<\/code> matters. Without it, <code>echo<\/code> appends a newline to the secret payload and every future read gets a trailing newline your code has to strip. This is the single most common &#8220;why does my password not work&#8221; debugging session on GCP.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Read the secret back. The <code>latest<\/code> alias returns the most recent version (with an important caveat covered below):<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud secrets versions access latest --secret=demo-secret<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The output is exactly what you piped in, without a trailing newline:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>super-secret-value-42<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Add more versions. Versions are monotonic integers starting at 1. Each <code>versions add<\/code> call creates a new immutable version and increments the counter:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>echo -n \"value-v2-rotated\" | gcloud secrets versions add demo-secret --data-file=-\necho -n \"value-v3-latest\" | gcloud secrets versions add demo-secret --data-file=-<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">List the versions to see the state of each. Versions have three states: <code>ENABLED<\/code> (readable), <code>DISABLED<\/code> (not readable but payload still stored), and <code>DESTROYED<\/code> (payload permanently deleted, version number preserved forever as a tombstone):<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud secrets versions list demo-secret \\\n  --format=\"table(name,state,createTime.date('%Y-%m-%d %H:%M:%S'))\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Three enabled versions in reverse creation order:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>NAME  STATE    CREATED\n3     enabled  2026-04-11 06:07:34\n2     enabled  2026-04-11 06:07:31\n1     enabled  2026-04-11 05:05:48<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">The &#8220;latest&#8221; Alias Gotcha Every Tutorial Gets Wrong<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Popular belief (including some official-looking blog posts) says the <code>latest<\/code> alias resolves to the highest-numbered <strong>enabled<\/strong> version, and that disabling a broken new version automatically rolls the alias back to the previous good one. This is the rollback story everyone tells, and it does not match what GCP actually does. We tested it to be sure.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Disable version 3 and try to access <code>latest<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud secrets versions disable 3 --secret=demo-secret\ngcloud secrets versions access latest --secret=demo-secret<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The access call fails, with GCP explicitly reporting that version 3 is disabled:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>ERROR: (gcloud.secrets.versions.access) FAILED_PRECONDITION: Secret Version [projects\/PROJECT_NUMBER\/secrets\/demo-secret\/versions\/3] is in DISABLED state.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That confirms the actual behavior: <code>latest<\/code> maps to the highest version number regardless of state. Disabling the highest version does not move the alias, it breaks the alias. To roll back a bad secret you have three real options, in order of increasing regret.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Pin your application to a specific version number<\/strong> instead of using <code>latest<\/code>. Disable version 3, your app still points at version 2, nothing breaks. This is the production pattern Google docs recommend and the reason pinning is considered good practice.<\/li>\n<li><strong>Add a new version<\/strong> with the old value. Version 4 now holds the known-good payload and <code>latest<\/code> resolves to it. Cost is $0.06 per month for the extra active version, which nobody notices.<\/li>\n<li><strong>Destroy version 3<\/strong> permanently. The alias then resolves to version 2. This is irreversible and should be the last resort because auditors hate missing history.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Re-enable the version to restore the demo state:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud secrets versions enable 3 --secret=demo-secret<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">IAM: The Four Roles Worth Knowing<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Secret Manager ships five predefined roles but four of them cover every real use case. Treat the fifth (Viewer) as a read-metadata-only role for auditors.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead><tr><th>Role<\/th><th>Permission<\/th><th>Typical user<\/th><\/tr><\/thead>\n<tbody>\n<tr><td><code>roles\/secretmanager.admin<\/code><\/td><td>Full CRUD on secrets, versions, and IAM<\/td><td>Platform team operators<\/td><\/tr>\n<tr><td><code>roles\/secretmanager.secretVersionManager<\/code><\/td><td>Add, enable, disable, destroy versions on existing secrets<\/td><td>Rotation automation<\/td><\/tr>\n<tr><td><code>roles\/secretmanager.secretVersionAdder<\/code><\/td><td>Add new versions only (no destroy)<\/td><td>CI deploy pipeline writing new secret versions<\/td><\/tr>\n<tr><td><code>roles\/secretmanager.secretAccessor<\/code><\/td><td>Read secret payloads<\/td><td>Application workloads (the by-far most common grant)<\/td><\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Grant at the secret level whenever you can. Granting <code>secretAccessor<\/code> at the project level means the identity can read every secret in the project, which is almost never what you want. The secret-level grant pattern looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud secrets add-iam-policy-binding demo-secret \\\n  --role=roles\/secretmanager.secretAccessor \\\n  --member=\"serviceAccount:app@PROJECT_ID.iam.gserviceaccount.com\" \\\n  --condition=None<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>--condition=None<\/code> flag is required to suppress the interactive prompt that gcloud otherwise opens asking if you want to add an IAM condition. IAM conditions are powerful (time-based access, resource attribute matching) but most deployments do not use them and the prompt is noise in scripts.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Regional Secrets: Data Residency Without Replication<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Secret Manager has two service endpoints. The classic global endpoint (<code>secretmanager.googleapis.com<\/code>) requires a replication policy and stores data in multiple regions. The regional endpoint (<code>secretmanager.REGION.rep.googleapis.com<\/code>) pins a secret to exactly one region with no replication at all. Regional secrets exist for workloads with data residency requirements under GDPR, HIPAA, or similar regulatory frameworks where moving the bytes outside the region is a contractual violation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create a regional secret with the <code>--location<\/code> flag:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>echo -n \"eu-only-password\" | gcloud secrets create eu-db-password \\\n  --location=europe-west1 \\\n  --data-file=-<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Access it the same way as a global secret, but always specify the location. Cross-region access of regional secrets fails by design. If you are building a global application and you do not have a specific regulatory reason to pin, use global with automatic replication. If your legal or compliance team has a region-lock requirement, regional is the answer.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Rotation: Notification, Not Automatic Replacement<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Rotation in GCP Secret Manager does not mean &#8220;GCP generates a new password for you and updates the downstream system.&#8221; That is AWS Secrets Manager&#8217;s model with the Lambda rotation templates. GCP does notification-based rotation: you configure a rotation period and a Pub\/Sub topic, GCP publishes a <code>SECRET_ROTATE<\/code> message to the topic at the configured interval, and you run your own Cloud Function or Cloud Run service subscribed to the topic that does the actual work (call the database admin API, generate a new credential, add a new version to the secret, update the consumer). Google never touches the upstream system.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Configure a rotation schedule on an existing secret. Rotation periods are specified in seconds (minimum 3,600 = 1 hour, maximum 3,153,600,000 = 100 years). The <code>next-rotation-time<\/code> must be at least five minutes in the future:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud pubsub topics create secret-rotation-events\n\ngcloud secrets update demo-secret \\\n  --next-rotation-time=\"2026-05-01T00:00:00Z\" \\\n  --rotation-period=\"2592000s\" \\\n  --topics=projects\/PROJECT_ID\/topics\/secret-rotation-events<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>2592000s<\/code> value is thirty days. The Secret Manager service agent needs <code>roles\/pubsub.publisher<\/code> on the topic, otherwise the rotation notification fires into the void. Grant it with a one-liner:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>PROJECT_NUMBER=$(gcloud projects describe PROJECT_ID --format=\"value(projectNumber)\")\ngcloud pubsub topics add-iam-policy-binding secret-rotation-events \\\n  --role=roles\/pubsub.publisher \\\n  --member=\"serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-secretmanager.iam.gserviceaccount.com\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each rotation notification costs $0.05 and the free tier includes three per month. The Pub\/Sub topic is free at this volume. The downstream Cloud Function that handles the event is where the real work (and the real cost) lives, and is application-specific. A minimal handler signature in Python:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>import base64, json\nfrom google.cloud import secretmanager\n\ndef handle_rotation(event, context):\n    attrs = event.get(\"attributes\", {})\n    secret_id = attrs[\"secretId\"]\n    event_type = attrs[\"eventType\"]\n    if event_type != \"SECRET_ROTATE\":\n        return\n    new_value = generate_new_credential()\n    client = secretmanager.SecretManagerServiceClient()\n    client.add_secret_version(\n        request={\"parent\": secret_id, \"payload\": {\"data\": new_value.encode()}},\n    )<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">CMEK: Customer-Managed Encryption Keys<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">By default, Secret Manager encrypts versions with Google-managed keys. You can bring your own Cloud KMS symmetric key as the key encryption key for extra control and to satisfy compliance frameworks that require customer-managed encryption. The setup has one trap that bites teams who default to automatic replication.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">CMEK keys are region-scoped. Automatic replication stores the secret across multiple Google-managed regions. To combine the two, you have to provide a <strong>CMEK policy with one key per replica location before creating the secret<\/strong>, and this policy is part of the secret&#8217;s replication config at creation time. You cannot retrofit CMEK onto an existing secret, and you cannot use one global CMEK key because the global key does not exist in KMS.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The simpler approach for most teams is user-managed replication with a single region and a single CMEK key, which matches how people deploy production workloads that need key control anyway. The tradeoff is losing the multi-region durability of automatic replication, but a user-managed replication to two chosen regions with two CMEK keys is still cheaper and simpler to reason about than automatic with a key-per-Google-region policy. If you destroy a CMEK key or disable all of its versions, every secret version encrypted with that key becomes permanently unreadable. There is no &#8220;oops, undo&#8221; path.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Audit Logging: Who Read What<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Cloud Audit Logs captures every Secret Manager operation, but the default log type depends on whether the operation mutates state or just reads it. Admin Activity logs are always on and always free and cover every create, update, delete, enable, disable, destroy, and IAM change. Data Access logs cover reads (<code>AccessSecretVersion<\/code>, <code>GetSecret<\/code>, <code>ListSecrets<\/code>, <code>GetIamPolicy<\/code>) and must be explicitly enabled per service in the project&#8217;s IAM audit config, because they can generate large log volumes on busy projects and those logs are billable ingestion into Cloud Logging.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Enable Data Access logging for Secret Manager via the IAM audit policy:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud projects get-iam-policy PROJECT_ID &gt; \/tmp\/policy.yaml\n# edit \/tmp\/policy.yaml and add:\n#   auditConfigs:\n#   - service: secretmanager.googleapis.com\n#     auditLogConfigs:\n#     - logType: DATA_READ\n#     - logType: ADMIN_READ\ngcloud projects set-iam-policy PROJECT_ID \/tmp\/policy.yaml<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Once enabled, every <code>versions access<\/code> call generates a log entry with the principal email, timestamp, secret resource name, caller IP, and authentication method. Query them in Logs Explorer:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>gcloud logging read \\\n  'protoPayload.serviceName=\"secretmanager.googleapis.com\"\n   AND protoPayload.methodName=\"google.cloud.secretmanager.v1.SecretManagerService.AccessSecretVersion\"' \\\n  --limit=20 --format=json<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The output is JSON with one log entry per access. Keep in mind Data Access logs are billed at standard Cloud Logging ingestion rates ($0.50 per GiB beyond the free tier), so enable them on high-traffic projects deliberately.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">End-to-End: External Secrets Operator on GKE (Tested)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The production pattern for pulling Secret Manager secrets into a Kubernetes cluster is the External Secrets Operator. ESO is a Kubernetes-native controller that syncs secrets from external providers (GCP Secret Manager, AWS Secrets Manager, Vault, Azure Key Vault, and many more) into native Kubernetes Secret objects that pods can mount as volumes or env vars. The big win on GKE is that ESO authenticates via Workload Identity Federation, so the cluster never holds a JSON key for the Secret Manager API.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This section assumes you have a GKE cluster with Workload Identity enabled (Autopilot has it on by default) and a Kubernetes ServiceAccount already bound as a direct-access principal on the target secret. The <a href=\"https:\/\/computingforgeeks.com\/gke-workload-identity-federation-complete-guide\/\">GKE Workload Identity guide<\/a> covers that setup in full. We reuse the same <code>demo<\/code> namespace, <code>demo-app<\/code> KSA, and <code>demo-secret<\/code> from there.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Install ESO via its Helm chart into its own namespace:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>helm repo add external-secrets https:\/\/charts.external-secrets.io\nhelm repo update external-secrets\nhelm install external-secrets external-secrets\/external-secrets \\\n  --namespace external-secrets \\\n  --create-namespace \\\n  --set installCRDs=true \\\n  --wait --timeout 5m<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Three ESO pods should come up in the <code>external-secrets<\/code> namespace (controller, webhook, cert-controller):<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>kubectl -n external-secrets get pods<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected output, all three Running:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>NAME                                                READY   STATUS    RESTARTS   AGE\nexternal-secrets-6449b64b4c-9z8t5                   1\/1     Running   0          2m3s\nexternal-secrets-cert-controller-59b6f778d9-6pghv   1\/1     Running   0          2m3s\nexternal-secrets-webhook-d9ccd5985-s79jn            1\/1     Running   0          2m3s<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Create a <code>SecretStore<\/code> in the <code>demo<\/code> namespace. The <code>workloadIdentity<\/code> auth block tells ESO to use the annotated Kubernetes ServiceAccount&#8217;s identity rather than a static key file. The ESO API version is <code>external-secrets.io\/v1<\/code> on 0.15+, which matches the current stable chart version as of April 2026:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>vim secret-store.yaml<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Paste the following manifest. The <code>clusterLocation<\/code> and <code>clusterName<\/code> fields are required for ESO to construct the correct federated token exchange against GKE&#8217;s metadata server:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>apiVersion: external-secrets.io\/v1\nkind: SecretStore\nmetadata:\n  name: gcp-demo-store\n  namespace: demo\nspec:\n  provider:\n    gcpsm:\n      projectID: PROJECT_ID\n      auth:\n        workloadIdentity:\n          clusterLocation: europe-west1\n          clusterName: cfg-lab-gke\n          serviceAccountRef:\n            name: demo-app<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Apply it and confirm ESO reports the store as Valid:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>kubectl apply -f secret-store.yaml\nkubectl -n demo get secretstore gcp-demo-store<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The store flips to Ready within a few seconds once ESO validates it can authenticate to GCP:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>NAME             AGE   STATUS   CAPABILITIES   READY\ngcp-demo-store   33s   Valid    ReadWrite      True<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now create an <code>ExternalSecret<\/code> that references the store and tells ESO to copy <code>demo-secret<\/code> version 3 into a Kubernetes Secret called <code>demo-k8s-secret<\/code>. The <code>refreshInterval<\/code> controls how often ESO re-pulls from the upstream, and version can be a specific number or the literal string <code>latest<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>vim external-secret.yaml<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The manifest:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>apiVersion: external-secrets.io\/v1\nkind: ExternalSecret\nmetadata:\n  name: demo-external-secret\n  namespace: demo\nspec:\n  refreshInterval: 1m\n  secretStoreRef:\n    kind: SecretStore\n    name: gcp-demo-store\n  target:\n    name: demo-k8s-secret\n    creationPolicy: Owner\n  data:\n  - secretKey: password\n    remoteRef:\n      key: demo-secret\n      version: \"3\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Apply and check:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>kubectl apply -f external-secret.yaml\nkubectl -n demo get externalsecret,secret demo-k8s-secret<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Within thirty seconds ESO reports <code>SecretSynced<\/code> and the native Kubernetes Secret exists:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>NAME                                                      STORETYPE     STORE            REFRESH INTERVAL   STATUS         READY   LAST SYNC\nexternalsecret.external-secrets.io\/demo-external-secret   SecretStore   gcp-demo-store   1m                 SecretSynced   True    31s\nNAME              TYPE     DATA   AGE\nsecret\/demo-k8s-secret   Opaque   1      32s<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Confirm the synced value matches what is in Secret Manager:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>kubectl -n demo get secret demo-k8s-secret -o jsonpath='{.data.password}' | base64 -d<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The value matches version 3 of the upstream secret:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>value-v3-latest<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">From here, mount <code>demo-k8s-secret<\/code> into a pod the same way you mount any other Kubernetes Secret. The app never knows or cares that the data came from GCP Secret Manager, the cluster never holds a JSON key file, and the refresh interval controls how quickly a rotation upstream propagates into the cluster. Every access is logged in Cloud Audit Logs on the Secret Manager side and in ESO&#8217;s own controller logs on the cluster side. This is the production pattern for GCP secret delivery to Kubernetes workloads and it is what you should use for anything new.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Error: &#8220;Permission &#8216;secretmanager.versions.access&#8217; denied&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Full error:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>PERMISSION_DENIED: Permission 'secretmanager.versions.access' denied for resource 'projects\/PROJECT_ID\/secrets\/demo-secret\/versions\/latest' (or it may not exist).<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The identity making the call is missing <code>roles\/secretmanager.secretAccessor<\/code> on either the specific secret or the parent project. Grant the role at the secret level (preferred) and wait two to seven minutes for IAM propagation before retrying. If the identity is a GKE Workload Identity principal, verify the <code>principal:\/\/<\/code> URL uses the project <strong>number<\/strong> in the pool path and the project <strong>ID<\/strong> in the pool name.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Error: &#8220;Secret Version [&#8230;] is in DISABLED state&#8221;<\/h3>\n\n\n\n<pre class=\"wp-block-code code\"><code>FAILED_PRECONDITION: Secret Version [projects\/PROJECT_NUMBER\/secrets\/demo-secret\/versions\/3] is in DISABLED state.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>latest<\/code> alias or a specific version call hit a disabled version. See the &#8220;latest alias gotcha&#8221; section for context. Either re-enable the version, pin to a known-good version number, or add a new version with the correct value.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Error: &#8220;Secret already has a replication policy&#8221;<\/h3>\n\n\n\n<pre class=\"wp-block-code code\"><code>FAILED_PRECONDITION: Secret [projects\/PROJECT_ID\/secrets\/demo-secret] already has a replication policy<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Replication is immutable. If you created a secret with automatic replication and need to change it to user-managed, you have to create a new secret, copy the versions over, and delete the old one. There is no in-place update. This is also why CMEK retrofit is impossible: the CMEK policy is part of the replication config.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">ESO SecretStore stuck in &#8220;InvalidProviderConfig&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Almost always caused by one of three things: (a) the <code>clusterLocation<\/code> does not match the real GKE region, (b) the referenced Kubernetes ServiceAccount lacks the IAM binding on Secret Manager, or (c) on clusters with both GKE WI and ESO&#8217;s older auth modes configured, ESO picks the wrong provider. Check the ESO controller logs with <code>kubectl -n external-secrets logs deploy\/external-secrets<\/code> and look for the exact reason. The fix for the IAM case is the same as the first troubleshooting entry: add the binding and wait for propagation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">GCP Secret Manager vs AWS Secrets Manager<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you already know AWS Secrets Manager, this table is the fast answer. Everything else maps to familiar concepts. For the deep dive on the AWS side, see our <a href=\"https:\/\/computingforgeeks.com\/aws-secrets-manager-rotation-guide\/\">AWS Secrets Manager tutorial<\/a>.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead><tr><th>Aspect<\/th><th>GCP Secret Manager<\/th><th>AWS Secrets Manager<\/th><\/tr><\/thead>\n<tbody>\n<tr><td>Price per version<\/td><td>$0.06\/version\/location\/month<\/td><td>$0.40\/secret\/month regardless of version count<\/td><\/tr>\n<tr><td>Access pricing<\/td><td>$0.03 per 10,000 calls<\/td><td>$0.05 per 10,000 calls<\/td><\/tr>\n<tr><td>Free tier<\/td><td>6 versions + 10k ops + 3 rotations always<\/td><td>30-day trial only<\/td><\/tr>\n<tr><td>Rotation model<\/td><td>Pub\/Sub notification. You write the rotation function<\/td><td>Lambda-based. AWS ships rotation templates for RDS, Redshift, DocumentDB<\/td><\/tr>\n<tr><td>Rotation bounds<\/td><td>1 hour to 100 years<\/td><td>4 hours to 1000 days<\/td><\/tr>\n<tr><td>Replication<\/td><td>Automatic (multi-region, 1 location billing) or user-managed (explicit regions, N location billing). Immutable after creation<\/td><td>Primary region plus on-demand replica regions, mutable. Each replica billed as full secret<\/td><\/tr>\n<tr><td>Versioning<\/td><td>Monotonic integer + <code>latest<\/code> alias + custom aliases. Disable to soft-delete<\/td><td>Staging labels (<code>AWSCURRENT<\/code>, <code>AWSPREVIOUS<\/code>, <code>AWSPENDING<\/code>). Move labels to roll back<\/td><\/tr>\n<tr><td>Encryption<\/td><td>Google-managed or CMEK (KMS, EKM). CMEK with auto-replication requires key-per-replica policy at creation<\/td><td>AWS-managed KMS or customer CMK. Per-region<\/td><\/tr>\n<tr><td>IAM<\/td><td>5 predefined roles, secret-level IAM supported<\/td><td>IAM identity policies + secret resource policies<\/td><\/tr>\n<tr><td>Data residency<\/td><td>Regional secrets endpoint, no replication<\/td><td>Single-region secret (no replica regions)<\/td><\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Practical verdict. GCP Secret Manager is cheaper at low to medium scale thanks to the always-on free tier and per-version pricing. AWS wins on the rotation story specifically for RDS, Redshift, and DocumentDB where the Lambda templates save real work. GCP&#8217;s versioning semantics (monotonic integer with disable-to-rollback) are easier to reason about than AWS staging labels, but the <code>latest<\/code> alias behavior documented in this article is a sharp edge you need to plan around. Pick based on where your workloads live; there is no rewriting your stack for a $0.40 per secret difference.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">FAQ<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">What is the difference between global and regional secrets in GCP Secret Manager?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Global secrets are the classic offering. They have a replication policy (automatic or user-managed) and the payload lives in multiple regions. Regional secrets are pinned to exactly one region, have no replication, and use a different service endpoint (<code>secretmanager.REGION.rep.googleapis.com<\/code>). Use regional only when a regulatory requirement mandates that data cannot leave a specific region. For everything else, use global with automatic replication.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does GCP Secret Manager rotate secrets automatically?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Not on its own. Secret Manager supports notification-based rotation: at the configured interval it publishes a <code>SECRET_ROTATE<\/code> message to a Pub\/Sub topic you set up. A Cloud Function or Cloud Run service subscribed to the topic must actually generate the new credential (call the database admin API, create a new IAM key, whatever the upstream system requires) and add a new version to the secret. GCP never modifies the upstream system for you. This is the opposite of AWS Secrets Manager&#8217;s Lambda rotation model where AWS-provided templates handle the full rotation for supported databases.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How does the &#8220;latest&#8221; alias work when I disable the highest version?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>latest<\/code> alias resolves to the highest version <strong>number<\/strong>, not the highest enabled version. If you disable version 3, an application using <code>latest<\/code> starts getting a FAILED_PRECONDITION error instead of silently falling back to version 2. To roll back a bad secret, either pin your application to a specific version number, add a new version with the known-good value (which then becomes the new <code>latest<\/code>), or permanently destroy the bad version. Pinning to explicit version numbers is the recommended production pattern for exactly this reason.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I change the replication policy after creating a secret?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. Replication policy is immutable. To switch from automatic to user-managed or to change the list of user-managed regions, you must create a new secret with the desired policy, copy the versions from the old secret into the new one, and delete the old secret. This is the same constraint that makes CMEK retrofit impossible.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does using External Secrets Operator on GKE require a JSON key file?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. ESO supports Workload Identity Federation for GKE natively. Configure the <code>SecretStore<\/code> with the <code>workloadIdentity<\/code> auth block referencing a Kubernetes ServiceAccount that has been bound to the Secret Manager IAM role (either via direct resource access or legacy GSA impersonation), and ESO will authenticate through the cluster&#8217;s metadata server. No JSON key file should ever touch a production GKE cluster in 2026.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How much does Secret Manager cost in practice?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For most projects, nothing. The always-on free tier covers six active versions and ten thousand access operations per month, which is enough for small production workloads. A medium-sized project with twenty secrets and a few million reads per month pays single-digit dollars. The only way Secret Manager becomes a meaningful line item is if you build a bad loop that queries the same secret on every HTTP request. Cache the secret in memory for at least one minute and you are almost always back inside the free tier.<\/p>\n\n\n\n<script type=\"application\/ld+json\">\n{\n  \"@context\": \"https:\/\/schema.org\",\n  \"@type\": \"FAQPage\",\n  \"mainEntity\": [\n    {\n      \"@type\": \"Question\",\n      \"name\": \"What is the difference between global and regional secrets in GCP Secret Manager?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Global secrets have a replication policy and live in multiple regions. Regional secrets are pinned to one region with no replication and use a different service endpoint. Use regional only when a regulatory requirement mandates that data cannot leave a specific region.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Does GCP Secret Manager rotate secrets automatically?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Not on its own. Secret Manager supports notification-based rotation via Pub\/Sub. A Cloud Function or Cloud Run service must actually generate the new credential and add a new version. GCP never modifies the upstream system for you.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"How does the latest alias work when I disable the highest version?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"The latest alias resolves to the highest version number, not the highest enabled version. Disabling version 3 causes latest to fail with FAILED_PRECONDITION instead of falling back to version 2. Pin your application to specific version numbers in production.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Can I change the replication policy after creating a secret?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"No. Replication policy is immutable. To change it you must create a new secret, copy versions over, and delete the old one.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Does using External Secrets Operator on GKE require a JSON key file?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"No. ESO supports Workload Identity Federation for GKE natively. Configure the SecretStore with the workloadIdentity auth block referencing a Kubernetes ServiceAccount bound to the Secret Manager IAM role.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"How much does Secret Manager cost in practice?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"For most projects, nothing. The free tier covers six active versions and ten thousand access operations per month. A medium project with twenty secrets pays single-digit dollars. The only way it becomes expensive is a bad loop that queries the same secret on every HTTP request.\"\n      }\n    }\n  ]\n}\n<\/script>\n\n\n\n<h2 class=\"wp-block-heading\">Where to Go Next<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Secret Manager is usually the first GCP security primitive a team puts in production because everything else depends on it. Natural next steps are wiring it into the CI\/CD pipeline (GitHub Actions via Workload Identity Federation, no JSON keys), cross-project access from a shared secrets project to application projects, and mapping the existing on-prem secret store (HashiCorp Vault, AWS Secrets Manager from a previous cloud) onto the same model. The <a href=\"https:\/\/cloud.google.com\/secret-manager\/docs\/overview\" target=\"_blank\" rel=\"noreferrer noopener\">official Secret Manager overview<\/a>, the <a href=\"https:\/\/external-secrets.io\/latest\/provider\/google-secrets-manager\/\" target=\"_blank\" rel=\"noreferrer noopener\">ESO GCP provider docs<\/a>, and our <a href=\"https:\/\/computingforgeeks.com\/gke-workload-identity-federation-complete-guide\/\">GKE Workload Identity guide<\/a> are the three references worth bookmarking.<\/p>\n\n","protected":false},"excerpt":{"rendered":"<p>Tested GCP Secret Manager guide covering pricing, IAM, versioning gotchas, rotation via Pub\/Sub, regional secrets, CMEK traps, and a full External Secrets Operator integration on GKE with Workload Identity.<\/p>\n","protected":false},"author":3,"featured_media":165707,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2680,36939],"tags":[36175],"cfg_series":[39811],"class_list":["post-165706","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cloud","category-gcp","tag-gcp","cfg_series-gcp-platform"],"_links":{"self":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/165706","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/comments?post=165706"}],"version-history":[{"count":0,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/165706\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media\/165707"}],"wp:attachment":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media?parent=165706"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/categories?post=165706"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/tags?post=165706"},{"taxonomy":"cfg_series","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/cfg_series?post=165706"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}