Secrets and outbound calls
A secret is a credential for someone else’s API, such as a Stripe key, that the platform keeps for your app. Your code calls the API without it, and the platform’s egress proxy adds it as a header to requests for the one host you name, so the key never enters your app, its logs or its versions.
Store one
Section titled “Store one”jiayang secret set acme/hello STRIPE_KEY --host api.stripe.com --header Authorization --format "Bearer {value}"Paste the value, then press Enter. It won't be shown:Stored STRIPE_KEY. It's added as Authorization on requests to api.stripe.com.At a terminal, the value is read at a prompt that doesn’t echo it. Anything else is read from stdin, less a trailing newline, which suits CI:
printf '%s' "$STRIPE_KEY" | jiayang secret set acme/hello STRIPE_KEY --host api.stripe.com --header Authorization --format "Bearer {value}"--value takes it on the command line instead, where it lands in your shell history.
| Option | What it does |
|---|---|
--host |
The one host that gets the header, matched exactly: api.stripe.com is not stripe.com and not files.stripe.com. |
--header |
The header that carries it, such as Authorization or X-Api-Key. |
--format |
How the header’s value is built, with {value} exactly once. The default is the value on its own. |
Setting a secret that already exists replaces its value, host, header and format. The app’s Secrets tab in the dashboard does the same.
Call the API without the key
Section titled “Call the API without the key”Leave the header out. The platform adds it on the way out:
const res = await fetch("https://api.stripe.com/v1/customers", { method: "POST", body: new URLSearchParams({ email: "someone@example.com" }),});import requests
res = requests.post("https://api.stripe.com/v1/customers", data={"email": "someone@example.com"})form := url.Values{"email": {"someone@example.com"}}res, err := http.PostForm("https://api.stripe.com/v1/customers", form)let res = reqwest::Client::new() .post("https://api.stripe.com/v1/customers") .form(&[("email", "someone@example.com")]) .send() .await?;This works the same in a Worker and in a container. A container’s HTTPS goes through the proxy too, and the variables it needs to trust it are set for you.
How the header is added
Section titled “How the header is added”Every outbound request from your app goes through the egress proxy. For each request it:
- adds the secrets for that request’s host, each in its header, replacing a header of the same name your code set;
- adds them only over
https, so a request tohttp://api.stripe.comgoes out without them; - doesn’t let a secret follow a redirect to another host;
- refuses requests to the platform’s own hosts, with a 403 and
egress to platform hosts is blocked.
Several secrets can go to one host, each in its own header.
The proxy keeps what it looked up for 10 seconds, so a change to a secret reaches your app’s requests within that time.
Secret values are encrypted where they’re stored. No command, API call or dashboard page shows a value again once it’s set.
What the host can hand back
Section titled “What the host can hand back”The platform keeps the value out of your app, but the host you named gets it on every request, and what that host sends back reaches your code. If it echoes request headers in a response, as a debugging endpoint does, or quotes the key in an error message, your code can read the value there. Anyone who can deploy the app can write code that calls such an endpoint.
So name only a host you trust with the key, and treat everyone who can deploy the app as able to use it and, through a host like that, to read it. If you think a key has been read, rotate it with the provider and set the new value.
List and remove
Section titled “List and remove”jiayang secret list acme/helloSENDGRID_KEY Authorization → api.sendgrid.comSTRIPE_KEY Authorization → api.stripe.comThe list shows each secret’s name, header and host, never its value.
jiayang secret rm acme/hello SENDGRID_KEYRemoved SENDGRID_KEY.Within 10 seconds, requests to that host go out without the header.
Setting, listing and removing secrets takes an editor or owner of the app, or a workspace admin or owner. Each change goes in the audit log with the secret’s name and host, never its value.
Rules and errors
Section titled “Rules and errors”| Name | Uppercase letters, digits and underscores, starting with a letter, at most 64 characters. |
| Value | 1 to 8,192 bytes, with no control characters. |
| Host | A hostname with at least one dot. It’s stored in lowercase. |
| Header | Letters, digits and dashes, at most 64 characters. Not Host, Content-Length, Transfer-Encoding, Connection, Upgrade, Cookie, TE, Trailer, Keep-Alive, Proxy-Authorization, or any X-Jiayang-* or CF-* header. |
| Format | At most 128 characters, with {value} exactly once. |
What each refusal says:
error: Secret names are uppercase letters, digits and underscores, like STRIPE_KEY.error: Secret values are 1 to 8192 bytes with no control characters.error: inject_host must be a hostname like api.stripe.com.error: That header can't carry a secret.error: inject_format must contain {value} exactly once, like "Bearer {value}".Why not an environment variable
Section titled “Why not an environment variable”Environment variables are plain text that your code reads and anyone who can deploy the app can list. jiayang env set refuses values that are clearly credentials for that reason, and points you here.