# 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`](/docs/guides/run) or
[an SDK](/docs/guides/sdks) 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](/docs/guides/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:

```bash
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:

```bash
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:

```bash
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:

```bash
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.

> **Warning:** **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

| 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](/docs/guides/third-party-sync) — the shared model, naming and filtering, deletions
- [Managed keys (KMS)](/docs/guides/kms) — seekrit's own client-side managed keys
- [CLI reference](/docs/reference/cli#third-party-sync) — every `seekrit sync` flag
