A public URL that is still a secret

Serve an environment as a self-describing encrypted JSON export from a public URL. Anything that can make an HTTP request can consume it, and the values never stop being ciphertext.

What a virtual environment is

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.

How a public URL stays safe

The feed trades an API key for encryption as the only perimeter, so every part of that perimeter is worth stating exactly.

2 opt-ins

Before anything is served

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.
ciphertext

Every value in the bundle

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.
one 404

For every failure mode

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.
published only

Versions served

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.
self-describing

The bundle explains itself

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.
notified

Owners and admins, on exposure

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

Two deliberate switches, then a URL anything can read. Do the passphrase work first, because it becomes the only thing protecting the data.

  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

Straight answers about how this works in practice.

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 workspace holds one custom domain, and custom domains need a paid plan.

Get Started

Ship secrets, not chaos.

Start free today and discover why developers trust Envless for end-to-end encrypted, versioned secrets across every environment.