Docs
Guidesince cloud@2026-09-10

External Secrets Operator

Point the webhook provider at one penv route and it reads a whole environment in a single request, with one audit entry per value.

Not verified yet

This page is an outline. Nothing here has been confirmed against source, so do not follow it as product behavior.

External Secrets Operator already fetches from a URL, so penv gives it one. Its webhook provider reads a whole environment in a single request and writes a Kubernetes Secret, and your pods read that Secret the way they read any other.

This is why penv ships no Kubernetes sync adapter of its own. A destination adapter would be a second implementation of something that already works.

The field names in the operator's own ClusterSecretStore manifest are not in this repository, so the manifest below is not reproduced here. Take the provider's schema from the External Secrets Operator documentation for the version you run, and map it onto the request and response described below. This page stays needs-source until that mapping is checked against a running operator.

The route

GET /api/v1/bulk-read-secrets

One method and one address. There is no request body and no query string, because the scope comes entirely from the credential: the workspace, the project and the environment are all read off it.

PartValue
MethodGET
HeaderAuthorization: Bearer pck_...
Permissionsecret:reveal
BodyNone

The permission is deliberately the stronger one. This route hands over a whole environment at once, so it is gated on secret:reveal rather than the secret:read that lists names.

The response

{
  "values": {
    "DATABASE_URL": "...",
    "redis/password": "..."
  },
  "skipped": ["stripe/rotating-key"]
}

values is a flat object. A key at the root of the environment is its bare name, and a key under a path is path/name. That naming is the same one an export sync uses, so a value reaches Kubernetes and a platform's own store under the same name.

skipped is present only when it is not empty. Two things land in it.

In skippedWhy
A dynamic addressIt mints on request and holds no stored value to return.
A key with no version yetIt was created and never written. Absent rather than empty.

Read skipped in your own tooling. A key your pod needs that arrives in skipped is a key the Secret will not carry, and a missing environment variable at boot is a harder failure to read than a refusal here.

Every value is one audit entry

A bulk read is a lot of reads rather than a different kind of event, and the record says so. Each value goes through the same read path a single fetch uses, so each one lands a secret.read entry carrying the credential that opened it. See the record of who did what.

Set the operator's refresh interval with that in mind. An interval of one minute over forty keys is forty entries a minute, which is a lot of log for very little news.

Refusals

StatuserrorWhat happened
401unauthorizedThe credential is not one penv knows.
401expiredThe credential was valid and its expiry has passed.
403forbiddenThe credential's role does not carry secret:reveal.
429rate_limitedA ceiling was reached. The response carries retry-after in seconds.

Two ceilings apply, and the first one is checked before the credential is even read. A client IP is held to 1000 requests a minute, then the identity is held to a bucket set by your plan. A bulk read is admitted at a cost of one and then charged one token per value actually opened, so a dynamic address costs nothing.

Give the operator a credential

Issue a pck_ machine credential in the console, bound to the project and the environment the operator should read, with a role carrying secret:reveal. Put it in a Kubernetes Secret and reference it from the store, the way you would any other provider credential.

A machine credential always carries an expiry. Where you would rather not rotate one by hand, give the operator's own service account a trust instead and exchange its projected token, as on Kubernetes.

Do it in order

  1. Create a machine identity in the console bound to the project and environment the operator reads.
  2. Give it a role carrying secret:reveal, then issue a credential.
  3. Store that credential in a Kubernetes Secret in the operator's namespace.
  4. Configure the webhook provider against GET /api/v1/bulk-read-secrets, reading the credential from that Secret into the Authorization header.
  5. Map the response's values object onto the keys your ExternalSecret asks for.
  6. Set a refresh interval you are willing to read in the audit log, then apply and check the first sync.

The record of who did what