Your API key stops living in your source code and starts living in Secret Manager.
Secret Manager stores a secret as a series of numbered versions. You never edit a version. You add a new one, switch an old one off, or destroy it. This page shows you how that works, with commands you can run.
One thing to rememberCode holds the name of the secret. Google holds the value.
You paste a key into a config file to get moving. It is copied into every clone and every branch from then on, and it can surface in CI output. Deleting the line later does not take it back — the value is still in your git history.
Rotating it now means a code change and a deployment, and nobody can say for certain which copy is current or who used it.
A leak is easier to contain.You add a new version and move callers to it instead of rewriting the application and chasing every copy, though moving those callers can still need a deployment or config change.
Access belongs to an application identity, not to a file.Your service reads with its own service account, and those reads appear in Data Access logs once that audit logging is enabled in the applicable project, folder or organization configuration.
One managed source of truth replaces scattered .env copies.Version history makes deliberate rotation and rollback easier.
The 20-second summary
A secret is a named container.It has a name, a permission list and a replication setting. On its own it holds no value.
Values become numbered versions.Adding a value creates version 1, then 2, then 3. A version’s bytes never change once it exists.
latest means most recently created.Not most recently enabled. If that version is disabled or destroyed, reading latest fails instead of falling back.
IAM decides who can read it.Your app signs in as a service account. Give that account roles/secretmanager.secretAccessor on the one secret it needs.
A secret holds versions. Versions never change.
One thing to rememberAdding a value never edits the old one. It creates a new numbered version.
Creating a secret gives you a name and a permission list. It holds no value yet. Each value you add becomes its own numbered version, and those bytes are fixed from then on.
latest is not a version. It points at the most recently created one, and it never skips a disabled or destroyed version. In those cases reading latest fails rather than falling back to an older version.
projects/my-project/secrets/api-key
Version details — sample data
Name
—
State
—
Created
—
Payload
—
Why fixed versions help. Because a version never changes, “which value did the service read on Tuesday?” has one answer, and rolling back is just naming an older number.
Name a number when it matters.latest is convenient but moves. versions/3 stays put. Rollouts that need to be reversible usually name a number.
Two ways your code gets the value
One thing to rememberAn injected value is fixed when the instance starts. An SDK read picks up a new version on its next uncached call.
Either the platform copies the value into your environment before your code starts, or your code asks Secret Manager for it directly. Both are normal, and they fail in different ways.
Injected before an instance starts
Before each instance starts, Cloud Run resolves the configured version and writes it into the environment. Your code just reads process.env.API_KEY. Nothing about Secret Manager appears in your source, but the value now sits in the environment, where a crash dump or a debug endpoint can expose it.
No client library, no extra credentials, no runtime delay. Works in any language.
The value is a per-instance copy. Existing instances keep it; new instances resolve the configured version again.
With latest, old and new instances can hold different values, so Google recommends pinning to a numeric version.
Fetched by your code
Your service signs in with its own identity and calls accessSecretVersion. That means adding the client library, working credentials, and handling a failed fetch. Your app decides how long to cache the result.
An uncached read of latest returns the most recently created version, and fails if that version is not enabled.
You can choose a version at runtime, including pinning to a number.
Each uncached read is an access operation and a network round trip. Access charges apply after the monthly free allowance.
Inject it — Cloud Run
gcloud run services update my-service \--update-secrets=API_KEY=api-key:1
Pins the environment variable to version 1, as Google recommends. Cloud Run resolves that version before every instance starts. Using api-key:latest instead can give new instances a newer value while existing instances keep their earlier copy.
Fetch it — Python
from google.cloud import secretmanager
client = secretmanager.SecretManagerServiceClient()
name = "projects/my-project/secrets/api-key/versions/latest"
value = client.access_secret_version(name=name).payload.data.decode("utf-8")
The payload comes back as bytes, so decode it. Cache the result for as long as your app can tolerate rather than making an uncached call on every request.
Who is allowed to read it
One thing to rememberGrant roles/secretmanager.secretAccessor on the one secret a service needs, never across the whole project.
Every read is checked first. Secret Manager compares the caller's identity against that secret's permission list, then returns the value or refuses. AccessSecretVersion is recorded as a Data Access log when the applicable audit logging is enabled.
Try a caller
Pick a caller to see whether the request is allowed.
Data Access log — simulated, with audit logging enabled
Time
Principal
Method
Result
No requests yet.
Administrative writes — creating a secret, adding or destroying a version, changing the policy — are Admin Activity logs. AccessSecretVersion is a Data Access log, and it normally has to be turned on in the applicable project, folder or organization audit configuration.
secretAccessor reads the value. This is the role a running service needs. Grant it on one secret, not project-wide.
viewer sees details, not values. It can list versions and read their states and timestamps, but never the value itself.
secretVersionManager rotates without reading. It can add, disable and destroy versions but cannot read any of them. A good fit for a rotation job.
admin does everything, including permissions. It belongs to the people and pipelines that set a secret up, never to a running app.
The whole life of a secret
One thing to rememberRotate by adding a version, disable the old one, and destroy it only once you are sure nothing still reads it.
Six steps cover almost everything you will do. You can undo five of them. The last one is set apart here because you cannot.
Step 1 of 6
Command
Danger zone — destroying is immediate by default
By default, destroy erases the value straight away and for good. The version stays in the list with its number and a DESTROYED state while the secret exists, and that number is never reused.
A secret configured with --version-destroy-ttl behaves differently: destroy disables the version and schedules it, and an administrator can restore it before the 1–1000 day delay runs out.
Commands you can actually run
One thing to rememberUse printf '%s' to add a value. echo adds a newline that becomes part of your secret.
Swap api-key, my-project and my-service for your own names. Set your project first with gcloud config set project my-project, or add --project=my-project to each command.
Creates the container, not a value. automatic lets Google choose the regions. User-managed replication can support location requirements, and when access itself must stay regional, consider regional Secret Manager. You cannot change a secret’s replication policy after creation.
Add a value without a stray newline
# echo appends a newline, and that byte gets stored in the secret# echo "$SECRET_VALUE" | gcloud secrets versions add api-key --data-file=- # stores "value\n"# printf '%s' writes exactly the bytes you gave it
printf '%s'"$SECRET_VALUE" | gcloud secrets versions add api-key --data-file=-
This is the mistake that costs an afternoon. The trailing \n is stored as part of the value, so your app gets "abc\n" and the API you call rejects it with an error that never mentions whitespace. printf '%s' adds nothing. Files have the same trap, because most editors add a final newline.
Binds the role on this secret only. The same role granted at project level would hand that identity every secret in the project, which is almost never what you meant.
Without --out-file, the value goes to stdout and can appear in terminal scrollback, recordings or captured logs. Write it to a protected file or pipe it straight to whatever needs it, then remove temporary files safely.
List secrets and versions
gcloud secrets list
gcloud secrets versions list api-key
The list shows number, state and creation time, never values. A destroyed version stays listed with its number and state while the secret exists; deleting that secret removes its versions too.
Reads of a disabled version fail immediately. If it is the most recently created version, latest still points at it and fails too, with no fallback. Disabling is reversible, so it is the right first move when you suspect a value leaked.
By default, destroy erases the value immediately and for good. With a configured 1–1000 day destruction delay, destroy disables and schedules the version instead, and an administrator can restore it before expiry. Version numbers are not reused within a secret.
Delete the whole secret
gcloud secrets delete api-key
Irreversibly removes the secret, every version in it and its permission list. Access eventually returns Not Found, and the identifier can be reused afterwards. This is for retiring a secret, not rotating one.
Put this to work
Paste this into Claude Code, Cursor, OpenCode, Copilot or another coding assistant to apply this guide to your own repository.
You are helping me move hard-coded secrets out of this repository and into
Google Secret Manager. Follow these rules for the whole task.
RULES
1. Give me the full plan before edits, and wait for my approval.
2. Mask every secret value in your output: show only the last 4 characters.
3. Never fetch or echo a secret payload into this conversation. Do not run
`gcloud secrets versions access` just to show me a value.
4. No destructive operations and no history rewrites. Do not delete secrets or
versions, do not force push, and do not rewrite git history.
STEPS
1. Scan the source, config files, .env* files and CI definitions for anything
that looks like a credential. Report every hit as file:line with the value
masked to the last 4 characters.
2. Confirm with me which GCP project to use, then enable the API:
gcloud services enable secretmanager.googleapis.com
3. For each secret, propose a kebab-case name. Create it with:
gcloud secrets create NAME --replication-policy=automatic
Add the value with:
printf '%s' "$VALUE" | gcloud secrets versions add NAME --data-file=-
Tell me why this uses printf and not echo: echo appends a newline, and that
byte is stored as part of the value.
4. Grant roles/secretmanager.secretAccessor on that one secret only, never
project-wide, and only to the service account that actually needs it:
gcloud secrets add-iam-policy-binding NAME \
--member="serviceAccount:SERVICE_ACCOUNT" \
--role="roles/secretmanager.secretAccessor"
5. Change the code to read the value from an environment variable, or through
the Secret Manager SDK at startup. No hard-coded fallback value. If the value
is missing, fail closed with a clear error instead of starting up.
6. Make sure .env* is gitignored, and keep only names, never values, in
.env.example.
7. For Cloud Run, inject the secret with a numeric version pin such as NAME:2,
not latest, so every instance gets the same value. To rotate: add a new
version, update the pin, then redeploy.
8. Run this project's build and test commands, then confirm the app starts with
no hard-coded values left.
9. Finish by listing every secret I must rotate at its provider, because
anything that ever touched git history is still in that history.
What people usually get wrong
One thing to rememberlatest follows creation order, not enabled state. Name a version number when a rollout has to be predictable.
Each one starts with a common assumption, crossed out, then what actually happens.
Updating a secret changes its value.
There is no update
Updating means adding a version. The old bytes stay exactly where they were, readable by anyone who names that number, until you disable or destroy them.
latest is a version I can rely on.
latest follows creation, not state
It points at the most recently created version. Add version 4 and an uncached read of latest goes to it. If version 4 is disabled or destroyed, that read fails instead of falling back. Name a number when a rollout has to be predictable.
echo is fine for piping a value in.
echo stores a newline
echo "abc" sends four bytes, so your app gets "abc\n" and every request fails with an error that never mentions whitespace. Use printf '%s', and check files for a trailing newline too.
Disable and destroy are roughly the same.
Disable is reversible. Destroy is not.
Enable brings a disabled version straight back. Destroy erases the value immediately and permanently, unless the secret has a configured destruction delay, which gives you a restore window before it expires. Disable, watch, then destroy.
My service just needs the Secret Manager role.
Grant secretAccessor on one secret
A running service needs roles/secretmanager.secretAccessor, bound on that individual secret. admin would also let it destroy versions and rewrite permissions, and a project-level binding hands it every other secret you own.
More details and edge cases
Rotating updates my running services.
Injected values are snapshots
Cloud Run resolves the configured version before each instance starts. Existing instances keep their injected value while new instances resolve again, so latest can mix values across instances. Pin environment variables to a numeric version and change that pin deliberately.
Reads are free, so fetch per request.
Every access is an operation and a round trip
Access charges apply after the monthly free allowance, and each uncached read adds a network round trip to your request. Cache for as long as your app can tolerate, and handle a failed fetch.
I will log the value just while I debug.
A logged secret is a leaked secret
Once it reaches your logging pipeline it is in every sink, backup and export downstream, and rotating is the only fix. Log the version name, api-key/versions/3, and never the value.
Deleting and recreating is a clean rotation.
Delete removes the history too
gcloud secrets delete irreversibly takes the secret, every version and the permission list with it. Access eventually returns Not Found, and the identifier can be reused afterwards. Rotate by adding a version; delete only when the secret is genuinely retired.
Replication is a detail I can fix later.
Replication is fixed at creation
automatic spreads the secret across regions for you. User-managed replication can support location requirements, but when access itself must stay regional, consider regional Secret Manager. Changing the policy means creating a new secret.
Six questions
One thing to rememberIf you can answer these six, you have the model.
You get the answer and the reason straight away. One attempt per question, and you can start over any time.
The short version
A secret is a named container. Values become numbered versions that never change. latest points at the most recently created one, and fails if that version is disabled or destroyed. IAM decides who can read it, and Data Access logging records who asked once it is enabled.
If you keep one habit from this page, make it this: rotate by adding a version, disable the old one, wait, and only then destroy.
Every project name, secret name, service account and value on this page is a placeholder written for the example. No real credential appears here, and nothing on this page is sent anywhere — it is one file with no network calls. Behaviour described here follows Google's public documentation; check the docs above for the current details before you rely on it.