carlos / platform Public

Is a console with or without CARLOS_RELEASE_KEY the expected default? Key-less has no usable promote path Idea open

Migrated from github.com/carlosframework/platform issue #85, opened 2026-08-07. GitHub issues are closed for this repo; see AGENTS.md, "Work is tracked on amadan, not GitHub".

Still outstanding: Decide whether a key-less console gets a graduated promote path, or document root-key-on-console as the expected default.

---

The question

Is a console with CARLOS_RELEASE_KEY the expected default for a deployment, or is a console without it?

The docs present it as a free choice — cmd/console/main.go says "optionally CARLOS_RELEASE_KEY (enables promote/rollback from the console; unset = read-only)". In practice the two configurations produce very different products, and only one of them has a usable developer experience. If key-less is meant to be a supported posture, the UX gap below needs closing. If it is not, the docs should say so plainly, because choosing it is currently a trap.

What happened

Setting up a self-hosted deployment, we left CARLOS_RELEASE_KEY unset on the console deliberately — the reasoning being that the deployment's root signing key should not sit on a box, since a host only needs the PUBLIC half to verify what it adopts. That reasoning still seems sound. The console announced itself honestly at boot:

console: CARLOS_RELEASE_KEY unset — promotions disabled, running read-only

Everything worked — auth login, apps create, env set, ship — right up to the first promote, which is where the deployment stopped being usable from a laptop.

Why key-less is currently a bad experience

Promotion moves a signed channel pointer, so a key-less console cannot do it. The only remaining path is direct-bucket mode, and that is a much bigger step than "one command needs different flags":

  1. It abandons the console entirely. apimode.go:71 — if CARLOS_DEPLOYMENT_BUCKET is set, the login is never consulted. So the operator goes from "talking to my console" to "poking the S3 bucket directly" for one verb in the middle of an otherwise console-driven workflow. Our user's reaction: "so in order for me to promote an app, I have to bypass the console that I've been talking to previously, and now suddenly be directly poking information into the S3 bucket?"
  1. It requires the operator to hold BOTH the signing key and the object store credentials on their laptop — strictly more secret material in a less controlled place than the box we were trying to protect. The posture arguably gets worse, not better.
  1. It contradicts the stated intent of direct mode. The README says direct-bucket mode is for CI and operators — "logging in is for a human at a laptop, never a box or a pipeline". Key-less forces the human at a laptop onto the pipeline path.
  1. The error does not name the real fix. no signing key on this host — the console is running read-only describes the console's state accurately, but says nothing about what the operator should do. Neither option (set the key on the console; or switch to direct mode and here is the full env you need) is mentioned.
  1. The obvious middle path is closed by design. A scoped key (release-keygen -scope canary,edge,beta) would let the console promote the lower rungs without holding the root key — exactly the delegation the scoped-key feature exists for. cmd/console/main.go:214 refuses it: "the console promotes with the deployment's ROOT key or runs read-only with none". That is a defensible rule, but it means key-less consoles have no graduated option at all: root key, or nothing.

What we would like decided

If key-on-console is the expected default: say so in the README's console section and in cmd/console/main.go, and consider making the boot line say that promotions are disabled and how to enable them. Right now the word "optionally" reads as "either is fine".

If key-less is a supported posture, at least one of these seems needed:

  • Let the console promote the lower rungs with a scoped key (canary, edge, beta), keeping stable for the root key plus the existing passkey step-up. This is the graduated option the scoped-key design already describes, and it would make key-less genuinely workable.
  • Or let the CLI sign locally and post the signed pointer through the console, so CARLOS_RELEASE_KEY in the operator's shell works in API mode. The console would verify against CARLOS_RELEASE_PUBKEY and store it — no key on the box, no bucket credentials on the laptop.
  • At minimum, make the refusal actionable: name both fixes and list the exact env vars direct mode needs, so the operator is not assembling CARLOS_DEPLOYMENT_BUCKET + CARLOS_S3_ENDPOINT + AWS_REGION + AWS_SHARED_CREDENTIALS_FILE + CARLOS_RELEASE_PUBKEY from source.

The second option looks strongest to us: it preserves the property that motivated key-less in the first place (no signing authority on a host) while keeping the operator on the console path for every verb.

Resolution here

We put the root key on the console. Not because the security argument was wrong, but because on a single-box deployment the same machine already runs the edge, the console and every tenant — so the marginal exposure is small, and the usability difference is large. That reasoning does not transfer to a multi-host fleet, which is exactly why the default deserves stating.

Related

Same first-run surface as #79, #81 and #83.