Account

Step-up verification

A re-verification that stands in front of 43 actions, from revoking a key to turning off two factor authentication, in the dashboard and in the CLI.

A signed-in session is enough to read a workspace and to edit a variable. It is not enough to hand out access, destroy a credential, rewrite an environment in one shot, or touch the account itself. 43 actions ask you to prove, right then, that the person driving the session is still you. Envless calls that step-up verification, and it applies to a CLI token exactly as it applies to a browser session.

The proof is one code: an emailed code, or, if you have two factor authentication turned on, a code from your authenticator app. Whichever you use, it buys a one hour window in which the rest of the guarded actions run without asking again. The dialog takes six characters, so it does not accept a recovery code, use the emailed code if your phone is not to hand. The CLI prompt takes either.

What it guards

The list below is the complete one. Every action here is refused with 403 and auth.step_up_required unless a live elevation covers it.

Your account

Seven actions are scoped to the account rather than to a workspace, and they are the ones that change how you sign in or end the account outright.

ActionWhere
Turn two factor authentication onAccount
Turn two factor authentication offAccount
Regenerate your recovery codesAccount
Change your account emailAccount
Connect a sign-in providerAccount
Disconnect a sign-in providerAccount
Delete your accountAccount, Danger zone

API keys

ActionWhere
Create a keyKeys
Rotate a keyKey row menu
Delete a keyKey row menu
Deactivate a key, re-activate one, or change its scopes or project allowlistKey row menu and the edit modal

Renaming a key is the one key edit that is not guarded. The condition is on what the edit changes, not on which endpoint you call: active flipping in either direction, or a new scopes object, or a new projectIds list. A request that sends only name runs on a plain session.

Webhooks

ActionWhere
Create an endpointWebhooks
Edit an endpoint, including enable and disableEndpoint row menu
Delete an endpointEndpoint row menu
Rotate the signing secretEndpoint row menu
Reveal the signing secretSecret column

Members, roles and product access

ActionWhere
Invite a memberTeam
Change a member's roleTeam
Remove a memberTeam
Create a roleSettings, Roles
Edit a roleSettings, Roles
Delete a roleSettings, Roles
Grant a member access to a private productProduct settings, Access
Revoke a member's access to a private productProduct settings, Access

Products, projects and environments

ActionWhere
Update a product, including its name, slug, icon and privacy flagProduct settings
Delete a productProduct settings
Delete a projectProject settings
Delete an environmentEnvironment settings
Expose an environment, or retract itEnvironment settings
Publish an example link, or retract itEnvironment settings

Variables and versions

ActionWhere
Apply a raw .env editRaw editor, Apply changes
Bulk edit the type, secret flag, visibility or required flag of selected variablesSelection bar on Variables
Bulk delete selected variablesSelection bar on Variables
Roll an environment back to an earlier versionVersions

Single-variable work is not guarded. Creating, editing or deleting one variable, and importing a block through Add variables, all run on a plain session. What is guarded is the wholesale shape: a raw apply replaces the entire document in one request, a bulk action rewrites every selected row, and a rollback replaces the environment with an older snapshot.

Workspace and encryption

ActionWhere
Delete the workspaceSettings, Danger zone
Turn Virtual Envs on or offSettings, Virtual Envs
Claim a custom domainSettings, Virtual Envs
Remove a custom domainSettings, Virtual Envs
Require two factor authentication of every member, or stop requiring itSettings
Turn verification codes off for the workspaceSettings, Verification Codes. This one always asks for a new code, see below
Claim the passphrase rotation lockSettings, Encryption

Billing and the CLI

ActionWhere
Open the billing customer portalBilling
Approve a CLI device codeThe page envless login opens

Account scope and workspace scope

Every challenge carries a scope, and the scope decides what it unlocks.

ScopeIssued whenSatisfies
WorkspaceThe request names a workspace, which is every action above except the seven account onesGuarded actions in that workspace only
AccountThe request names no workspaceThe seven account actions, and any workspace check

