Osy#betaRun it on Osyrin: osyrin.com

Reference / Zones

Setting a secret from a page — the value goes from the browser to each zone

Secrets.SendToZones(destinations, fromField: "secret-value") · Secrets.ClearInZones(destinations)

An application's secret is sealed in each zone that runs it, under a short-lived grant the control plane signs. From a page, `Secrets.SendToZones` reads the value out of an UNBOUND password input, posts it straight from the browser to each zone's gateway, empties the input, and answers one receipt per zone. The value is never an Osy# value, so it never reaches the control plane — not in a request, a continuation, or a trace.

preview1 example compiled by CIzonessecretssecurityui

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
destinationsthe 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 answera 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 delete

Description#

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:

checkwhy
https, or plain http only to localhosta 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 URLnothing rides alongside the value
a signed grant is presentthe 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#

Related

declaring secrets (app.Secrets)

`app.Secrets` declares the named secrets your app uses — API keys, tokens, client secrets. Each is `new…

Giving a secret its value

Declaring `app.Secrets` names a secret; it does not give it a value, and an app cannot run until something does. `osy…

Deploying an application to a cloud platform — the zone builds it

A cloud deploy hands over the application's bundle — its app blob and the package archives it pins — to the zone the…

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…