seekrit
Docs/Google Secret Manager

Sync to Google Secret Manager

Sync to Secret Manager when something in your project already reads from it: a Cloud Run --set-secrets mount, the GKE Secret Manager CSI driver, a Terraform data source, or a team convention you are not going to change. Where you control the process instead, seekrit run or an SDK keeps decryption on your side.

At a glance
What seekrit writesOne secret per name, or all of them as one JSON secret
Addressed byAn optional ID prefix, or the one bundle secret's ID
Connection carriesThe service-account key JSON, and the project to write
IAMroles/secretmanager.admin on the project — never versions.access
Takes effectNext read by your app
DeletionImmediate and permanent — there is no recovery window

New to sync? Read Third-party sync first — the decryption grant, name mapping, deletions, and failure handling are the same on every destination.

1. Create a service account and a key

seekrit authenticates as a service account, so make one for this and give it access to nothing else:

gcloud iam service-accounts create seekrit-sync \
  --display-name="seekrit sync" --project acme-prod

gcloud projects add-iam-policy-binding acme-prod \
  --member="serviceAccount:seekrit-sync@acme-prod.iam.gserviceaccount.com" \
  --role="roles/secretmanager.admin"

gcloud services enable secretmanager.googleapis.com --project acme-prod

gcloud iam service-accounts keys create key.json \
  --iam-account=seekrit-sync@acme-prod.iam.gserviceaccount.com

roles/secretmanager.admin is the convenient answer. The permissions actually used are narrower — secretmanager.secrets.get, create, update, delete, versions.add, plus versions.destroy if you turn on version pruning and secrets.list for Test connection — so a custom role holding just those works too.

You can go tighter still and grant the role on individual secrets rather than on the project, but know what that costs: Google answers a request for a resource you have no access to with PERMISSION_DENIED whether or not it exists, and seekrit reads a secret's metadata as its first step. So a name the binding resolves that has no grant of its own — a secret you add to the environment later — fails the whole run with a permission error instead of being created. Project-level is the setup that keeps a new secret working without a second deploy.

note

Nothing here needs secretmanager.versions.access. seekrit writes values and never reads one back, so the credential you hand it cannot be used to read your project's secrets. That is deliberate — it is why the connector compares a digest instead of comparing values.

2. Add the connection

The credential is the whole key file, so pipe it in:

seekrit sync connect --name acme-gcp --provider gcp-secret-manager \
  --project-id acme-prod < key.json

One connection covers one project. The key JSON says which service account it is; the project is stated separately because a service account can be granted secrets in projects other than its own. --project-id takes the project ID (acme-prod) or its number — both work, and the ID is the one you can read off your own dashboard.

Delete key.json afterwards — seekrit holds it encrypted to the connection's public key, and nothing else needs it.

3. Bind an environment

The default writes one GCP secret per seekrit secret, under an optional prefix:

seekrit sync verify acme-gcp --provider gcp-secret-manager

seekrit sync enable --connection acme-gcp --provider gcp-secret-manager \
  --gcp-prefix prod-storefront- \
  --app storefront --env production --acknowledge-decryption

The alternative packs every value into one secret as a JSON object, which an app reads and parses at boot:

seekrit sync enable --connection acme-gcp --provider gcp-secret-manager \
  --layout json-bundle --secret-name prod-storefront-env \
  --app storefront --env production --acknowledge-decryption

Prefer the bundle when your runtime reads secrets as a group — Secret Manager bills per active version, so fifty names cost fifty times as much stored separately. Prefer one-per-name when consumers mount them individually (gcloud run deploy --set-secrets, the GKE CSI driver) or you want per-secret IAM.

note

A prefix here is not a path. A Secret Manager ID takes letters, digits, hyphens, and underscores — no slashes and no dots — so the namespace is spelled prod-storefront-DB_URL, not prod/storefront/DB_URL. A name that would break that rule is reported as a failure against that name alone; the rest of the environment still pushes. IDs stop at 255 characters, and values at 64 KiB.

Versions, and what they cost

Secret Manager has no "set the value" call — addVersion appends, and every active version is billed for as long as it stays active. A sync run pushes the whole environment rather than a diff, so writing unconditionally would leave a new version on all fifty secrets every time one of them changed.

