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.

A hard-coded key moving out of source code and into Secret ManagerA source file called app/config.js loses its hard-coded API key line. The value becomes version 1 of a secret named api-key, and latest points at it. The source file keeps only a reference to an environment variable.app/config.jsapiKey: "sk_live_4f2c9a1e7b"apiKey: process.env.API_KEYapi-key0203v1VERSIONLATEST

Why not keep the key in your code?

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 pasted key spreading through copies, next to a single stored secretOn the left, a config file holds the key itself, and the same key rides along in every clone and branch stacked behind it. On the right, one config file holds only the secret's name, and the value sits in a single place that Google manages.key = "sk_live_4f2c"every clone, branch and logkey = env.API_KEYapi-keyv1 v2 v3one value, one place
  • 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

  1. A secret is a named container.It has a name, a permission list and a replication setting. On its own it holds no value.
  2. Values become numbered versions.Adding a value creates version 1, then 2, then 3. A version’s bytes never change once it exists.
  3. latest means most recently created.Not most recently enabled. If that version is disabled or destroyed, reading latest fails instead of falling back.
  4. 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.

    Environment variable injectionBefore each Cloud Run instance starts, the platform resolves the configured secret version and copies its value into the environment. Existing instances keep their copy, while newly started instances resolve again.secretapi-keyinstance startresolves secretcontainerAPI_KEY=…existing instance keeps v1

    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.
    SDK fetch at runtimeThe running service calls the Secret Manager API itself. On an uncached read, latest targets the most recently created version; the application may cache the result according to its needs.secretapi-keyAPIchecks IAMcontainerapp may cachepicks up v2 on next read

    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.

    How an access request is checkedA read request is compared against the secret's permission list and is either allowed or refused. With Data Access logging enabled, the request also appears in the audit log.THE SECRETread requestIAMCHECKED

    Try a caller

      Pick a caller to see whether the request is allowed.

      Data Access log — simulated, with audit logging enabled
      TimePrincipalMethodResult

      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

        The secret as it stands at this stepThe illustration updates at each step to show which versions exist, which are enabled, and which version latest points at.api-keysecret created — no versions yetv1ENABLEDSLOT 1v2ENABLEDSLOT 2LATESTread → your service

        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.

        Create the secret

        gcloud secrets create api-key \
          --replication-policy=automatic

        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.

        Let one service read it

        gcloud secrets add-iam-policy-binding api-key \
          --member="serviceAccount:app-runtime@my-project.iam.gserviceaccount.com" \
          --role="roles/secretmanager.secretAccessor"

        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.

        More commands, when you need them

        Add a version from a file

        gcloud secrets versions add api-key \
          --data-file=./api-key.txt

        Prints the new version name. This is also how you rotate. There is no separate rotate command, just another version.

        Read a value

        gcloud secrets versions access latest --secret=api-key --out-file=./api-key.txt
        gcloud secrets versions access 3 --secret=api-key

        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.

        Disable and re-enable

        gcloud secrets versions disable 1 --secret=api-key
        gcloud secrets versions enable 1 --secret=api-key

        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.

        Destroy a version

        gcloud secrets versions destroy 1 --secret=api-key
        
        # Optional: configure a one-day delayed-destruction policy
        gcloud secrets update api-key --version-destroy-ttl=86400s

        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.