A public URL that is still a secret

Serve encrypted JSON at a public URL. Anything that speaks HTTP can read it, and values stay ciphertext.

How a public URL stays safe

One environment, served as encrypted JSON at a public URL with a plain GET. The consumer brings only the workspace passphrase or the raw key.

Full definition

Some consumers cannot install a CLI or an SDK: a shell script, a language with no client, an appliance, a build step in a toolchain you do not control. A virtual environment serves one environment as a plain GET at a public URL, returning a JSON bundle that carries the encrypted values plus everything needed to decrypt them apart from the secret itself. The consumer brings only the workspace passphrase or the raw key. It is opt-in twice over, it refuses to touch a private product, and it never carries a plaintext value.

  • Before anything is served

    2 opt-ins

    The workspace flag and the per-environment flag must both be on. Turning the workspace flag back off unexposes every exposed environment in one write and tells you how many. An environment inside a private product can never be exposed at all.

  • Every value in the bundle

    ciphertext

    Values are the same AES-256-GCM ciphertext the server stores, and the feed never carries the workspace key or the passphrase. Everything around them is readable: variable names, the slugs in the URL, and the version label and note. Treat those as public and keep secrets out of a version note.

  • For every failure mode

    one 404

    A wrong slug, an unexposed environment, a disabled workspace flag and a private product all answer with the identical message, so probing the URL tells an attacker nothing. The single exception is an exposed environment that has never been published, which says so plainly.

  • Versions served

    published only

    The feed serves published versions, so what a consumer reads is a snapshot you deliberately cut, not whatever someone last typed into the dashboard. Pin a version id for reproducible builds or use latest to always track the newest.

  • The bundle explains itself

    self-describing

    Tagged envless.encrypted-export/v1 and carrying the KDF parameters, the derived salt, the cipher spec and the ciphertext format, plus each variable's type, visibility and required flag. A consumer needs no lookup and no documentation to decrypt.

  • Owners and admins, on exposure

    notified

    Flipping the toggle emails every owner and admin in the workspace, minus whoever did it, and emits an environment.exposed event to your webhooks with environment.unexposed on the way back. Nobody can quietly publish a feed.

Exposing an environment

  1. 1

    Make sure the passphrase can carry the weight

    This is the first step, not the last. Once a feed is public, the response alone is enough to start an offline guessing attack, so the passphrase stops being a convenience and becomes the entire perimeter. If yours was chosen by a human rather than generated, rotate it before you expose anything.

    envless passphrase set
  2. 2

    Turn the feature on for the workspace

    Off by default, and it takes the workspace:update permission, which owners and admins hold. Turning it off later is not just a flag flip: the same write unexposes every exposed environment in the workspace.

    envless workspace virtual-envs
    
    # and to reverse it
    envless workspace virtual-envs --off
  3. 3

    Expose the one environment you mean

    Opt-in per environment, never all-or-nothing. It takes the environments:update permission, and it is refused outright for any environment inside a private product.

    envless env expose preview
    
    # make it private again
    envless env expose preview --off
  4. 4

    Read it with nothing installed

    A plain GET with no Authorization header. The path is built from your hierarchy, so it reads like a URL rather than an opaque token, and a separate listing endpoint returns version metadata with no ciphertext at all.

    curl https://api.envless.cloud/exposed/<workspace>/<product>/<project>/<environment>/versions/latest
    
    # a specific pinned version, by uuid or label
    curl https://api.envless.cloud/exposed/<workspace>/<product>/<project>/<environment>/versions/v13
    
    # metadata only, no ciphertext
    curl https://api.envless.cloud/exposed/<workspace>/<product>/<project>/<environment>/versions
  5. 5

    Decrypt with the passphrase and nothing else

    The bundle hands you the KDF parameters and the salt, so a consumer derives the key and decrypts each value with a standard library. On an unattended machine prefer the raw exported key, which skips derivation entirely.

    const bundle = await (await fetch(url)).json()
    
    // bundle.decryption.kdf -> { algorithm, hash, iterations, keyLength, salt }
    // bundle.decryption.cipher, bundle.decryption.ciphertextFormat
    // bundle.variables -> [{ name, value, type, visibility, required }]
    
    const key = pbkdf2(passphrase, base64(bundle.decryption.kdf.salt), bundle.decryption.kdf.iterations)

Virtual environments FAQ

It would be, if the URL served secrets. It serves ciphertext, the same bytes the server itself stores, and the server cannot read those either. What changes compared to the rest of the product is the perimeter: everywhere else an attacker needs a credential before they can even fetch the ciphertext, whereas here they can fetch it and are left facing the encryption alone. That is a real trade and the reason the feature is off by default and needs two separate switches.

The workspace passphrase, and only that. Anyone can download the bundle, and the bundle deliberately includes the KDF parameters and salt so that legitimate consumers need no lookup. The direct consequence is that the response is enough to start an offline guessing attack. A generated passphrase of real length is fine against that; a human-chosen one is not. Rotate before you expose.

Because a salt is not secret, and withholding it would only break honest consumers. A salt exists to stop one precomputed table attacking many workspaces at once, and ours is derived per workspace, which achieves that whether or not it is published. Serving it is what lets a script decrypt with nothing but the passphrase and the response body.

No, and the refusal is deliberate rather than incidental. Privacy on the product wins over the virtual-env feature, so the write is rejected with a clear message. If you want a feed for work that lives in a private product, move that environment into a product that is not private and re-check who can reach it.

Yes, if they are an owner or an admin. Exposure emails every owner and admin in the workspace apart from the person who did it, and it emits environment.exposed to your webhook endpoints, with environment.unexposed when it is turned back off. Ordinary members are not emailed, so if you want everyone to see it, the webhook is the surface to wire up.

A JSON object tagged envless.encrypted-export/v1 containing the workspace, product, project and environment slugs, the version id, label, note and variable count, a decryption block with the KDF parameters, salt, cipher and ciphertext format, and the variables themselves with name, encrypted value, type, visibility and required flag. Everything needed to decrypt except the secret. Note what that list implies: only the values are encrypted, so variable names and your version note are readable by anyone who fetches the URL.

Yes, and prefer the version uuid over the label if you do. Labels are derived from a per-environment count at publish time, so v13 describes a position rather than a fixed snapshot. Use latest when you always want current values, and the uuid when you want the same bytes every time.

Yes. Claim the hostname, add the DNS records Envless prints for it, and verify it. Envless provisions the certificate, and once the domain is active the feed is served on your own hostname. The workspace is read from the host, so its slug drops out of the path and the URL you hand out gets shorter: https://vars.acme.com/exposed/watch/website/production/versions/latest instead of the Envless one, which keeps answering alongside it. A custom domain needs a paid plan: Dev, Team and Enterprise each hold one per workspace.