Summary#
A secret's value is sealed by the zone that runs the application, with that zone's own key ring — a value the control plane sealed is one the zone cannot open. So setting one is two steps. First the control plane issues a plan: one signed grant per zone, naming the application, the secret and the zone, and nothing else. Then the value is posted to each zone under its grant.
On the command line osy secret set does both. On a page, the second step is Secrets.SendToZones. It is built so
that the value never becomes something the program holds:
| what it is | |
|---|---|
destinations | the plan's List<SecretSetDestination> — where to post, and the signed grant for each zone |
fromField: | the name of a password input on the page, which must have no value: binding |
| the answer | a List<SecretSetResult> — one receipt per zone: Zone, Environment, Grant, Accepted, Code, Message |
Signature#
List<SecretSetResult> results = Secrets.SendToZones(plan.Destinations, fromField: "secret-value");
List<SecretSetResult> cleared = Secrets.ClearInZones(plan.Destinations); // sends an empty value — a zone's store has no deleteDescription#
A page that sets one secret#
List<SecretSetDestination> PlanFor(string secret) {
return new List<SecretSetDestination>(); // your control plane's plan — the Admin portal calls RequestSecretSet
}
void Confirm(Guid grant) {
Log.Information("confirmed {Grant}", grant);
}
[Route("/set-secret")]
[AllowAnonymous]
component SetSecret() {
string notice = "";
action Set() {
var results = Secrets.SendToZones(PlanFor("Stripe"), fromField: "secret-value");
foreach (var r in results) {
if (r.Accepted) { Confirm(r.Grant); }
else { notice = notice + r.Environment + " (zone " + r.Zone + "): " + r.Message + " "; }
}
}
render {
Input(type: "password", name: "secret-value"); // NO `value:` — the verb reads it, posts it, and empties it
Button("Set", onPress: Set);
Text(notice);
}
}Why the value is a field NAME and not an argument#
When an action awaits a server function — Confirm above — the page hands the server a continuation: the action's
locals, so the server can resume it. A trace records the same locals. If the value were an argument, or a local, or a
bound component field, it would ride that continuation to the control plane that issued the grant, which is exactly
what the zone route exists to prevent. Naming the input keeps the value in two places only: the input, and the request
to each zone.
So the verb refuses a bound input (Input(value: draft, …)), before it sends anything, with a message saying why:
a binding has already copied every keystroke into component state. And the compiler refuses a value handed to it
directly — Secrets.SendToZones(destinations, "sk_live_…") does not compile.
What is checked before anything is sent#
Every destination is checked first, and one bad destination sends nothing, to any zone:
| check | why |
|---|---|
https, or plain http only to localhost | a secret goes to a zone over TLS; a local platform is the one exception |
the path is exactly /_osyrin/zones/{Zone}/secrets/{Grant} | a destination cannot aim one zone's grant — or the value — at another route |
| no query, fragment or credentials in the URL | nothing rides alongside the value |
| a signed grant is present | the zone admits nothing without one |
⚠ A browser cannot prove that the host is really that zone's gateway: the only statement of which host serves a zone is the plan itself. The zone, for its part, verifies the grant's signature, its path, its zone and its expiry, and seals into the application the grant names — nothing in the request can choose another.
The request carries no cookies and no credentials, and the zone's secret route answers CORS for that one route only:
any origin, POST, and the grant header — never credentials.
Reading the receipts#
A zone that refuses is a receipt, not an exception: Accepted is false and Code/Message are the zone's own
words — SECRET_NOT_DECLARED when the running version does not declare that secret, GRANT_REFUSED, GRANT_USED.
A zone the browser could not reach answers UNREACHABLE. Confirm each accepted grant (ConfirmSecretSet in the Admin
portal) so the console can say where the secret is set, and name every zone that did not take it — a secret set in two
of three zones is the state nobody can reason about later.
A page that is wrong — no input by that name, two of them, a bound one, a bad destination — throws an
InvalidOperationException and sends nothing.
In the Admin portal#
An application's Secrets tab lists the secrets its deploys declare and, for whoever may set them, where each is set and by whom. Set or replace opens the password field; Clear sends an empty value, which the application reads exactly as a secret nobody set. Setting a secret is for an owner or admin of the organization, or a member of the application who is not a Developer.
Beside it, the Config tab shows the application's app.Config values per environment and the hosts its packages
are granted — read-only, each saying where in the repository it is set (per-environment config (app.Config)).
See also#
- declaring secrets (app.Secrets) — declaring a secret
- Giving a secret its value —
osy secret set, the same flow from a terminal - Deploying an application to a cloud platform — the zone builds it — the other thing a zone accepts under a signed grant
- component — actions, and where a client verb is called from