The asymmetry is deliberate. An account elevation is the stronger proof, so it stands in for a workspace one; a workspace elevation does not reach the account. Switch workspaces inside the hour and the next guarded action in the new workspace asks again, unless what you hold is an account elevation.

Account-scoped actions used to be reachable a different way: account deletion checked every workspace you belonged to, and a user who belonged to none skipped the gate entirely. That path is gone. Deletion now needs a real account elevation whether you hold workspaces or not, and requesting one no longer depends on membership anywhere.

The two factors it accepts

Verification accepts a code from either factor, and the dialog decides which one it asks for first.

FactorAvailable toWhat the dialog does
Emailed codeEveryoneSends a code to the account address as soon as it opens
Authenticator appAccounts with two factor authentication onOpens on the app-code field and sends no email until you switch

With two factor authentication on, the dialog opens asking for the app code and offers the emailed code as the alternative underneath. Nothing is mailed unless you pick it, so the ordinary path costs no round trip through your inbox at all. Recovery codes work here too, and each one still burns on use.

Signing in is app-only; step-up is not. At sign-in the second factor has to be the authenticator app, because the first factor there is already a code mailed to the same address and accepting a second one would let a single mailbox pass both checks. Step-up runs against an already authenticated session, where we cannot assume the phone is within reach, so both factors are offered.

How it works

You trigger a guarded action

The API returns 403 with auth.step_up_required and the message This action needs a verification code to continue. rather than performing the action, and the dashboard opens the verification dialog.

You get a code

The emailed code is 6 characters drawn from a 32-symbol alphabet with the ambiguous I, O, 0 and 1 removed, so it reads cleanly. Only a SHA-256 hash of it is stored, and the comparison is constant-time. An app code is the usual 6 digits from your authenticator.

You enter it

An emailed code is valid for 10 minutes and allows 5 attempts. A sixth attempt is rejected outright even if the code is correct, so a wrong-code loop cannot be brute-forced. App codes carry their own limit: 10 accumulated failures lock the app path, the lock lifts 15 minutes after the last attempt, and a code that works clears the counter. Recovery codes are accepted throughout, including while the app path is locked, so a lost phone is never a dead end.

You get a one hour window

A successful verification elevates you for 60 minutes from the moment you verified. It does not slide forward as you work. Every guarded action in scope inside that window runs without another code, so rotating three keys is one round trip, not three.

The original call is retried

The dashboard, and the CLI, rerun the request that was refused, exactly once, as soon as verification succeeds. You do not click the action or run the command again.

Asking again is not throttled. Nothing caps how many codes you can request, so a code that never arrived can be asked for again immediately. Each request issues a fresh challenge and verification always checks the newest live one, which means an earlier code stops working the moment you ask for another. A request made while you are already elevated answers 200 with elevated: true, sent: false and sends nothing, so the dialog can ask without spending a code.

Passphrase rotation is gated at the lock

Rotating the workspace passphrase is guarded when you claim the rotation lock, not on the re-encrypt calls that follow it.

That is not an oversight. One rotation is an unbounded fan-out of re-encrypt requests inside a single held lock, sized by how many variables, versions and history entries the workspace holds. An elevation expiring partway through would abandon the run with the workspace split across two encryption keys. Gating the entry point instead means the check happens once, before anything is rewritten, and the run cannot stall on a code prompt. Releasing the lock is not guarded either, so a rotation you abandon can always be cleaned up.

What is not covered

The public API-key surface enforces no step-up at all. It mirrors most of the actions above, and a key with the right scopes performs them with no challenge, because an ev_sk_ key has no inbox and no authenticator to answer with. Step-up protects a hijacked browser session. It is not a second gate over the REST API, and scoping keys narrowly is still the only control there. See Authentication.

Some neighbours of guarded actions are deliberately open. Leaving a workspace yourself, revoking or resending an invite, resending a webhook delivery, denying a CLI device code, and revoking a CLI session token all run on a plain session. Note the last one in particular: deleting an API key asks for a code, revoking a CLI session does not.

