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 writes | One secret per name, or all of them as one JSON secret |
| Addressed by | An optional ID prefix, or the one bundle secret's ID |
| Connection carries | The service-account key JSON, and the project to write |
| IAM | roles/secretmanager.admin on the project — never versions.access |
| Takes effect | Next read by your app |
| Deletion | Immediate 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.
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.
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.
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.
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
GETper 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_EXHAUSTEDwaits for the quota window to roll, not the few seconds a token bucket needs. AnyRetry-AfterGoogle sends wins, clamped.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
PERMISSION_DENIED on the whole run | The service account lacks a permission, or the role was granted per secret and a new name has no grant | Grant roles/secretmanager.admin at project level — see above for why per-secret grants break new names |
PERMISSION_DENIED naming an API | Secret Manager isn't enabled on the project | gcloud services enable secretmanager.googleapis.com |
| Every secret got a new version in one run | The service-account key was rotated, so every digest changed | Expected once. Nothing after that run rewrites unchanged values |
| A hand-added version is still live | seekrit compares its own record, not the project's contents | Change the value in seekrit — it is the source of truth — and the next run supersedes it |
| A deleted secret is gone for good | Secret Manager has no recovery window | Use --on-delete retain if you would rather clean up by hand |
| One name failed, rest landed | The mapped ID contains a slash or dot, is over 255 characters, or the value is over 64 KiB | A prefix here is not a path — use hyphens |
| Enabling refused a KMS key | A customer-managed key with several user-managed locations | KMS keys are regional. Use automatic replication, or one location |
RESOURCE_EXHAUSTED, run reported partial | The project's per-minute write quota, shared with everything else in it | Nothing to do — seekrit waits for the window and retries |
See also
- Third-party sync — the shared model, naming and filtering, deletions
- Managed keys (KMS) — seekrit's own client-side managed keys
- CLI reference — every
seekrit syncflag