seekrit therefore writes a version only when the value changed. It reads the secret's metadata first and compares a keyed digest it keeps in the secret's own annotations (seekrit-digest), so an unchanged secret costs one read and no version. The digest is an HMAC keyed by your service-account key, not a plain hash of the value — metadata is readable by anyone with secretmanager.secrets.get, and a bare hash of a short value would be a guessing oracle.

The digest is written after the version it describes lands, never before. The other order would risk recording a digest for a value that failed to write, which would make the next run skip a secret that isn't there — silence being the one failure mode a secrets manager must not have. This order's worst case is one duplicate version.

Two consequences worth knowing:

  • Rotating the service-account key changes every digest, so the run after a rotation writes one new version per secret. Nothing after it does.
  • A version you add by hand stays live until seekrit's own copy of that value changes. seekrit compares its own record, not your project's contents; it never reads a value back.

--gcp-prune-versions closes the loop on the bill: with it on, the version each push supersedes is destroyed as soon as the new one lands, so a secret keeps exactly one active version.

caution

Both destructive paths here are permanent. Secret Manager has no recovery window like AWS's 30-day scheduled deletion: removing a secret from the environment (with onDelete: delete) deletes the GCP secret and every version of it immediately, and --gcp-prune-versions destroys the superseded version outright.

Pruning only ever touches a version seekrit wrote and recorded itself — never one added by anyone else — but it does mean you cannot roll a value back inside GCP. Roll it back in seekrit instead, which is the source of truth, and the next run pushes it.

Replication and CMEK

Replication is set when a secret is created and cannot be changed afterwards. automatic lets Google choose the locations and bills one replica; --gcp-replication user-managed --gcp-locations us-east1,europe-west4 names the regions yourself, which is how data residency is stated, and bills each one. Changing your mind means deleting the secret and letting the next run recreate it.

--gcp-kms-key encrypts with a customer-managed key, as its full resource name (projects/…/locations/…/keyRings/…/cryptoKeys/… — the only form the API accepts). KMS keys are regional and Secret Manager sets the key per replica, so seekrit accepts one key with automatic replication (where it must be a global key) or with a single location — several regions would each need their own, which a binding has no way to express. A binding that asks for both is refused when you create it rather than at the first run.

Secrets seekrit creates are labelled managed-by: seekrit, so gcloud secrets list --filter="labels.managed-by=seekrit" finds them. A secret that already existed keeps its own labels.

note

Global secrets only. seekrit writes through secretmanager.googleapis.com, not the per-location secretmanager.<location>.rep.googleapis.com endpoints that GCP's regional secrets live behind. Use user-managed replication for residency.

How a push behaves

  • One read per secret, then a write only if the value changed. An unchanged environment costs one GET per name and nothing else.
  • A run is capped at 400 requests, sized against Secret Manager's quota of 600 write requests a minute per project — shared with everything else in it — rather than the Worker subrequest cap.
  • A RESOURCE_EXHAUSTED waits for the quota window to roll, not the few seconds a token bucket needs. Any Retry-After Google sends wins, clamped.

Troubleshooting

SymptomCauseFix
PERMISSION_DENIED on the whole runThe service account lacks a permission, or the role was granted per secret and a new name has no grantGrant roles/secretmanager.admin at project level — see above for why per-secret grants break new names
PERMISSION_DENIED naming an APISecret Manager isn't enabled on the projectgcloud services enable secretmanager.googleapis.com
Every secret got a new version in one runThe service-account key was rotated, so every digest changedExpected once. Nothing after that run rewrites unchanged values
A hand-added version is still liveseekrit compares its own record, not the project's contentsChange the value in seekrit — it is the source of truth — and the next run supersedes it
A deleted secret is gone for goodSecret Manager has no recovery windowUse --on-delete retain if you would rather clean up by hand
One name failed, rest landedThe mapped ID contains a slash or dot, is over 255 characters, or the value is over 64 KiBA prefix here is not a path — use hyphens
Enabling refused a KMS keyA customer-managed key with several user-managed locationsKMS keys are regional. Use automatic replication, or one location
RESOURCE_EXHAUSTED, run reported partialThe project's per-minute write quota, shared with everything else in itNothing to do — seekrit waits for the window and retries

See also