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
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.
| Part | Value |
|---|---|
| Method | GET |
| Header | Authorization: Bearer pck_... |
| Permission | secret:reveal |
| Body | None |
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 skipped | Why |
|---|---|
| A dynamic address | It mints on request and holds no stored value to return. |
| A key with no version yet | It 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
| Status | error | What happened |
|---|---|---|
401 | unauthorized | The credential is not one penv knows. |
401 | expired | The credential was valid and its expiry has passed. |
403 | forbidden | The credential's role does not carry secret:reveal. |
429 | rate_limited | A 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
- Create a machine identity in the console bound to the project and environment the operator reads.
- Give it a role carrying
secret:reveal, then issue a credential. - Store that credential in a Kubernetes
Secretin the operator's namespace. - Configure the webhook provider against
GET /api/v1/bulk-read-secrets, reading the credential from thatSecretinto theAuthorizationheader. - Map the response's
valuesobject onto the keys yourExternalSecretasks for. - Set a refresh interval you are willing to read in the audit log, then apply and check the first sync.
Serverless functions
A function is your code from the first line, so values arrive through the platform's own store, and a sync writes them on Lambda, Cloud Run, Workers, Vercel and Deno Deploy.
Upgrade from 0.16
penv 1.0 is a new binary with one committed schema, so this page names what each 0.16 part became and the one path that gets your values across.