Skip to content

Secrets and outbound calls

A secret is a credential for a third-party API, such as a Stripe key, that we store for your app. Your code calls the API without the key. Our egress proxy adds it as a header to requests for the host you name, so the key stays out of your app, its logs and its versions.

Terminal window
jiayang secret set acme/hello STRIPE_KEY --host api.stripe.com --header Authorization --format "Bearer {value}"
Paste the value and press Enter. It stays hidden:
Read 1 line, 107 characters.
Stored STRIPE_KEY. We'll send it as Authorization on requests to api.stripe.com.

At a terminal, you paste the value at a prompt that doesn’t echo it, and it says how much it read, so you can tell a paste that came up short.

When the value was typed rather than pasted, runs over more than one line, or comes from a Windows terminal, which sends a paste as typing, it asks before storing it:

Paste the value and press Enter. It stays hidden:
Read 1 line, 107 characters.
If that's less than you pasted, it got cut short. Answer n and pipe the value in instead, which reads all of it: jiayang secret set ... < value.txt
Store it? [y/N]

Answer y to store it. The default is no, so pressing Enter alone stores nothing.

Otherwise the value is read from stdin, without a trailing newline, which works in CI:

Terminal window
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, but then it ends up 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. You can do the same in the app’s Secrets tab in the dashboard.

Leave the header out of your request. We add it:

const res = await fetch("https://api.stripe.com/v1/customers", {
method: "POST",
body: new URLSearchParams({ email: "someone@example.com" }),
});

This works the same in a Worker and in a container. A container’s HTTPS requests also go through the proxy, and the variables it needs to trust the proxy are set for you.

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 to http://api.stripe.com goes out without them;
  • doesn’t let a secret follow a redirect to another host;
  • refuses requests to our own hosts, with a 403 and Refused: apps can't send requests to Jiayang Cloud itself.;
  • adds the header X-Jiayang-Outbound: 1, which tells a custom domain on Jiayang Cloud to refuse the request. The API you call sees it too;
  • refuses every request, over http or https, while the app isn’t being served because its workspace is suspended or over a limit that stops it. The answer is a 403 with Refused: this app is stopped, so it can't send requests.

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, or the app stopping or starting again, reaches your app’s requests within that time.

Secret values are encrypted at rest. Once a value is set, nothing in the CLI, API or dashboard shows it again.

We keep the value out of your app, but the host you named receives it on every request, and your code reads that host’s responses. If the host echoes request headers, as a debugging endpoint does, or quotes the key in an error message, your code can read the key from the response. Anyone who can deploy the app can write code that calls such an endpoint.

Name only a host you trust with the key. Assume everyone who can deploy the app can use the key, and, through a host like that, read it. If you think a key has been read, rotate it with the provider and set the new value.

Terminal window
jiayang secret list acme/hello
SENDGRID_KEY Authorization → api.sendgrid.com
STRIPE_KEY Authorization → api.stripe.com

The list shows each secret’s name, header and host, without its value.

Terminal window
jiayang secret remove acme/hello SENDGRID_KEY
Removed 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 is recorded in the audit log with the secret’s name and host, without its value.

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, and not one of ours or a name under .internal, .local or .localhost, which the proxy never sends to. 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.

jiayang secret set checks the name and the flags before it asks for the value, so you don’t paste a value only to have it refused. The errors for each:

error: Secret names are uppercase letters, digits and underscores, like STRIPE_KEY.
error: Secret values must be 1 to 8,192 bytes, with no control characters.
error: --host takes a hostname like api.stripe.com, with no scheme, port or path: --host api.stripe.com.
error: --header X Token isn't a header name: letters, digits and dashes, like Authorization.
error: --header Host can't carry a secret: HTTP or the platform sets it.
error: --format must contain {value} exactly once, like "Bearer {value}".

When --host is a URL, or has a port or a path, the error ends with its hostname, to use instead. With --json, the errors for the name and the flags have the code invalid_argument. The dashboard, the API and the agent tools say the same things in their own words.

Environment variables are plain text that your code reads and anyone who can deploy the app can list. For that reason, jiayang env set refuses values that are clearly credentials and points you here.

  • Database for the data your app stores.
  • Security for how we keep credentials out of your code.