The elevation is not bound to the credential that earned it. It is a row against your user and the scope, so any live session of yours inside the window benefits, a browser tab and a terminal alike. A CLI token can earn a workspace elevation: /internal/step-up/request and /internal/step-up/verify accept a CLI token principal when the request names a workspace. They still refuse one with auth.cli_token_not_accepted at account scope, because an account elevation would unlock the seven account actions and a terminal has no business changing your email address. Account deletion, two factor enrollment and CLI device approval refuse a CLI token outright as before.

From the CLI

Every guarded action the CLI can reach asks for the same code. That covers envless push, envless var rm, envless version restore, the delete subcommands of workspace, product, project and env, envless env expose, envless product update, envless workspace virtual-envs, envless domain claim and envless domain remove, the member, access and role write subcommands. When the server refuses with auth.step_up_required, the command pauses its spinner, requests a code, prompts for it, verifies it, and reruns the refused request once. One code covers the rest of the run and, like the dashboard, the next hour.

text
✔ Resolving billing
◇ Delete Billing? Its projects, environments and variables go with it.
│ Yes
› Enter the code we emailed you.
◆ Verification code
│ K7M2PX
└
› ✓ Verified. We will not ask again for an hour.
✔ Removed from Acme

With two factor authentication on, the prompt says so and takes an authenticator code or a recovery code in the same field; the server works out which one it got, and an emailed code still works. The prompt repeats on a wrong code up to five times, matching the server's attempt cap, and stops at once if the authenticator path is locked.

Nothing bypasses the code. --yes skips the CLI's own confirmation and nothing else. Without a terminal to type into, the command fails before a code is even requested, with This action needs a verification code. Run it from an interactive terminal, or do it from the dashboard. A CI job that pushes with a CLI token therefore fails cleanly rather than hanging. Either verify from the dashboard first, which elevates the same user for an hour, or turn verification codes off for that workspace, see below.

Turning it off for a workspace

A workspace can switch the guard off for itself, under Settings, Verification Codes. It is on for every workspace by default, and the verification dialog itself points members who hold workspace:update at the switch; everyone else sees only the code field. With it off, the 36 workspace-scoped actions above run on a signed-in session or a CLI token alone, in the dashboard and in the CLI, and auth.step_up_required is never answered for that workspace. The seven account-scoped actions are not affected; they are yours, not the workspace's, and keep asking.

Turning it off is itself guarded, and more strictly than anything else: it asks for a new code every time, whether or not you verified something a minute ago. The server answers auth.step_up_fresh_required instead of auth.step_up_required, the dialog opens on a fresh challenge, and the retry only passes if a code was verified within the last two minutes. An elevation you already hold does not count. Turning it back on needs the workspace:update permission and nothing else, because that is the safe direction.

Both directions notify. Every member of the workspace, the person who made the change included, gets an email naming who made it, and the audit log records a workspace.updated event with changes: ["requireStepUp"], which reaches any webhook subscribed to it. The email that announces the guard is off says what to do if nobody on the team meant it: turn it back on, review the team list, and rotate whatever the session that did it could reach.

Turning the guard off does not help a workspace whose owner has lost their inbox. Switching it off needs a fresh code, and without an authenticator that code arrives by email. Enroll an authenticator before that becomes a problem, not after.

Operational consequences

Without two factor authentication, whoever revokes a leaked key needs their inbox at that moment. Revocation is guarded, so an account whose email nobody can currently receive cannot revoke its own keys. Turning on two factor authentication removes that dependency, because an app code satisfies step-up on its own. Either way, do not leave production key ownership with a departed employee's account or a shared address nobody monitors.

If an emailed code does not arrive, check the spam folder for the account's own address rather than a team alias, then ask for another. Expired and consumed challenges are cleaned up on a schedule, so an old code never lingers as a valid credential.

Going deeper