# varsafe > Docs for varsafe, the secrets manager for teams that ship with agents: the CLI, the dashboard, agents over MCP, CI pipelines and the security model. # varsafe Source: https://docs.varsafe.dev/ varsafe keeps your application secrets out of plaintext files and injects them into your processes at runtime. One CLI command replaces scattered `.env` files across machines, teammates, and CI. varsafe dashboard ## Quick start ```bash # Install curl -fsSL https://varsafe.dev/install.sh | bash # Log in once varsafe login # Inject secrets and run your app — nothing is written to disk varsafe run -- npm run dev # Or export an encrypted .env when a file is required varsafe export -o .env ``` :::tip[Multiple projects?] Set your context with `varsafe use` — it is saved locally for subsequent commands. ::: ## What you get Inject secrets directly into your process environment — no files on disk. Export encrypted `.env` files when a file is required. Secrets are encrypted at rest (vault + KMS) and in transit (TLS), and injected only into the child process environment at runtime. Every access is recorded in an append-only audit trail. Know who accessed what, when, and from where. Every change is tracked. Preview diffs and roll back to any previous state. SAML 2.0, OIDC, passkeys, 2FA, and device trust. Connect Claude Code, Cursor, and other tools with scoped access and a full audit trail. A published OpenAPI spec, a per-operation API reference, and llms.txt for agents. ## Where to next Zero to injected secrets, in five steps. Teams, projects, environments, and how secrets are addressed. Every command and flag, generated from the binary. --- # varsafe API Source: https://docs.varsafe.dev/api varsafe stores environment variables — database URLs, API keys, signing secrets — encrypted at rest, and hands them to the process that needs them without writing a `.env` file to disk. ## When to use this API - Inject a project environment into a process or pipeline (`GET /secrets/inject`). - Read one secret value at the moment it is needed (`GET /secrets/value`), rather than caching a whole environment. - Create, update or delete secrets from automation (`POST /secrets`, `POST /secrets/bulk`). - Discover what a credential may do before acting (`GET /me/cli`, `GET /capabilities`). If you are an AI agent, prefer the Model Context Protocol endpoint at `POST /mcp` over these REST routes: it exposes the same capabilities as typed tools with per-tool scope enforcement and a consent step. See https://docs.varsafe.dev/guides/mcp. ## Authentication Read the `security` field on each operation — it is derived from what the guards and handlers actually accept, and it is not uniform. Most routes take an **API token** (`Authorization: Bearer …`), created self-serve in the dashboard and scopable to a project, an environment and read-only access. Routes carrying `x-varsafe-cli-scopes` also accept a **CLI grant** from the device authorization flow and enforce the listed scopes. Four routes are narrower: `GET /me/cli` and `POST /auth/cli/logout` answer only to a CLI grant, because both describe or revoke that grant itself; `POST /auth/cli/device/token` and `GET /auth/cli/device/events` authenticate with the device code rather than an account. `GET /me` accepts a session or a CLI grant but not an API token. The MCP endpoint at `POST /mcp` uses **OAuth 2.1**, or an API token, with the scope vocabulary in `components.securitySchemes.varsafeMcpOAuth`. ## Responses Every operation documents its success body with a JSON Schema under `components.schemas`, and every failure with the body the server actually sends: `ErrorResponse` (branch on its `code`) for REST routes, `OAuthErrorResponse` and `JsonRpcErrorResponse` for the MCP transport. The schemas are generated from the same Zod definitions the handlers are type-checked against and that the `@varsafe/shared` package exports. Objects list the properties sent today; a later release may add one, so ignore properties you do not recognise. Streaming routes (`text/event-stream`) describe their events in prose. ## Scope of this document This is the programmatic surface: the routes a token, a CLI grant or an agent may call. Dashboard-only and administrative routes are intentionally not described here. The same document is served at https://varsafe.dev/openapi.json and https://api.varsafe.dev/openapi.json. ## OAuthProviderWellKnown RFC 8414 and RFC 9728 discovery documents for the OAuth 2.1 flow. ## Me The identity and authority behind the calling credential. ## CliDeviceAuth Device authorization grant — how a CLI or an unattended agent obtains a credential. ## CliSession Lifecycle of a CLI grant, including self-revocation. ## Projects Projects, the top-level grouping that owns environments and secrets. ## Environments Environments within a project — development, staging, production, and any others. ## Health Liveness and readiness probes. ## Keypairs Per-environment keypairs used to encrypt values into a committable .env file. ## Secrets Reading, writing and resolving secrets. The core of the API. ## SecretComposition How secrets reference one another through ${KEY} templates. ## Capabilities What this deployment supports, so a client can adapt instead of probing. ## McpTransport Model Context Protocol endpoint over Streamable HTTP. ## PublicOpenApi This contract, as a machine-readable document. --- # Get server capabilities Source: https://docs.varsafe.dev/api/capabilities/capabilitiescontroller-get Reports the features and limits this deployment supports, so a client can adapt instead of probing endpoints and interpreting failures. --- # Stream device-approval events Source: https://docs.varsafe.dev/api/clideviceauth/clideviceauthcontroller-events Server-sent events that signal approval or denial of a pending device authorization, so a client can stop polling immediately instead of waiting for its next interval. --- # Start device authorization Source: https://docs.varsafe.dev/api/clideviceauth/clideviceauthcontroller-start Begins the device authorization grant. Returns a user code and a verification URL that a human opens in a browser to approve the credential, plus a device code to poll with. This is how a CLI or an unattended agent obtains a credential without a password. --- # Exchange a device code for a CLI credential Source: https://docs.varsafe.dev/api/clideviceauth/clideviceauthcontroller-token Polls the device authorization grant. Every answer is a 201 whose `status` says where the grant stands: `pending` (keep polling; `reason: slow_down` asks for a longer interval), `failed` (expired or denied — start again), or `granted` / `granted-machine`, which carries the credential exactly once. --- # Revoke the calling CLI credential Source: https://docs.varsafe.dev/api/clisession/clisessioncontroller-logout Revokes the CLI grant presented on the request. Self-service and immediate — it needs no scope because it can only ever destroy the caller’s own credential. --- # List environments in a project Source: https://docs.varsafe.dev/api/environments/environmentscontroller-list Lists the environments of one project — the second half of resolving a `project/environment` context before reading secrets. --- # Liveness check Source: https://docs.varsafe.dev/api/health/healthcontroller-live Returns 200 while the process is running. It does not check dependencies — use the readiness probe for that. --- # Readiness check Source: https://docs.varsafe.dev/api/health/healthcontroller-ready Reports whether the API and its dependencies (database, cache, vault) can serve traffic. Returns 503 when any dependency is unavailable. --- # Get an environment public key Source: https://docs.varsafe.dev/api/keypairs/keypairscontroller-get Returns the public half of the environment keypair, used to encrypt values into a committable encrypted .env file. The private half is never returned by this route. --- # MCP transport (end session) Source: https://docs.varsafe.dev/api/mcptransport/mcptransportcontroller-handledelete The Streamable HTTP session-teardown call. The transport is stateless — every request stands alone — so there is no server-side session to release and this acknowledges with an empty 200. --- # MCP transport (event stream) Source: https://docs.varsafe.dev/api/mcptransport/mcptransportcontroller-handleget Opens the server-to-client server-sent event stream. The transport is stateless, so the stream carries no session-scoped messages; clients that probe for it get a valid, idle stream. --- # MCP transport (JSON-RPC request) Source: https://docs.varsafe.dev/api/mcptransport/mcptransportcontroller-handlepost Model Context Protocol endpoint over Streamable HTTP. Send JSON-RPC requests here to list and call varsafe tools. Requires an OAuth 2.1 access token whose granted scopes cover the tool being called; an unauthenticated request answers 401 with a WWW-Authenticate header pointing at the protected-resource metadata. --- # Get the current identity Source: https://docs.varsafe.dev/api/me/mecontroller-me Returns the user, their teams and the active team for the calling credential. --- # Revoke the calling API token Source: https://docs.varsafe.dev/api/me/mecontroller-revokeowntoken Revokes the API token presented on this request. It takes no id, so a token can only ever destroy itself — the safe thing for an automation to call when it believes it has been compromised. Idempotent: revoking an already-revoked token still answers 200. --- # Verify the calling credential Source: https://docs.varsafe.dev/api/me/mecontroller-verify Confirms that the presented credential is valid and returns the identity behind it. The cheapest call to make first when diagnosing an authentication problem. --- # Get the CLI grant behind the credential Source: https://docs.varsafe.dev/api/me/mecontroller-whoamicli Returns what the person who approved this CLI credential consented to: the teams it may act on, the scopes it holds per team, and any project or environment restriction. This is what `varsafe whoami` prints. --- # OAuth 2.1 authorization server metadata Source: https://docs.varsafe.dev/api/oauthproviderwellknown/oauthproviderwellknowncontroller-authorizationserver RFC 8414 metadata describing the authorization and token endpoints, supported grant types and the scope vocabulary used by the MCP endpoint. --- # Protected resource metadata for the MCP endpoint Source: https://docs.varsafe.dev/api/oauthproviderwellknown/oauthproviderwellknowncontroller-protectedresourcemcp RFC 9728 metadata for https://api.varsafe.dev/mcp. MCP clients read this after a 401 to discover where to authorize. --- # Protected resource metadata Source: https://docs.varsafe.dev/api/oauthproviderwellknown/oauthproviderwellknowncontroller-protectedresourceroot RFC 9728 metadata naming the authorization server for this API and the scopes it supports. --- # List projects Source: https://docs.varsafe.dev/api/projects/projectscontroller-list Lists the projects visible to the credential across every team it has been granted. A CLI credential restricted to a subset of projects sees only that subset. --- # Get this OpenAPI document Source: https://docs.varsafe.dev/api/publicopenapi/publicopenapicontroller-get Returns this OpenAPI 3.1 document, byte for byte the same file published at https://varsafe.dev/openapi.json. Needs no credential and may be cached for five minutes. --- # Get the secret composition graph Source: https://docs.varsafe.dev/api/secretcomposition/secretcompositioncontroller-composition Returns which secrets reference which others through `${KEY}` templates, so a caller can see what a rotation will change. Returns no values. --- # Create a secret Source: https://docs.varsafe.dev/api/secrets/secretscontroller-create Creates one secret in a project environment. Fails rather than overwrites when the key already exists. --- # Create or update secrets in bulk Source: https://docs.varsafe.dev/api/secrets/secretscontroller-createbulk Writes many secrets to one environment in a single transaction. The intended path for importing an existing .env file. --- # Delete a secret Source: https://docs.varsafe.dev/api/secrets/secretscontroller-delete Deletes one secret from a project environment. The deletion is recorded in the audit trail and, on plans with versioning, earlier versions remain recoverable. --- # Delete secrets in bulk Source: https://docs.varsafe.dev/api/secrets/secretscontroller-deletebulk Deletes several secrets from one environment in a single transaction, so a partial failure leaves the environment unchanged. --- # Diff two environments Source: https://docs.varsafe.dev/api/secrets/secretscontroller-diff Compares the secret keys of two environments and reports what is added, removed or changed. Useful before promoting configuration between environments. --- # Read one secret value Source: https://docs.varsafe.dev/api/secrets/secretscontroller-getvalue Returns the decrypted value of a single secret. Prefer this over listing with values when only one key is needed, so the audit trail records what was actually read. --- # Resolve an environment for injection Source: https://docs.varsafe.dev/api/secrets/secretscontroller-inject Returns the fully resolved key/value set for a project environment, with composed secrets expanded. This is what `varsafe run` calls before spawning a child process. --- # List secrets Source: https://docs.varsafe.dev/api/secrets/secretscontroller-list Lists secret keys and metadata for a project environment. Values are never included — that requires `secrets:read_values`. --- # List secrets with values Source: https://docs.varsafe.dev/api/secrets/secretscontroller-listwithvalues Lists secrets for a project environment including decrypted values. Every call is recorded in the audit trail. --- # Changelog Source: https://docs.varsafe.dev/changelog ## [7.4.0] - 2026-09-25 ### Added - CLI: `varsafe login` now asks you to enter its code in your browser rather than confirm it, matching the new approval page. ## Platform — 2026-09-25 ### Changed - Dashboard: the code field formats what you type as `ABCDE-FGHJK`, with or without the dash, and shows which account you are signed in to, with a sign-out link, before you enter the code. ### Security - Dashboard: approving `varsafe login` now requires typing the code your terminal shows, and you get an email each time CLI access is approved. If you receive one you did not expect, revoke that access from your profile and change your password. ## [7.3.0 – 7.3.1] - 2026-09-24 ### Added - CLI 7.3.0: lengthen the CLI sign-in code to 10 characters (XXXXX-XXXXX) ### Changed - CLI 7.3.0: move the build-cli release job out of YAML into tested scripts ### Fixed - CLI 7.3.1: stop the install gate if its scratch directory cannot be made - CLI 7.3.1: run the install gate from an exec-capable directory - CLI 7.3.0: drop the "someone else approved that code" hint after login - CLI 7.3.0: route every documentation link through a checked registry - CLI 7.3.0: make the install gate unskippable, and test the pin end to end ## [7.2.32] - 2026-09-17 ### Fixed - CLI: run the downloaded binary before it replaces the working one - CLI: close the escalated review findings ### Security - CLI: `varsafe run` no longer hands a Varsafe token set in your environment to the command it runs. If that command needs it to call Varsafe itself, pass `--pass-credential`. ## [7.2.30 – 7.2.31] - 2026-09-14 ### Fixed - CLI 7.2.31: close defects from a fourth OpenCodeReview pass - CLI 7.2.30: close defects from a second OpenCodeReview pass - CLI 7.2.30: preserve mount semantics - CLI 7.2.30: make publisher execution and reseal tests deterministic - CLI 7.2.30: separate registry and artifact credentials - CLI 7.2.30: require an explicit publish decision ## [7.2.10 – 7.2.29] - 2026-09-13 ### Fixed - CLI 7.2.29: close defects found by an OpenCodeReview pass - CLI 7.2.29: terminate publisher command explicitly - CLI 7.2.28: keep publisher failure handling shell-safe - CLI 7.2.27: keep Bunny purge key in protected CI - CLI 7.2.26: retain protected storage writer - CLI 7.2.25: bound build keyring verification - CLI 7.2.24: enforce a build ceiling - CLI 7.2.23: avoid ACL mutation for restricted publisher - CLI 7.2.22: skip emulated keyring daemon roundtrip - CLI 7.2.21: force-stop wedged emulated keyring checks - CLI 7.2.20: bound CDN verification requests - CLI 7.2.19: keep piped welcome independent of keyring - CLI 7.2.18: isolate cli smoke test home - CLI 7.2.17: avoid bucket administration checks - CLI 7.2.16: support write-only object publisher - CLI 7.2.15: use stateless restricted s3 uploads - CLI 7.2.14: keep publisher shell literal-safe - CLI 7.2.13: isolate publisher configuration - CLI 7.2.12: pin publisher config per upload - CLI 7.2.11: configure restricted publisher remote - CLI 7.2.10: Release recovery uploads can now complete with the scoped publisher credential. ## [7.2.9] - 2026-09-12 ### Changed - CLI: finish the SonarQube quality sweep and undo its behavioural regressions ### Fixed - CLI: repeating `--include` now injects every pattern you passed. It previously kept only the last flag and silently left the other secrets empty, which could write a credential file that looked correct but was not. ## [7.2.8] - 2026-09-05 ### Fixed - CLI: keep --tmpfs writing to /dev/shm, and close the attack with the guard - CLI: give the loader-control rule its own module - CLI: stop an export writing through a symlink, and a secret hijacking the child ## [7.2.7] - 2026-09-04 ### Changed - CLI: upgrade NestJS to 12 and validate requests with the built-in Standard Schema pipe ### Fixed - CLI: stop caching node_modules, so a bad install layout cannot latch ## Platform — 2026-09-04 ### Changed - MCP: a client that registers a `http://localhost` callback must now send `"application_type": "native"` in its registration request. Loopback callbacks are only accepted for native clients; a client that omits the field is refused at registration. ## Platform — 2026-08-22 ### Added - API: the programmatic API surface is now published as an OpenAPI specification at https://varsafe.dev/openapi.json, with the scope each route requires. Point a client generator or an AI agent at it instead of hand-writing requests. - Docs: https://varsafe.dev/llms.txt describes what varsafe is for and when to reach for it, so agents can decide whether it fits a task without reading the marketing site. ### Changed - Docs: a missing page on varsafe.dev now answers with links to the sitemap, the API specification and the documentation, in Markdown for clients that ask for it, instead of a bare "file not found". ## [7.2.6] - 2026-08-19 ### Changed - CLI: close the last three convention decisions ## [7.2.5] - 2026-08-15 ### Fixed - CLI: tolerate EPIPE on stdout data pipes ## [7.2.4] - 2026-08-15 ### Changed - CLI: finish the ruled convention remediation (wave 2) - CLI: typed errors everywhere, strict scoped writes, canonical transaction scope ### Fixed - CLI: honor the disclosure policy in MCP tool errors and repair dead assertions ## [7.2.3] - 2026-08-12 ### Fixed - CLI: keep snippet scratch out of the git checkout ## Platform — 2026-08-12 ### Fixed - API: Emails that a provider rejects are now retried instead of being recorded as delivered. Some invitations, verification links and password resets could previously be dropped without notice. ### Security - API: SSO configuration is now restricted to team owners and admins on the Team plan, where other signed-in accounts could previously create a provider. Existing SSO settings are unchanged. - API: Names shown in emails, such as a team or inviter name, are now escaped. A crafted name could previously alter the appearance of an invitation. - API: Repointing an SSO provider at a different identity provider now issues a new provider ID, so existing linked accounts cannot be inherited by the new one. ## [7.2.2] - 2026-08-10 ### Fixed - CLI: pass the session bus through to spawned snippets ## [7.2.1] - 2026-08-10 ### Changed - CLI: import the canonical secret-key pattern instead of redeclaring it ## Platform — 2026-08-10 ### Added - API: Ending a session is now recorded, whichever way it happens — you revoking one of your own devices, an admin revoking a member's, or an incident response — each with the reason it ended and what the person who did it had proved. - API: Changes to the account allowlist are now recorded, so who was granted or denied the ability to create an account is answerable from the audit log. - MCP: Connect an AI agent with an API token instead of the browser sign-in — for machines with no browser of their own, or when your agent should use a different account than your CLI. Grant a token MCP scopes on the **API Tokens** page, then pass it as an `Authorization` header. ### Fixed - API: An access review is returned even if its audit entry cannot be written, instead of failing the whole request. ### Security - MCP: A connected agent is now held to its owner's current role, so lowering someone's role takes effect on their agent immediately — previously only removal and deactivation did. An agent that starts refusing a tool needs the role that permits it restored. ## Platform — 2026-08-09 ### Added - API: Your own account activity — sign-ins, sign-outs, passkey and two-factor changes — is now readable at `GET /audit/actor/:actorId`. These records were always written but no view could return them. - API: Generating a compliance export or an access review is now itself recorded in the team's audit log, with who asked and which period it covered. ### Fixed - Dashboard: The Audit Log page now opens for people who belong to no team, instead of failing with a validation error. - API: Account creation is recorded as its own event rather than as a sign-in, so signing up and signing in are no longer indistinguishable in the audit log. - API: Operator-only platform records no longer appear in a platform administrator's personal activity, and personal activity now respects your plan's retention window. ## Platform — 2026-08-08 ### Added - **Composed secrets** — build a secret from other secrets: switch the value field to **Composed** and type `${` to pick a key. The dashboard shows what each secret is built from and what depends on it, and asks what to do before a delete or rename would break something. ### Changed - Plan limits on team members and projects are now enforced on every path that creates one. ### Fixed - Dashboard: two dialogs that could get stuck now recover — the Secrets page no longer sits on "Select team", and the Add Passkey dialog no longer stays on "Registering…" after you dismiss the browser prompt. ### Security - **Every credential event is audited** — issued, approved, refused, expired and automatically revoked — in each team the credential can reach. - **Identity-provider URLs are validated** when you save your SSO settings, and must be https. - **API hardening** — rate limits are counted across the whole service, secret responses are never cached, the client address can no longer be chosen by the caller, and the dashboard ships a Content-Security-Policy. - **AI-agent tools validate every argument** the way the API always has, and export patterns can no longer be made expensive to evaluate. ## [7.2.0] - 2026-08-05 ### Added - CLI: read, write and delete composed secrets. ### Fixed - CLI: `varsafe set --stdin` works when its input is a redirected file. ## [7.1.3] - 2026-07-31 ### Fixed - CLI: `varsafe status` tells an inaccessible project apart from a connection problem, and re-checks which team you are in rather than trusting a stale local copy. ## Platform — 2026-07-31 ### Fixed - **The one-line installer works again** — `curl -fsSL https://varsafe.dev/install.sh | bash` exited without installing anything. It now also checks it can write to the destination before downloading, and never hangs a CI runner. - AI agents: revoking a connection takes effect immediately, and membership or role changes retire the connections they affect. ## [7.1.2] - 2026-07-30 ### Fixed - CLI: `varsafe update` offers a sudo-free reinstall to `~/.varsafe/bin` when it cannot write to the install it is running from. ## [7.1.1] - 2026-07-30 ### Fixed - CLI: `varsafe update` says it cannot write to its install **before** downloading, and gives the exact command to run. ## [7.1.0] - 2026-07-28 ### Added - CLI: refreshed look and layout. ### Fixed - CLI: `varsafe get -n` suppresses the trailing newline. ## [7.0.0] - 2026-07-28 Breaking: `varsafe run` and `varsafe run --env-file` both changed behaviour, and the API now rejects unrecognised request fields. Read the migration notes below before upgrading. ### Added - CLI: **Linux ARM64 support**, **light and dark themes** (`varsafe theme`), and `varsafe status` — one screen with the credential, project and environment you are working against. - CLI: `varsafe list --include` filters by glob, and `varsafe run --shell` runs a command that genuinely needs a shell. - Dashboard & API: **read-only API tokens**, ideal for CI jobs and audits. An existing token can be narrowed to read-only at any time, effective immediately. ### Changed - CLI: **Breaking** — `varsafe run` runs your command directly, and `--env-file` reads encrypted files only. **Migration**: write the arguments out or use `--shell`, and source plaintext `.env` files inside the child process. - API: **Breaking** — an unrecognised field in a request body is rejected instead of being dropped while the request still returns success. **Migration**: the response names the field. ### Removed - CLI: **shell completion**. **Migration**: remove any `eval "$(varsafe completion zsh)"` from your shell config. ### Security - CLI: **encrypted `.env` values are tied to the variable they belong to**, so a value moved onto another variable no longer decrypts. **Re-export any `.env` written before this release.** - API: **key rotation now covers leaving and demotion**, not just removal by an admin. ## [2.0.0 – 6.1.0] - 2026-07-27 Thirty-six releases over nine days, during which varsafe gained AI-agent access, encrypted exports and multi-team workspaces. Rather than list every patch from that window, the changes that affect you are collected here. Individual releases before 2.0.0 continue below. ### Added - **AI-agent access** — agents manage secrets through a standard OAuth 2.1 flow, with scoped consent and full audit coverage. Works with Claude Code, Cursor and any MCP-capable client. - **Encrypted `.env` exports** — `varsafe export` encrypts by default, so an exported file is safe to commit. CI can write one; reading one back stays restricted to an owner or admin. - **Multi-team support** — belong to several teams and switch between them from the dashboard and the CLI. The active team is remembered per project. - **More ways to set a secret** — `varsafe set` reads from stdin, a masked prompt, an environment variable or a file, so a value never appears in your shell history. `varsafe get` prints one. ### Changed - **Breaking** — `varsafe set` no longer takes the value as an argument, which was visible in your shell history. **Migration**: `printf %s "$VALUE" | varsafe set API_KEY --stdin`. ### Fixed - **`varsafe export` writes faithful files** — values containing shell characters or Windows line endings came back mangled, and multi-line secrets were silently flattened. **Re-export anything written before this release.** ### Security - **Sign-in and local storage hardened** — OAuth 2.1 with PKCE, logout revoked server-side, the local encryption key in the OS keychain, and publisher-signed release manifests. - **Tenant isolation is enforced at the storage layer**, so teams cannot reach each other's data even if an application check were bypassed. ## [1.0.56] - 2026-03-05 ### Added - `varsafe run --include ''` — inject only the secrets matching a glob. - `varsafe login` takes an API token from a masked prompt, from stdin, or from `VARSAFE_TOKEN`. ### Fixed - API tokens authenticate CLI and API requests end to end. ## [1.0.52] - 2026-02-14 ### Added - `varsafe set` and `varsafe unset` — create, update and remove a secret from the command line. - Checksum verification on CLI install and update. ## [1.0.0] - 2026-01-21 ### Added - **Initial release** — a CLI-first secrets manager for developers and teams: `login`, `use`, `ls`, `export`, and `run ` to inject secrets into any process. - Dashboard for projects, environments, secrets, teams and API tokens. - One-line install: `curl -fsSL https://varsafe.dev/install.sh | sh`. --- # CLI Reference Source: https://docs.varsafe.dev/cli import CliReference from '../generated/cli-reference.mdx'; import help from '../examples/offline/help.sh?raw'; import runInject from '../examples/cli/run-inject.sh?raw'; import exportFormats from '../examples/cli/export-formats.sh?raw'; import unsetAndContext from '../examples/cli/unset-and-context.sh?raw'; varsafe CLI ## Installation The CLI is a self-contained binary — it is not published on npm. Six artifacts are published: Linux x64 and arm64, each for glibc and musl, plus macOS x64 and arm64. The installer picks the right one by detecting your architecture and C library. Windows works via WSL. ```bash curl -fsSL https://varsafe.dev/install.sh | bash ``` The installer downloads the binary to `~/.varsafe/bin` and adds it to your shell's `PATH`. Verify the install and explore the commands — no account needed: To upgrade later, the binary updates itself: ```bash varsafe update ``` ## Commands at a glance The CLI has 14 flat commands — no nested subcommands: | Command | Description | | -------- | -------------------------------------------------------- | | `login` | Authenticate with varsafe | | `logout` | Revoke session and clear credentials | | `whoami` | Display current authenticated user | | `use` | Set default project and environment context | | `list` | List secrets for a project/environment (alias: `ls`) | | `get` | Print a single secret value to stdout | | `set` | Set a secret in an environment | | `unset` | Remove a secret from an environment | | `run` | Run a command with secrets as environment variables | | `export` | Export secrets to a file or stdout | | `status` | Show the credential, the current context, and what is in it | | `doctor` | Diagnose whether the CLI can securely store credentials | | `theme` | Choose the colour palette (auto, dark, light) | | `update` | Update the CLI to the latest version | Every command and flag is documented in the [generated reference](#command-reference) at the bottom of this page. --- ## Authentication There are two ways to authenticate: an interactive browser login for humans, and an API token for machines. ### Browser login (default) ```bash varsafe login ``` The CLI opens your browser and prints a short, human-readable confirmation code in the terminal. Approve the request in the browser — after checking the code matches — and the CLI completes automatically via a server push, falling back to polling if the streaming connection drops. Your password never passes through the terminal. Browser login stores the credential in your operating system's keychain. That makes it the wrong choice for CI: a headless runner has no keychain, so `varsafe login` fails there. Use a token instead. ### API token (CI and automation) API tokens are created in the dashboard under team settings. Three ways to supply one: ```bash # The CLI picks the token up automatically — no login step at all export VARSAFE_TOKEN="vsafe_example_ci_token" varsafe run -- ./deploy.sh ``` ```bash # "-" reads the token from stdin — keeps it out of argv secret-manager read varsafe-token | varsafe login -t - ``` ```bash # Interactive masked input — never lands in shell history varsafe login -T ``` :::warning There is no flag for passing a token inline. `varsafe login --token "$TOKEN"` is **rejected**, because the value would be visible in shell history and in `ps` output. Supply the token through the environment, on stdin, or via the masked prompt. ::: For pipelines, prefer the environment variable and skip `login` entirely — see [CI/CD usage](#cicd-usage). ### Session commands ```bash varsafe whoami # show the authenticated user and teams varsafe logout # revoke the session server-side and clear local credentials ``` Logout invalidates the session immediately — a stolen credentials file is useless afterward. --- ## Environment variables | Variable | Purpose | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `VARSAFE_API_TOKEN` | API token for non-interactive auth — the primary CI variable | | `VARSAFE_TOKEN` | Shorter alias for `VARSAFE_API_TOKEN`; also read by `varsafe login -t` | | `VARSAFE_API_URL` | Overrides the API base URL (default `https://api.varsafe.dev`) — for self-hosted or test stacks | | `VARSAFE_DEBUG` | Set to any value to append a troubleshooting log to `~/.varsafe/debug.log` (capped at 5 MB). Writes to the file, not to your terminal | | `VARSAFE_NO_UPDATE_CHECK` | **Presence** disables the interactive "update available" prompt — any value counts, including `0` and `false`, as with `NO_COLOR`. Set it in CI so the prompt cannot hang an unattended run | | `VARSAFE_THEME` | `auto`, `dark`, or `light` — overrides the saved palette for one invocation without rewriting settings. An unrecognized value is ignored | --- ## Context Commands that touch secrets need a project and environment. Resolution order: 1. **Flags** — `-p`/`--project` and `-e`/`--env` (or `-i`/`--project-id` in automation) always win 2. **Local `.varsafe` file** — found by searching from the current directory up to the git root 3. **Global saved context** — set via `varsafe use` 4. **Auto-selection** when only one option exists; interactive prompt otherwise ```bash # Set context — inside a git repo this writes a .varsafe file, # elsewhere it saves globally varsafe use -p my-api -e development # View the current context varsafe use # Clear it varsafe use --clear ``` This script removes a secret and exercises the context lifecycle: --- ## Inject secrets into a process `varsafe run` is the core workflow: it fetches secrets over TLS and injects them as environment variables into the child process. Nothing is written to disk; when the process exits, the secrets are gone. ```bash varsafe run -- npm run dev ``` The command after `--` is executed argv-exact — no shell re-parsing, so quoting is preserved and arguments arrive in the child process exactly as you wrote them. Status messages ("Injecting 5 secrets") go to stderr, so stdout stays clean for piping. That also means shell syntax is *not* interpreted: `;`, `|`, `&&`, `$()` and backticks reach your program as literal characters rather than being executed. This is deliberate — it stops a value such as a CI branch name from running as a command inside the process holding your secrets. When you do want a shell, ask for one explicitly with `--shell`, which takes a single command string: ```bash varsafe run --shell 'bun run build && ./deploy.sh' ``` Prefer the argv form when you can. Reach for `--shell` only for pipes, `&&`, and redirection — and note that `--shell 'setup && exec app'` keeps your real process in the foreground, which matters for signal handling. Shape what gets injected: ```bash # Only secrets matching glob patterns (comma-separated, and the flag can be repeated) varsafe run --include 'VITE_*,CRISP_*' -- bun run build varsafe run --include 'VITE_*' --include 'CRISP_*' -- bun run build # Rename injected keys: each secret is exposed with MONITORING_ prepended # (e.g. DATABASE_URL becomes MONITORING_DATABASE_URL). Keys that already start # with the prefix are left unchanged. varsafe run --prefix monitoring -- ./run-monitoring.sh ``` `--include` selects *which* secrets are injected; `--prefix` renames them. They compose. Repeating `--include` unions the patterns, so the two forms above inject the same keys. Merge in an [encrypted `.env` file](/guides/encrypted-env) — file values win over API values, and the flag can be repeated. Every file must be sealed to the environment the command resolves to, since that is the key it can ask for: ```bash varsafe run --env-file .env --env-file .env.extra -- npm start ``` `--env-file` reads **encrypted files only**; a plaintext file is refused. To merge a plaintext `.env`, source it in the shell — inside the child, so its values still win over the injected secrets: ```bash varsafe run --shell 'set -a; . ./.env.local; set +a; exec npm start' ``` The full version, including the child process assertion: :::tip[Transparent re-encryption] When `--env-file` detects a file encrypted with a rotated (inactive) key, it silently re-encrypts the file with the current key. ::: ### Read a single value For piping one secret into another tool, `varsafe get` prints just the value: ```bash # Use a secret inline without exposing it in your shell profile psql "$(varsafe get DATABASE_URL)" # Exact bytes, no trailing newline — for tools that are byte-sensitive. # On Linux, /dev/shm keeps it in RAM; macOS has no /dev/shm, so use a # path you control and remove it afterwards. varsafe get SIGNING_KEY -n > ./key # Structured output: {key, value, version, updatedAt} varsafe get DATABASE_URL --json ``` --- ## Export secrets to a file When a tool insists on a file, `varsafe export` writes one. The `.env` format is **encrypted by default**: the file starts with a `#@varsafe/v2/ek_` header identifying the environment key, and each value is individually encrypted and bound to its variable name (`varsafe:v2:...`). Encrypted `.env` files are readable only by your team — safe to commit — and are decrypted on the fly by `varsafe run --env-file`. Writing an encrypted file needs only the environment's public key, so it works with an API token as well as interactively. **Reading one back does not**: decryption needs the private key, which only an interactive owner or admin can fetch. So a CI job can produce an encrypted `.env`, but it cannot consume one — see [encrypted .env](/guides/encrypted-env). ```bash # Encrypted .env (default) varsafe export -o .env # Plaintext requires an explicit opt-in varsafe export --plain -o .env # Other formats: compose (alias: env), docker (alias: kubectl), json, yaml (alias: yml) — # auto-detected from the output file extension, or forced with -f varsafe export --plain -o secrets.json varsafe export --plain -f yaml # Write to RAM-backed tmpfs (/dev/shm) so the file never touches disk varsafe export --plain --tmpfs -o app.env ``` :::info[Encryption applies to the env format only] `json`, `yaml`, `docker`, and `kubectl` outputs are always plaintext. Only the default `env` format encrypts values. ::: The export flow, covering the encrypted default, `--plain`, and format variants: --- ## CI/CD usage For pipelines, set `VARSAFE_API_TOKEN` from your CI secret store — no `varsafe login` step is required: ```yaml name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install varsafe CLI run: curl -fsSL https://varsafe.dev/install.sh | bash - name: Deploy with secrets run: varsafe run -p my-api -e production -- ./deploy.sh env: VARSAFE_API_TOKEN: ${{ secrets.VARSAFE_API_TOKEN }} ``` ```yaml deploy: stage: deploy script: - curl -fsSL https://varsafe.dev/install.sh | bash - varsafe run -p my-api -e production -- ./deploy.sh variables: VARSAFE_API_TOKEN: $VARSAFE_API_TOKEN ``` :::tip Prefer `varsafe run` over file exports in CI — secrets stay in process memory. If a file is unavoidable, `varsafe export --plain --tmpfs` writes to RAM-backed `/dev/shm`; delete it in the same job step. ::: --- ## Exit codes | Code | Meaning | | ---- | --------------------- | | 0 | Success | | 1 | Authentication failed | | 2 | Network error | | 3 | Validation error | | 4 | Resource not found | | 5 | Permission denied | | 99 | Internal error | `varsafe run` is an exception: once the child process starts, `run` exits with the **child's** exit code, so wrappers and CI steps behave as if the command ran directly. --- ## Uninstall To remove varsafe from your system: ```bash # Remove the binary and local data rm -rf ~/.varsafe ``` Then remove the PATH entry from your shell configuration: ```bash # Edit ~/.zshrc and remove the line: # export PATH="$HOME/.varsafe/bin:$PATH" ``` ```bash # Edit ~/.bashrc and remove the line: # export PATH="$HOME/.varsafe/bin:$PATH" ``` ```bash # Edit ~/.config/fish/config.fish and remove: # fish_add_path ~/.varsafe/bin ``` If you used `varsafe use` in any project directories, you can also delete the `.varsafe` context files in those repositories. :::tip This only removes the CLI. Your account, secrets, and team data remain in varsafe and can be accessed from the dashboard or by reinstalling the CLI. ::: --- ## Troubleshooting Error messages, their causes, and the fix for each are in the [troubleshooting guide](/guides/troubleshooting) — expired tokens, a stuck browser login, `Not signed in`, an empty `varsafe list`, `Project is required` in CI, secure-storage failures, and rate limits. One case belongs here, because it happens before the CLI can report anything: **`varsafe: command not found` right after installing.** The installer adds `~/.varsafe/bin` to your shell config, which an already-open shell has not read yet. Restart the terminal, or add it for the current session: ```bash export PATH="$HOME/.varsafe/bin:$PATH" ``` If it is still missing, run the installer again and read its `Detected platform:` line — it names the artifact chosen for your OS, architecture and C library. --- ## Command reference Generated from the CLI's own `--help` output — always matches the shipped binary. --- # Core Concepts Source: https://docs.varsafe.dev/concepts ## Organizational Model Four things nest, each one inside the one before it: a **team** owns **projects**, a project has **environments**, and an environment holds **secrets**. Every secret has exactly one home — the same key name in three environments means three separate values under three separate access rules. The top-level unit. Owns projects, holds the members and their roles, and carries billing. An application or repository that needs secrets. Belongs to exactly one team. A deployment stage within a project — `development`, `staging`, `production`, or your own. An encrypted key-value pair living in one environment, versioned on every change. ### Teams A team is a group of people who collaborate on the same projects. - Teams own projects - Teams have members with roles — and a user can belong to multiple teams - Access to secrets flows through team membership - Billing is managed at the team level - Teams on the Team plan can configure [SSO](/guides/sso) (SAML 2.0 / OIDC) for domain-based login ### Projects Each project has: - A name (typically matching your repository) - An owning team - Multiple environments - Its own secret history ### Environments Every project has environments for different deployment stages: | Environment | Purpose | Default Protected | | ------------- | ---------------------- | ----------------- | | `development` | Local development | No | | `staging` | Pre-production testing | No | | `production` | Live systems | Yes | You can also create **custom environments** for specific needs (e.g., `qa`, `demo`, `feature-x`), and protect or unprotect any environment in its settings. Each environment also has a server-managed keypair used to produce [encrypted `.env` exports](/guides/encrypted-env) — files that are useless to anyone who obtains them without access to your team. The private key is held and KMS-protected by varsafe, not by you, so this is not end-to-end encryption; see [the access model](/reference/security#the-access-model-honestly). {/* claim-lint-ok — denies the E2E claim rather than making it */} :::info[Protected environments] When an environment is marked as protected, write access is restricted to owners and admins; developers and operators get read-only access; viewers and billing members get no access. See the [roles reference](/reference/roles) for the full matrix. ::: ### Secrets Secrets are key-value pairs stored securely: - **Key**: starts with an uppercase letter, then uppercase letters, digits, and underscores (e.g., `DATABASE_URL`); up to 255 characters - **Value**: encrypted, up to 64 KB - **Version**: auto-incremented on each change - **History**: full change history with rollback capability #### Values are stored, not interpreted A secret's value is bytes. varsafe stores what you give it and returns exactly that — it does **not** expand `${...}`, read your shell environment, or substitute anything into a value. A secret whose value is `${DB_PASS}` is a secret whose value is the eight characters `${DB_PASS}`, forever, on every read. This matters when secrets hold templates as data: a Grafana dashboard, a Kubernetes manifest, another tool's config file. Those pass through untouched. #### Composed secrets A secret can *opt in* to being a **pointer** instead of a value: ``` DATABASE_URL = postgres://${DB_USER}:${DB_PASS}@db.internal/app ``` This is a different kind of secret, marked as such when you create it — never inferred from what the value happens to look like. Rotating `DB_PASS` changes every secret that references it, so a password lives in one place rather than in every connection string that embeds it. Reads return the resolved value; only an explicit request returns the source. Secrets created before composition existed are literal, and stayed literal — nothing was reinterpreted. See [Composed Secrets](/guides/composition). --- ## CLI-First Architecture varsafe is designed around the CLI workflow: Under the hood, a single `varsafe run` does four things: 1. **Authenticate** — CLI presents your session token or API token over TLS 2. **Fetch** — API retrieves your secrets from the vault, where they are encrypted at rest (vault + KMS) 3. **Return** — Secrets travel back to the CLI over TLS 4. **Inject** — CLI sets secrets as environment variables in the spawned process — nothing is written to disk The dashboard exists for management tasks (creating teams, inviting members, viewing audit logs) but the primary developer interaction is through the CLI. --- ## Context Management Most commands need to know which project and environment you mean. Four sources, checked in order — the first that answers wins. ```bash # Set your working context varsafe use -p my-api -e development # Now these commands use that context automatically varsafe list varsafe run -- npm run dev varsafe export -o .env ``` When you run `varsafe use` inside a git repository, the context is written to a `.varsafe` file in the repo — teammates and future shells in that repo pick it up automatically. Outside a repository, the context is saved globally. Override context anytime with flags: ```bash varsafe list -p my-api -e staging ``` :::tip[Save time with context] Set your context once with `varsafe use` and forget about `-p` and `-e` flags. Use `varsafe use --clear` to reset. ::: --- ## Ephemeral Injection Secrets are never written to disk by default. Instead, they're injected directly into your process's environment: ```bash varsafe run -- npm run dev ``` Four steps between your keystroke and a running process. On your machine the values live only in process memory — the CLI's and the child's — and are never written to disk. When your app terminates, the secrets are gone — no files left behind. :::warning[Ephemeral by design] Secrets injected via `varsafe run` exist only in the child process environment. If you need a file, use `varsafe export` explicitly — the default export is [encrypted](/guides/encrypted-env), and plaintext requires `--plain`. ::: --- ## Access Control Model Access follows this hierarchy: Six roles exist: **owner**, **admin**, **developer**, **operator**, **viewer**, and **billing**. Owners and admins manage the team and write everywhere; developers write to non-protected environments; operators and viewers are read-only; billing members manage billing without secret access. The full permissions matrix lives in the [roles reference](/reference/roles). ### API Tokens Use API tokens instead of user sessions for non-interactive access: - **CI/CD pipelines** — Inject secrets during build and deploy - **Automation scripts** — Scheduled jobs, cron tasks, infrastructure tooling - **Server-side applications** — Backend services that need secrets at startup - **Development tooling** — Custom scripts, internal CLIs, or IDE integrations Token properties: - Scoped to a specific team - Prefixed `vs_at_` so scanners can recognize them - Separate audit trail (shows as a service account) - Optional expiration date; revocable at any time See [API tokens](/reference/api-tokens) for creation and usage. --- ## Version History Every secret change is tracked. Operation types include: - `create` — New secrets added - `update` — Secret values changed - `delete` — Secrets removed - `rotate` — Secret rotated ### Rollback You can roll back an environment to any previous state via the dashboard: 1. Go to **Secrets → History** 2. Select the operation to roll back 3. Review the preview showing what will change 4. Confirm rollback --- ## Audit Trail Every action is logged. An audit event looks like this (illustrative): ```json { "action": "secret.accessed", "actor": "alice@example.com", "project": "api-backend", "environment": "production", "secretKeys": ["DATABASE_URL", "API_KEY"], "ip": "192.168.1.100", "source": "cli", "timestamp": "2026-02-15T10:30:00Z" } ``` Audit logs are: - **Append-only** — Events are added, never edited - **Complete** — Every action is logged - **Queryable** — Filter by team, project, actor, or time range - **Exportable** — Download for compliance reviews Audit retention is plan-gated: - **Developer plan**: 7 days - **Team plan**: 90 days --- ## Session Management varsafe uses opaque session tokens for authentication: - **High-entropy tokens** generated with secure randomness - **Hashed before storage** — the raw token is never stored server-side - **Instantly revocable** — no waiting for token expiry - **Session metadata** includes IP, user agent, and timestamp When you revoke a session, it's immediately invalid everywhere. See the [security model](/reference/security) for the full picture. --- ## Related Documentation - [CLI Reference](/cli) — Command details - [Roles](/reference/roles) — Full permissions matrix - [Dashboard Guide](/guides/dashboard) — Full dashboard features - [API Tokens](/reference/api-tokens) — Service accounts - [MCP Server](/guides/mcp) — AI agent access via Model Context Protocol - [Security Model](/reference/security) — Security architecture - [Operations](/reference/operations) — Operational workflows --- # Getting Started Source: https://docs.varsafe.dev/getting-started import quickstart from '../examples/cli/quickstart.sh?raw'; import runInject from '../examples/cli/run-inject.sh?raw'; By the end of this page you will have the CLI installed, a secret stored in varsafe, and that secret injected into a running process — without a plaintext `.env` file on disk. ## Prerequisites sign up free', }, { label: 'Supported', value: 'Linux and macOS, x64 and arm64, on both glibc and musl — including Raspberry Pi, Graviton and ARM CI runners. Windows works via WSL.', }, ]} /> ## From zero to injected secrets 1. **Install the CLI** The CLI ships as a self-contained binary — it is not published on npm. The installer places it in `~/.varsafe/bin` and adds that directory to your shell's `PATH`: ```bash curl -fsSL https://varsafe.dev/install.sh | bash ``` Open a new shell (or source your shell config), then verify: ```bash varsafe --version ``` The CLI keeps itself current — run `varsafe update` any time to install the latest release. :::warning[Command not found?] The installer appends a `PATH` entry to your shell config (`~/.zshrc`, `~/.bashrc`, or fish config). If `varsafe` isn't found, restart your terminal or run `export PATH="$HOME/.varsafe/bin:$PATH"`. ::: :::note[Alpine and other musl systems] varsafe needs `libstdc++` and `libgcc` on musl-based systems. Running as root, the installer adds them for you. Otherwise it stops and tells you to run: ```bash apk add --no-cache libstdc++ libgcc ``` Or re-run the installer with `VARSAFE_INSTALL_PREREQS=1` to let it use `sudo`. It won't do that on its own — a script piped from the internet shouldn't change your system packages just because `sudo` happens to be available. It stops rather than continuing because without those libraries the binary cannot start at all. The installer also runs the downloaded binary once before putting it on your `PATH`, so a broken install fails loudly instead of silently. ::: 2. **Log in** ```bash varsafe login ``` This opens your browser and shows a short confirmation code in the terminal. Approve the request in the browser — the code on screen must match the one in your terminal — and the CLI completes automatically. No password ever passes through the terminal. If a browser isn't available (headless machine, container, CI runner), use an API token instead — see [CLI authentication](/cli#authentication). 3. **Point at a project and environment** ```bash varsafe use -p my-api -e development ``` Saved locally, so the commands that follow do not need `-p` or `-e`. 4. **Store a secret** Secret values are never accepted as arguments — they would leak into shell history and `ps` output. Pick whichever input suits you: **Prompt** ```bash varsafe set DATABASE_URL ``` Omit the value and the CLI asks for it with masked input. Best at a terminal. **Pipe** ```bash printf %s 'postgres://app:app@localhost:5432/app' | varsafe set DATABASE_URL --stdin ``` Best in scripts, where there is nothing to type into. **From a file** ```bash varsafe set TLS_PRIVATE_KEY --from-file ./server.key ``` Best for multi-line values such as certificates and private keys. **From the environment** ```bash varsafe set API_KEY --from-env BUILD_API_KEY ``` Best in CI, where the value already exists as a variable. Then confirm it landed: ```bash varsafe list # keys only — values stay masked varsafe get DATABASE_URL # revealing a value is audited ``` :::tip[Key format] Secret keys start with an uppercase letter and contain only uppercase letters, digits, and underscores (e.g., `DATABASE_URL`, `API_KEY`). ::: 5. **Inject it into your app** This is the core workflow. `varsafe run` fetches your secrets over TLS and injects them as environment variables into the child process — never written to disk, gone when the process exits: ```bash varsafe run -- npm run dev ``` Your app sees `DATABASE_URL` in its environment; your shell and filesystem never do. To prove it: ```bash varsafe run -- sh -c 'echo "DATABASE_URL is ${DATABASE_URL:+set}"' ``` Expected output: `DATABASE_URL is set`. ## Recap | Command | What it does | | --- | --- | | `varsafe use -p -e ` | Points the CLI at a project and environment, saved locally | | `varsafe set KEY` | Stores a secret, prompting for the value with masked input | | `varsafe list` | Shows the keys in scope, values masked | | `varsafe run -- ` | Runs your app with secrets in its environment, nothing on disk | | `varsafe export -o .env` | Writes an encrypted file, for tools that insist on one | Everything else is a variation on these. `varsafe status` tells you which credential, project, and environment the next command will use. ## Export when a file is required Some tools insist on a `.env` file. `varsafe export` writes one — **encrypted by default**, so the file is useless without your environment's keys: ```bash varsafe export -o .env ``` Plaintext requires an explicit opt-in with `--plain`. See [encrypted .env files](/guides/encrypted-env) and the [export workflow](/cli#export-secrets-to-a-file) for formats and options. :::warning Add plaintext `.env` files to your `.gitignore`. Encrypted exports are safe to commit, but plaintext ones are not. ::: ## The same flow, scripted The script below uses the automation-style login: `$VARSAFE_TOKEN` holds an API token (in CI, from your CI secret store), `$VARSAFE_PROJECT` names the project, and `$VARSAFE_API_URL` points at the API — in normal usage you omit `--api-url` and the CLI talks to `https://api.varsafe.dev`. And the injection flow end to end — note the child process assertion proving the secret arrived: ## Project setup The dashboard gives you a full overview of your secrets across all projects and environments: varsafe dashboard Signing up already created a team, a project, and three environments — which is why the commands above worked without any setup. You only add more when you outgrow them: another project per repository, another environment for a QA or demo stage. | Environment | `-e` slug | Purpose | Protected | | ----------- | ------------- | ---------------------- | --------- | | Development | `development` | Local development | No | | Staging | `staging` | Pre-production testing | No | | Production | `production` | Live systems | **Yes** | The slug is what you pass to `-e`, and it is also the address your secrets are stored under. Renaming an environment is safe; changing its slug re-points everything that referred to it, including API-token environment allowlists — so treat the slug as fixed once secrets exist. **Protected** means writes ask for confirmation: `varsafe set` and `varsafe unset` require you to type the environment name, or pass `-y` in automation. Production starts protected; you can protect any environment in its settings. Two things the dashboard does that the CLI does not: - **Bulk import** — on the Secrets page, "Add secrets" → "Bulk" takes a pasted or drag-dropped `.env` file, which is the fastest way to move an existing project in. - **Seeing everything at once** — every project and environment side by side, plus the audit trail of who read or changed what. ## Team collaboration Invite teammates from the Teams page ("Invite member") and assign each a role: owner, admin, developer, operator, viewer, or billing. Roles determine read/write access per environment — production is protected by default, restricting writes to owners and admins. See the [roles reference](/reference/roles) for the full permissions matrix. ## CI/CD integration For automated pipelines, use an API token instead of a personal session: 1. Go to your team settings in the dashboard 2. Navigate to "API Tokens" 3. Create a new token 4. Add it to your CI/CD secrets as `VARSAFE_API_TOKEN` ```yaml # GitHub Actions example - name: Deploy with secrets run: varsafe run -p my-api -e production -- ./deploy.sh env: VARSAFE_API_TOKEN: ${{ secrets.VARSAFE_API_TOKEN }} ``` The CLI picks up `VARSAFE_API_TOKEN` automatically — no `varsafe login` step needed. See [CI/CD usage](/cli#cicd-usage) for GitLab CI and more. ## Next steps - **[Core concepts](/concepts)** — Understand teams, projects, environments, and injection - **[CLI reference](/cli)** — Full command reference with examples - **[Dashboard guide](/guides/dashboard)** — Managing secrets, teams, and audit logs - **[API tokens](/reference/api-tokens)** — Service accounts for CI/CD pipelines - **[MCP server](/guides/mcp)** — Connect AI agents like Claude Code and Cursor - **[Security model](/reference/security)** — How your secrets stay safe --- # CI/CD Pipelines Source: https://docs.varsafe.dev/guides/ci import gitlabCi from '../../examples/config/gitlab-ci-example.yml?raw'; import githubActions from '../../examples/config/github-actions-example.yml?raw'; By the end of this page your pipeline authenticates with a team-scoped [API token](/reference/api-tokens) and injects secrets directly into your deploy command's process environment — no `.env` file in the repository, no secrets on the runner's disk, nothing echoed to the job log. ## Prerequisites - A varsafe project containing the secrets your pipeline needs - A team role that can create API tokens - Access to your CI system's secret store (GitLab CI/CD variables or GitHub Actions secrets) ## 1. Create a CI token 1. In the dashboard, open your team settings and go to **API Tokens** 2. Click **Create API Token** and name it after the pipeline that will use it (e.g. `github-actions-production-deploy`) 3. Under **Access**, choose **Read-only** unless the pipeline genuinely writes secrets — a deploy job that only injects configuration never needs write. See [access level](/reference/api-tokens#access-level) 4. Copy the `vs_at_...` token immediately — it is shown only once API tokens are scoped to the team they were created in: a leaked token can never reach another team's secrets. For least privilege, create one token per pipeline rather than sharing a single token — token names appear in the audit log, so per-pipeline tokens let you trace every action back to the job that performed it. A read-only token narrows the blast radius further: if it leaks, an attacker can read what that pipeline could read, but cannot rotate or delete your secrets. An existing token can be changed to read-only later without a rotation window, so this is worth checking on tokens you already have. ## 2. Store the token in your CI secret store Go to **Settings → CI/CD → Variables** and add a variable: ``` Key: VARSAFE_TOKEN Value: vs_at_... Flags: Masked, Protected ``` **Masked** keeps the value out of job logs; **Protected** restricts it to protected branches and tags, so a merge request from a fork cannot exfiltrate it. Go to **Settings → Secrets and variables → Actions** and click **New repository secret**: ``` Name: VARSAFE_API_TOKEN Value: vs_at_... ``` GitHub Actions automatically masks registered secrets in log output. ## 3. Authenticate in the job Set `VARSAFE_API_TOKEN` (or the shorter alias `VARSAFE_TOKEN`) in the job environment. Every varsafe command picks it up automatically. There is no login step: ```bash export VARSAFE_TOKEN="$CI_VARSAFE_TOKEN" varsafe run -i "$VARSAFE_PROJECT_ID" -e production -- ./deploy.sh ``` :::warning Do not add `varsafe login` to a pipeline. `login` persists the credential to the operating system keychain, and a headless runner has no keychain to persist to — the step fails before your deploy runs. The environment variable is the supported CI path. ::: Because there is no login step, an invalid token first surfaces on the command that needs it. To fail fast on a revoked or mistyped token, put a cheap authenticated call at the top of the job: ```bash varsafe whoami ``` ## 4. Pin the project with `--project-id` CI jobs are non-interactive: when the CLI cannot prompt, it needs the project and environment stated explicitly. Use `-i/--project-id` rather than `-p/--project` — the ID is a stable UUID that survives project renames: ```bash varsafe run -i "$VARSAFE_PROJECT_ID" -e production -- ./deploy.sh ``` To find the ID, run `varsafe use` locally inside your repository — it writes the selected `projectId` into a `.varsafe` file at the repo root (IDs only, no secrets). Store the ID as a plain CI variable (it is not sensitive). ## 5. Wrap the deploy command with `varsafe run` `varsafe run` fetches secrets, injects them into the child process environment, and passes the command's arguments through exactly as written. The secrets exist only in that process — they never touch the runner's filesystem: ```bash varsafe run -i "$VARSAFE_PROJECT_ID" -e production -- docker compose up -d ``` Narrow what gets injected with `--include` glob patterns, comma-separated or repeated: ```bash varsafe run -i "$VARSAFE_PROJECT_ID" -e production --include 'DB_*,API_*' -- ./deploy.sh ``` ## Complete examples ### GitLab CI ### GitHub Actions ## When the pipeline needs a file Prefer `varsafe run` — most tools read configuration from the environment. When a consumer genuinely requires a file (e.g. `docker compose --env-file`), export one, keep it in RAM, and delete it in the same command chain: ```bash varsafe export -i "$VARSAFE_PROJECT_ID" -e production --plain --tmpfs -o app.env docker compose --env-file /dev/shm/app.env up -d rm -f /dev/shm/app.env ``` - `--tmpfs` writes to `/dev/shm/` (RAM-backed), so the plaintext never reaches persistent disk - `--plain` is needed here because `docker compose --env-file` reads the file itself. `varsafe export` encrypts `.env` by default and that works fine under a token — but nothing in CI can *decrypt* it, since the private key is restricted to an interactive Owner/Admin session. Encrypt when the artifact is for a human to use later; use `--plain` when a tool in the same job has to read it. See [encrypted .env](/guides/encrypted-env). :::danger[Never commit or persist generated .env files in CI] A `.env` file written to the runner's workspace can end up in artifacts, caches, or Docker build contexts. Keep exports in `/dev/shm`, delete them immediately, and never check one into the repository. ::: ## Keep secrets out of job logs - Register the token as a **masked** variable so the CI system redacts it if a command prints it - Never `echo` injected variables, and avoid `varsafe list -r` or `varsafe get` in job scripts — their output lands in the log - Be careful with `set -x` (shell tracing): it prints expanded command lines, including any secret passed as an argument ## Self-hosted or staging API Set `VARSAFE_API_URL` to point the CLI at a different API instance — it takes priority over the URL saved at login: ```bash VARSAFE_API_URL=https://varsafe.internal.example.com varsafe run -i "$VARSAFE_PROJECT_ID" -e production -- ./deploy.sh ``` ## Verify it works - Add `varsafe whoami` after the auth step — it confirms the token is valid before the deploy runs - Check the **Audit Log** in the dashboard: every fetch performed with the token is recorded, with the token identified as the actor ## Troubleshooting - **`Invalid or expired token`** — the token was revoked or rotated, or a stale saved context points at a team the token cannot access. See [Invalid or expired token](/guides/troubleshooting#invalid-or-expired-token). - **`Project is required. Use -p or -i to specify.`** — the job is non-interactive and no project was given. Add `-i "$VARSAFE_PROJECT_ID"` (and `-e` for the environment) to the command. - More failure modes: [Troubleshooting](/guides/troubleshooting). --- # Composed Secrets Source: https://docs.varsafe.dev/guides/composition A composed secret is a **pointer, not a copy**. Instead of storing a connection string that happens to contain your database password, you store the shape of it and name the password: ``` DATABASE_URL = postgres://${DB_USER}:${DB_PASS}@db.internal/app ``` Rotate `DB_PASS` once and every secret that references it changes with it. Nothing else to find, nothing else to update. ## Why this exists Without composition, a password that appears in four connection strings exists in five places. Rotating it means editing all five, and missing one produces a partial outage that reads like a credential bug — the service is up, the password is correct, and one worker is still using the old one. This is not `.env` variable expansion. varsafe does **not** expand `${...}` in values by default, and it never has (see [What composition is not](#what-composition-is-not)). Composition is something you opt into per secret. ## Writing one In the dashboard, switch the value field to **Composed**, then type `${` to pick a key. The mode has to be chosen: a plain value containing `${...}` is stored as those exact characters, so the dashboard never reinterprets one for you. From the CLI, mark the value as composed: ```bash varsafe set DATABASE_URL --template 'postgres://${DB_USER}:${DB_PASS}@db.internal/app' ``` :::warning[Quote the value in your shell] Use **single** quotes. In `bash` and `zsh`, `"${DB_PASS}"` is expanded by the *shell* before varsafe sees it, so you would store whatever that variable held locally — usually the empty string. Single quotes pass the text through untouched. ::: ### The grammar `${KEY}` and nothing else. Every omission is deliberate: | Not supported | Why | | --- | --- | | `$KEY` (unbraced) | No unambiguous end. `$FOO_BAR` cannot be told from `${FOO}_BAR`, so renaming a key would silently change which secret a value points at. | | `${VAR:-default}` | An empty value is a legal secret, so a default cannot tell "unset" from "deliberately empty" — it would quietly paper over a missing production secret with a fallback that looks like it worked. | | `process.env` fallback | Resolution would depend on the machine doing the read, so CI and a laptop would disagree about what a secret *is*. | | Cross-environment / cross-project | Keeps the whole dependency graph inside one tenant boundary, so reading a composed secret authorizes exactly like reading an ordinary one. | To include a literal `${` in a composed value, escape it: `\${`. A backslash anywhere else stays a backslash, so Windows paths and `C:\$Recycle.Bin` need no special handling. References must be `UPPER_SNAKE_CASE`, matching the rule for secret keys — a template can only ever name something that could exist. ### Limits A composition may reference up to 32 distinct keys with at most 64 placeholders, chained up to 8 deep, and may resolve against at most 64 secrets in total. A resolved value is capped at 64KB and a whole resolved environment at 1MiB. Cycles are rejected outright, including the one-step `A = ${A}`. The 64KB cap applies to the **resolved** value, which is only computed when the secret is read. A short composition can therefore save successfully and still exceed the cap once its references are substituted in — the read is what refuses it, not the write. ## Reading one Everything that delivers values gives you the **resolved** result: ```bash varsafe run -- ./server # DATABASE_URL=postgres://alice:hunter2@db.internal/app varsafe export -f env # resolved varsafe get DATABASE_URL # resolved ``` That default is load-bearing. A CLI binary already installed somewhere cannot ask for anything else, so resolving is what keeps an upgrade from shipping the literal text `${DB_PASS}` into a running deployment. To see what was *written* rather than what it resolves to, ask for the source: ```bash varsafe get DATABASE_URL --source # postgres://${DB_USER}:${DB_PASS}@db.internal/app varsafe list --reveal --source ``` `varsafe list --reveal` marks each secret as `composed` or `literal`. :::warning[Read source before you write it back] Anything that edits a value must load `--source` first. Saving a **resolved** value replaces the pointer with a frozen copy of whatever it resolved to at that moment — the secret keeps working, looks correct, and silently stops tracking the next rotation. The dashboard's edit form does this for you. If you script an edit, read with `--source` and write back the source. ::: ## Resolution is all-or-nothing If any part of a composed value cannot be resolved — a missing reference, a cycle, a limit exceeded — the read **fails**. It never returns a partial result and never hands back a literal `${...}`. This is deliberate. A half-resolved connection string is worse than an error: the process starts, connects to the wrong place or fails to authenticate, and the cause is many layers away from the config that produced it. ## Deleting a secret that others reference varsafe refuses, and asks what should happen to the dependents: ``` $ varsafe unset DB_PASS 1 secret(s) are composed from DB_PASS: DATABASE_URL. Choose flatten to keep them working by copying this value into each of them, or cascade to delete them as well. ``` Two answers, named by consequence: ```bash varsafe unset DB_PASS --flatten # DATABASE_URL keeps working; the value is copied into it varsafe unset DB_PASS --cascade # DATABASE_URL is deleted too ``` :::danger[`--flatten` propagates the value you are deleting] If you are deleting `DB_PASS` **because it leaked**, flattening copies the compromised value into every dependent, where it becomes an ordinary literal nobody will think to rotate. For a security deletion you almost always want `--cascade`, then recreate. ::: There is deliberately no default and no prompt-and-guess. A CI job that picked for itself would either propagate a compromised value or take a service's config away, with nobody in the loop. The dashboard asks the same question, listing the secrets that would be affected before you choose. `--cascade` follows the chain: if `REPORT` is composed from `APP_CONFIG` and `APP_CONFIG` from `DB_PASS`, all three go. Stopping partway would leave a reference to something that no longer exists. The set is computed **at the moment of the delete**, not when you previewed it. If someone adds a secret referencing `DB_PASS` while you are deciding, that secret is deleted too — `cascade` means "this key and everything that depends on it", and what depends on it is a fact about the environment right then. Because of that, the result always names what actually went: ```bash varsafe unset DB_PASS --cascade --json ``` ```json { "key": "DB_PASS", "removed": true, "alsoRemoved": ["APP_CONFIG", "DATABASE_URL"], "flattened": [], "project": "my-project", "environment": "production" } ``` Without `--json` the same information is printed as a warning. `--flatten` reports the secrets whose values it copied into, under `flattened` — those are plain values now, not references. A bulk delete computes impact against the **whole batch**, so tearing down a service's interdependent config raises nothing. It refuses only if a secret *outside* the batch would be left pointing at one inside it. ## What composition is not - **Values are not expanded by default.** A literal secret containing `${DB_PASS}` stays exactly those bytes, forever, on every read. Only a secret explicitly marked as composed is ever resolved. - **Secrets that existed before composition are literal.** Nothing was reinterpreted. A value that happens to look like a template is still a value. - **There is no shell interpolation.** `varsafe run` executes without a shell, so `$VAR` in a *command* is not expanded either. This matters if you store templates as data — Grafana dashboards, Kubernetes manifests, another tool's config. Pasting `${DS_PROMETHEUS}` into a value stores that text and returns that text. ## Rotating Rotation is where composition pays for itself. Change the referenced secret: ```bash varsafe set DB_PASS --stdin < new-password.txt ``` Every composed secret that references it now resolves to the new value. No other secret changed, and nothing needs finding — which is the whole point, because the failure mode without composition is *missing* one of the copies. Rotation is also available from the dashboard and through the API's rotate endpoint. Rotating a **composed** secret is refused: a composition has no value of its own to rotate, since the value lives in what it references. Rotate what it points at instead. ## Auditing Reading one composed secret discloses every secret it embeds. The audit trail records the whole **closure**, not just the key you asked for: reading `DATABASE_URL` is logged as access to `DATABASE_URL`, `DB_USER` and `DB_PASS`. A compromise review that saw only the requested key would clear a password that in fact leaked. The 64-secret closure limit exists partly for this reason — it sits below the cap on names one audit event retains, so a composed read's disclosure is always recorded in full rather than truncated. ## Availability Composition is rolled out per deployment and is off until every serving instance can read a composed value. Self-hosted operators enable it with `SECRET_COMPOSITION_ENABLED` **after** completing the storage-format migration — the ordering is what makes it safe, not the flag. While it is off, the dashboard does not offer to compose a secret, and any composition made earlier keeps resolving normally but cannot be edited as a composition — the gate refuses every write that carries one, not only new ones. The edit form says so and offers to convert it to a plain value, which is a one-way move while the gate stays closed. --- # Dashboard Guide Source: https://docs.varsafe.dev/guides/dashboard ## Dashboard vs CLI **Use the dashboard for:** - Creating and managing teams - Inviting team members - Managing secrets (bulk import, rotation, diff) - Environment management - API token management - Viewing audit logs - Security settings (2FA, passkeys) - Billing and subscriptions **Use the CLI for:** - Injecting secrets into applications - Exporting secrets - Day-to-day developer workflow - CI/CD integration --- ## Navigation Dashboard overview The dashboard has these main sections: | Section | Purpose | | -------------- | ------------------------------------------------- | | **Dashboard** | Overview of teams, projects, and recent activity | | **Secrets** | View and manage secrets by project/environment | | **Teams** | Manage teams, members, and projects | | **API Tokens** | Create and manage tokens for CI/CD and automation | | **Profile** | Account settings, security, and sessions | | **Audit Log** | Activity history and compliance exports | | **Billing** | Subscription and payment management | | **MCP** | Review and revoke AI agent connections | --- ## Secrets Management Secrets page ### Viewing Secrets 1. Navigate to **Secrets** 2. Select a project from the dropdown 3. Choose an environment tab (Development/Staging/Production/Custom) Secrets display: - Key name - Version number - Last updated date - Masked value (click eye icon to reveal) :::warning Revealing a secret value is an audited action. ::: ### Adding Secrets #### Single Secret 1. Click **Add secrets** 2. Select **Single** mode 3. Enter key (automatically uppercased) 4. Enter value 5. Click **Add** :::tip[Key format] Must be uppercase letters, numbers, and underscores. Must start with a letter. Example: `DATABASE_URL`, `API_KEY_V2`. ::: #### Bulk Import 1. Click **Add secrets** → **Bulk** 2. Either: - Drag and drop a `.env` or `.json` file - Paste contents directly 3. Review parsed secrets 4. Confirm with the add button — it names the count it parsed, so a malformed file that yielded fewer secrets than you expected is visible before you commit ```env DATABASE_URL=postgres://localhost/db API_KEY=sk-1234567890 ``` ```json { "DATABASE_URL": "postgres://localhost/db", "API_KEY": "sk-1234567890" } ``` ### Editing Secrets 1. Click the edit icon next to a secret 2. Modify the value 3. Click **Save** Each edit creates a new version. The previous value is preserved in history. ### Deleting Secrets 1. Click the delete icon next to a secret 2. Confirm deletion :::info Deleted secrets can be restored via rollback. ::: ### Bulk Delete 1. Select multiple secrets using checkboxes 2. Use the delete action in the selection toolbar 3. Confirm — the button names the count, e.g. **Delete 5 secrets**, so you can check it matches what you selected before committing ### Bulk Edit Edit multiple secrets at once: 1. Click **Bulk edit** 2. Modify keys and values in the editor 3. Keys are automatically normalized to uppercase with underscores 4. Duplicate keys are flagged and must be resolved before saving 5. Click **Save** to apply all changes in one operation Bulk edit handles renames and value changes together. If a key is renamed, the old key is removed and the new key is created. --- ## Sync Suggestions The sync suggestions panel helps you keep secrets consistent across environments. It detects secrets that exist in other environments but are missing from the currently selected one. 1. In the Secrets page, look for the **Sync suggestions** panel 2. Suggestions are grouped by source environment with color-coded badges 3. For each missing secret you can: - Click **Add** to copy it to the current environment - Click **Edit & Add** to modify the value before adding 4. Click **Add all** on an environment group to copy all its missing secrets at once :::tip Run sync suggestions after creating a new environment or before deploying to catch missing configuration. ::: --- ## Comparing environments Find drift between two environments before it becomes a production incident. 1. On the Secrets page, pick the environment you want to start from 2. Click **Compare with...** and choose the environment to compare it against 3. Filter the result by **All**, **Different**, or **Identical** The comparison reports four things: keys present only on the left, keys present only on the right, keys present in both with different values, and keys that match. The **Identical** filter is the useful one after a migration — it tells you what has already been reconciled, so a long list of remaining differences is not mistaken for no progress. The button appears only when the project has more than one environment. :::tip Compare `staging` against `production` before a release: a key that exists in staging and not in production is the failure that shows up as a crash on boot, after the deploy. ::: --- ## Secret History & Rollback Every edit creates a new version, and rollback works at two levels: a single secret, or the whole environment. ### Single Secret 1. Open a secret's row actions and click **History** to see its version history 2. Each entry shows the version, who changed it, and when 3. Click **Restore** on a previous version and confirm — the value is restored as a new version ### Whole Environment 1. In the Secrets page, click **History** to see all operations for the current environment (creates, updates, deletes — with actor and timestamp) 2. Click **Rollback** next to an operation 3. Preview what will change: - Secrets to be deleted - Secrets to be restored - Secrets to be reverted to previous versions 4. Confirm the rollback :::warning Rollback restores the environment to its state _before_ that operation. Review the preview carefully. ::: --- ## Secret Rotation Rotate secrets with preview mode to ensure safe propagation: 1. Open a secret's row actions and click **Rotate** 2. Either: - Let varsafe auto-generate a new value - Enter a new value manually 3. Preview the change 4. Confirm rotation The old value becomes immediately invalid. The next `varsafe run` or `varsafe export` receives the new value. --- ## Environment Management ### Default Environments Every project comes with three environments: - **Development** — Local development - **Staging** — Pre-production testing - **Production** — Live systems (protected by default) ### Creating Custom Environments 1. Go to your project settings 2. Click **Create New Environment** 3. Enter name and slug 4. Choose a color for visual identification 5. Set protection status 6. Click **Create** ### Protected Environments When an environment is marked as protected: - **Owner / Admin** — read/write access - **Developer / Operator** — read-only access - **Viewer / Billing** — no access To protect an environment: 1. Go to environment settings 2. Toggle **Protected** on 3. Save changes ### Duplicating an Environment Duplicate an existing environment to create a copy with all its secrets: 1. Go to your project's environment list 2. Click the **Duplicate** button next to an environment 3. In the modal, enter: - **Name** — The new environment name - **Slug** — URL-safe identifier - **Color** — Visual identifier for the environment - **Protected** — Whether the new environment should be protected 4. Click **Duplicate** All secrets from the source environment are copied to the new one. Changes to the new environment do not affect the original. :::tip Duplicating is useful for creating feature-specific environments (e.g., duplicate `staging` to `feature-x`) or setting up new deployment targets with the same configuration. ::: ### Deleting Environments 1. Go to environment settings 2. Click **Delete environment** 3. Confirm deletion (this is permanent) :::danger Deleting an environment removes all its secrets permanently. This cannot be undone. ::: --- ## Team Management Teams page ### Creating a Team 1. Navigate to **Teams** 2. Click **Create team** 3. Enter team name 4. Click **Create** ### Creating a Project 1. Navigate to **Teams** 2. Click **Add project** 3. Select the owning team 4. Enter project name (match your repository name) 5. Click **Create** ### Inviting Members 1. Expand a team by clicking it 2. Click **Invite member** 3. Enter their email address 4. Select a role: - **Admin** — Manage members and all secrets in all environments - **Developer** — Read/write secrets in non-protected environments, read-only in protected - **Operator** — Read-only access to all environments including protected - **Viewer** — Read-only access to non-protected environments - **Billing** — Billing management only, no secret access 5. Click **Send invite** :::info Invitations expire after a set period. The invitee receives an email with a link to accept. ::: ### Changing Roles 1. Expand a team 2. Click the **...** menu next to a member 3. Select **Change role** 4. Choose the new role ### Deactivating Members Deactivation soft-disables a member instead of removing them, preserving their audit history and making reactivation easy. 1. Expand a team 2. Click the **...** menu next to a member 3. Select **Deactivate** Deactivated members: - Retain their role and audit history - Have a 7-day grace period where access is preserved - Cannot access secrets after the grace period - Do not count toward active member limits after grace period To **reactivate** a member, click the **...** menu and select **Reactivate**. The member regains full access immediately. :::info Owners cannot be deactivated. Transfer ownership first if needed. ::: ### Removing Members 1. Expand a team 2. Click the **...** menu next to a member 3. Select **Remove from team** 4. Confirm removal ### Transferring Ownership 1. Expand a team 2. Click the **...** menu next to a member 3. Select **Transfer ownership** 4. Confirm transfer :::warning Only one owner per team. The current owner becomes an admin after transfer. ::: --- ## API Tokens API tokens enable programmatic access for CI/CD pipelines and automation. ### Creating a Token 1. Go to **API Tokens** in the sidebar 2. Select the team the token should belong to — tokens are scoped to a single team 3. Click **Create API Token** 4. Enter a descriptive name (e.g. the pipeline that will use it) 5. Click **Create** 6. **Copy the token immediately** — It won't be shown again ### Using Tokens Set the `VARSAFE_API_TOKEN` environment variable (or `VARSAFE_TOKEN` as an alias): ```bash export VARSAFE_API_TOKEN=vs_at_xxxxxxxxxxxxx varsafe run -p my-api -e production -- ./deploy.sh ``` Or in CI/CD: ```yaml env: VARSAFE_API_TOKEN: ${{ secrets.VARSAFE_API_TOKEN }} ``` ### Rotating Tokens 1. Click **Rotate** next to a token 2. A new token is generated 3. Copy the new token 4. Update your CI/CD secrets The old token is immediately invalidated. ### Revoking Tokens 1. Click **Revoke** next to a token 2. Confirm revocation The token stops working immediately. --- ## Security Settings Security settings ### Two-Factor Authentication (2FA) 1. Navigate to **Profile** → **Security** 2. Click **Enable 2FA** 3. Enter your password 4. Scan the QR code with your authenticator app 5. Enter the verification code 6. Save your backup codes securely ### Passkeys Passkeys provide passwordless authentication using biometrics or security keys. 1. Navigate to **Profile** → **Security** 2. Click **Add passkey** 3. Give it a name (e.g., "MacBook Pro Touch ID") 4. Follow your browser's prompts 5. Confirm with biometrics or security key ### Session Management View and manage active sessions: 1. Navigate to **Profile** → **Security** 2. See all active sessions with: - Device/browser info - IP address - Last active time 3. Click **Revoke** to end a session immediately --- ## Audit Log Audit log ### Viewing Activity 1. Navigate to **Audit Log** 2. Use filters: - Search by email, action, or target - Filter by action type - Filter by target type - Set date range ### Understanding Events Each event shows: - **Action** (e.g., "Created secret", "Signed in") - **Actor** (who performed the action) - **Target** (what was affected) - **Source** (dashboard, CLI, or API token) - **Timestamp** - **IP address** Click an event to expand details. ### Tracked Actions | Category | Actions | | ------------ | -------------------------------------------------------- | | Auth | Login, logout, failed login, 2FA events, passkey events | | Secrets | Create, update, delete, access, export, rotate, rollback | | Teams | Create, update, invite, remove member, role change | | Projects | Create, update, delete | | Environments | Create, update, delete | | Tokens | Create, rotate, revoke | ### Exporting for Compliance 1. Click **Export** dropdown 2. Choose format: - **CSV** — General export - **SOC 2** — Security audit trail - **GDPR** — Processing activities record - **HIPAA** — Access controls audit --- ## Billing & Plans ### Plans | Feature | Developer (Free) | Team | | ----------------- | ------------------ | -------------- | | Team members | 3 free, up to 25 | Unlimited | | Projects | Unlimited | Unlimited | | Environments | Unlimited | Unlimited | | Audit retention | 7 days | 90 days | | Role-based access | Developer only | 6 roles | | Price | $0 + $6/extra user | $15/user/month | ### Upgrading 1. Navigate to **Billing** 2. Click **Upgrade to Team** 3. Start your 14-day free trial 4. Add payment method before trial ends ### Managing Subscription 1. Navigate to **Billing** 2. View current plan and usage 3. Manage payment methods 4. View invoice history --- # Docker Source: https://docs.varsafe.dev/guides/docker import dockerfileExample from '../../examples/config/Dockerfile.example?raw'; import composeExample from '../../examples/config/compose-example.yaml?raw'; The goal: containers receive secrets in their process environment at **runtime**, and nothing sensitive ever lands in an image layer, a build argument, or a committed file. The only credential a container needs is a single team-scoped [API token](/reference/api-tokens). ## Build Time vs Runtime This distinction drives every pattern on this page: - **Runtime** — the container fetches or receives secrets when it starts. Rotating a secret in varsafe takes effect on the next restart, with no rebuild. This is the default for application secrets. - **Build time** — a secret is needed while building the image (private registry credentials, license keys). Use BuildKit secret mounts, never `ARG`/`ENV`. :::danger[Never bake secrets into images] `ENV API_KEY=...`, `ARG API_KEY` passed via `--build-arg`, and `COPY .env .` all persist secrets in image layers, where `docker history` and any registry pull can read them — forever, even if a later layer deletes the file. Rotation then requires rebuilding and redistributing the image. ::: ## Which approach to use The four runtime approaches below all end with secrets in the process environment. They differ in where the CLI runs and what the container needs to reach: | Approach | CLI lives in | Container needs | Pick it when | | --- | --- | --- | --- | | [Entrypoint](#varsafe-as-the-entrypoint-recommended) | The image | Network to the varsafe API | Default. Restart picks up rotated secrets, nothing on the host | | [Inject from the host](#inject-from-the-host-environment) | The host | Nothing | You cannot or will not add the CLI to the image | | [Export to env_file](#export-to-env_file) | The host | Nothing | A tool insists on a file — see the cleanup caveat below | | [Pipe via stdin](#pipe-via-stdin) | The host | Nothing | One-off runs and scripts, no file at any point | ## varsafe as the Entrypoint (recommended) Install the CLI in the image and make `varsafe run` the entrypoint wrapper — secrets exist only in the application process environment, fetched fresh at every container start: Run it with the token as the only secret you handle yourself: ```bash docker run -e VARSAFE_TOKEN=vs_at_... my-image ``` Or with compose, taking the token from the host environment: Rotate secrets in varsafe and containers pick them up on the next restart — no rebuild, no redeploy. :::info The container needs network access to the varsafe API at startup. `varsafe run` passes the command's arguments through to the child process exactly as written. ::: ## Inject from the Host Environment Keep the CLI out of the image entirely: inject secrets into the shell that starts the containers, and let compose interpolate them. ```bash varsafe run -- docker compose up ``` ```yaml services: app: image: my-image environment: - DATABASE_URL=${DATABASE_URL} - API_KEY=${API_KEY} ``` Compose resolves `${VAR}` from the environment `varsafe run` created. Filter what gets injected with `--include`, which takes comma-separated glob patterns and can be repeated: ```bash varsafe run --include 'DB_*,API_*' -- docker compose up ``` With `docker run`, list each variable to pass through (a bare `-e KEY` with no value forwards it from the host): ```bash varsafe run -- docker run -e DATABASE_URL -e API_KEY my-image ``` ## Export to env_file When a file is unavoidable, export one — and keep it out of git and off persistent disk where possible. With `docker run`, use `--format docker` (unquoted `KEY=value`), since `docker run --env-file` treats quotes as literal characters: ```bash varsafe export -f docker -o .env.production docker run --env-file .env.production my-image ``` With compose, prefer a plaintext export to RAM-backed tmpfs, consumed and deleted in one command chain: ```bash varsafe export --plain --tmpfs -o app.env docker compose --env-file /dev/shm/app.env up -d rm -f /dev/shm/app.env ``` :::warning `-f docker`, `-f json`, and `-f yaml` exports are always plaintext — gitignore them and delete them after use. Only the default `.env` export is [encrypted](/guides/encrypted-env) and safe to commit. ::: ## Pipe via stdin Pass secrets into `docker run` without any file at all: ```bash varsafe export -f docker | docker run -i --env-file /dev/stdin my-image ``` :::info This works with `docker run` only, not `docker compose`. ::: ## Build-Time Secrets For secrets needed during `docker build`, use BuildKit secret mounts — the secret is available to the single `RUN` instruction and never written to a layer: ```dockerfile # syntax=docker/dockerfile:1 RUN --mount=type=secret,id=env,target=/tmp/.env \ export $(cat /tmp/.env | xargs) && npm run build ``` ```bash varsafe export -f docker | docker build --secret id=env,src=/dev/stdin . ``` ## CI/CD In a pipeline, store the API token as a masked CI variable and combine it with any pattern above — the full walkthrough, with GitLab CI and GitHub Actions configurations, is in [CI/CD Pipelines](/guides/ci). ## Verify - `docker run -e VARSAFE_TOKEN=vs_at_... my-image env | grep -c '='` inside a shell entrypoint shows the injected variables arrived (avoid printing the values themselves) - `docker history my-image` must show no secret values in any layer - Rotate a test secret in the dashboard, restart the container, and confirm the new value is live --- # Encrypted .env Files Source: https://docs.varsafe.dev/guides/encrypted-env varsafe encrypts `.env` exports by default, so the file can be committed to git or stored on disk without exposing secrets. `varsafe run --env-file` decrypts it transparently at injection time. ## How It Works Each environment has a keypair managed by varsafe. When you run `varsafe export`, each value is individually encrypted with that environment's public key. The private key is stored server-side, encrypted by KMS — it never exists in plaintext in the database. **Cryptography:** ECIES with X25519 key exchange, HKDF-SHA256 key derivation, and AES-256-GCM symmetric encryption. Each value gets its own ephemeral keypair, and each ciphertext is cryptographically bound to the key id and the variable name it sits under. :::warning[A sealed file is confidential, not signed] Sealing uses the environment's **public** key, so anyone who may read the secrets can also produce a valid sealed entry for any variable name. Encryption hides values from people outside the team; it is not proof that a given line was written by someone allowed to change secrets. **Review changes to a committed `.env` the way you review any other config change** — a changed blob is a changed value, even though you cannot read it. What the format *does* guarantee: a blob cannot be moved to a different variable name, duplicated onto another variable, or replayed under a different environment key. All three fail decryption. ::: :::warning[You can write an encrypted `.env` in CI; you cannot read one back] The two directions have different requirements, because they need different halves of the keypair: - **Encrypting** — sealing needs only the environment's **public** key, which any caller allowed to read the secrets may fetch. `varsafe export` therefore works with an API token, in CI, and for members below admin. - **Decrypting** — reading a sealed `.env` needs the **private** key, and fetching that is restricted to **owner or admin** roles through an interactive session. Holding it would allow offline decryption of files that outlive a token's access, so **API tokens cannot decrypt**. So a pipeline can produce an encrypted `.env` as an artifact, but `varsafe run --env-file ` will not work there — that is for an admin developing locally. To hand secrets to a process in CI, inject them directly with `varsafe run`, or use `varsafe export --plain` to RAM (see [CI/CD](/guides/ci)). ::: ## Quick Start ```bash # Export an encrypted .env file varsafe export -p my-api -e development -o .env # The file is safe to commit git add .env # Decrypt and inject into a command varsafe run --env-file .env -- npm run dev ``` ## File Format An encrypted `.env` file looks like this: ```env #@varsafe/v2/ek_a1b2c3d4 DATABASE_URL="varsafe:v2:base64url-encoded-ciphertext..." API_KEY="varsafe:v2:base64url-encoded-ciphertext..." ``` - The header comment (`#@varsafe/v2/ek_...`) identifies the format version and the keypair used for encryption - Each value is prefixed with `varsafe:v2:` followed by the encrypted payload - A file whose header names any other version is not read as a sealed file - The public key is **not** stored in the file — it's fetched from the API and cached in the CLI's encrypted local store Only the default `.env` export is encrypted. `--format docker`, `json`, and `yaml` are always plaintext, and `--plain` disables encryption for `.env` output. ## Workflows ### Local Development ```bash # One-time: export encrypted .env varsafe export -p my-api -e development -o .env # Daily: run with decryption varsafe run --env-file .env -- npm run dev ``` Commit the encrypted `.env` — new team members clone and run without configuring secrets manually. ### CI/CD In CI, prefer skipping `.env` files entirely and injecting straight from the API: ```bash varsafe run -i "$VARSAFE_PROJECT_ID" -e production -- ./deploy.sh ``` When a consumer requires a plaintext file (e.g. `docker compose --env-file`), export with `--plain` to RAM-backed tmpfs and delete it in the same command chain: ```bash varsafe export --plain --tmpfs -o app.env docker compose --env-file /dev/shm/app.env up -d rm -f /dev/shm/app.env ``` See [CI/CD Pipelines](/guides/ci) for the full pipeline setup. ### Multiple Environments ```bash varsafe export -p my-api -e development -o .env varsafe export -p my-api -e staging -o .env.staging # Run with a specific file — pass the environment it was sealed to varsafe run -e staging --env-file .env.staging -- npm run dev ``` A sealed file can only be opened by a run that resolves the same environment: the key it names belongs to that environment, and varsafe will not hand it over under another one. Mismatch it and the run fails rather than silently injecting nothing. ### Merging Sources API secrets and env-file secrets can be combined; file values take priority over API values: ```bash varsafe run -p my-api -e development --env-file .env -- npm run dev ``` :::note[`--env-file` reads encrypted files only] A plaintext `.env` is refused, not loaded. Whether a file was *meant* to be encrypted cannot be read off its contents — a sealed file whose markers were erased looks exactly like an ordinary `.env` — so accepting both would mean silently running with attacker-chosen values wherever a sealed file was expected. The shell already merges a plaintext file, and doing it inside the child keeps those values winning over the injected secrets: ```bash varsafe run --shell 'set -a; . ./.env.local; set +a; exec npm run dev' ``` ::: ## Key Rotation ### Auto-Rotation on Loss of Access When someone loses the ability to export environment keys — they are **removed** from the team, they **leave**, or they are **downgraded** out of owner/admin — varsafe automatically rotates the keypair of every environment whose private key that person accessed. Private-key access is tracked in the audit trail, which is what makes this targeted rotation possible. Rotation does not undo past exposure: files sealed before it still open with the old key, which the departing member may have copied. Treat the secrets in those files as compromised and rotate the secrets themselves, not just the key. The behavior is a team setting (**Key rotation on loss of access** in team settings): - **auto** (default) — rotate exposed environment keys when someone loses key-export access - **manual** — keys stay in place; rotate them yourself ### Transparent Re-Encryption After a rotation, existing encrypted `.env` files still decrypt: on the next `varsafe run --env-file`, the CLI detects the stale key, decrypts with the old key, and silently rewrites the file encrypted with the current key (you'll see `Re-encrypted with current key` on stderr). No manual migration needed. ## Plaintext Export For cases where encryption isn't wanted: ```bash # Plaintext .env varsafe export --plain -o .env # JSON and YAML are always plaintext varsafe export -f json -o secrets.json varsafe export -f yaml -o config.yaml ``` :::warning Plaintext exports must be gitignored. Only encrypted `.env` files are safe to commit. ::: ## Security Properties - **At rest**: values encrypted with AES-256-GCM; the private key is KMS-encrypted in the database - **Value isolation**: each value uses its own ephemeral keypair, so recovering the plaintext of one value tells you nothing about the others - **Context binding**: each ciphertext is authenticated against its key id and variable name, so blobs cannot be shuffled between variables or replayed across key generations - **No silent downgrade**: `--env-file` requires the file to actually be sealed, so stripping the markers off a committed file fails the run instead of turning it into unauthenticated input - **Key exposure tracking**: the audit trail records every private-key access, enabling targeted rotation - **Transparent re-encryption**: stale files are silently upgraded on next use after a keypair rotates ### What it does not give you - **Not forward secrecy.** The environment's long-lived private key decrypts every value ever sealed to it, including files already in git history. The ephemeral keypair per value gives value isolation, not forward secrecy — whoever obtains the environment private key can open every past export sealed under it. - **Not sender authentication.** See the warning at the top: sealing needs only the public key. - **Rotation is prospective.** Rotating a keypair stops *future* files from being readable with the old key, but the old key is retained so existing files keep opening — and anyone who already exported the old private key keeps whatever they copied. Rotation limits ongoing exposure; it does not revoke access to files someone already holds. --- # MCP Server (AI Agents) Source: https://docs.varsafe.dev/guides/mcp import McpReference from '../../generated/mcp-reference.mdx'; Connect AI coding assistants like [Claude Code](https://claude.ai/claude-code) and [Cursor](https://cursor.com) to varsafe via the [Model Context Protocol](https://modelcontextprotocol.io). Your AI agent can list projects, read secrets, set values, compare environments, and export configurations — all with fine-grained permissions and a full audit trail. ## Ways to Connect varsafe offers two MCP transports, and the hosted one accepts two kinds of credential. They are near-identical, but **not** interchangeable — two tools exist on only one transport, so check the [tool reference](#available-tools) before committing: | Option | Transport | Auth | Best for | | ----------------------- | --------- | --------- | ------------------------------------------------------------------ | | **Hosted + OAuth** | HTTPS | OAuth 2.1 | Zero-config: browser-based consent, no tokens to manage | | **Hosted + API token** | HTTPS | API token | Headless machines, or a different account from the one your CLI uses | | **Local stdio** | stdin/out | API token | Local inspection, self-hosting, or MCP clients without URL support | The two overlap heavily but are **not identical**: `varsafe_generate_secret` exists only on the hosted transport, and shared tools take different parameter names (the hosted endpoint uses `environmentSlug`, the stdio server uses `environment`). The stdio server also accepts a `teamId` argument on every tool, required when your token spans multiple teams. The [tool reference](#available-tools) lists both signatures per tool. The **Hosted** option is the default recommendation — most users should start there. ## How It Works (Hosted) 1. **Discover** — Your AI tool reads the MCP server URL and discovers OAuth endpoints automatically 2. **Consent** — Your browser opens a consent screen where you choose teams, scopes, and optional restrictions 3. **Authorize** — The agent receives an OAuth 2.1 access token (PKCE-secured, 1-hour expiry, auto-refreshed) 4. **Use** — The agent calls varsafe tools with the token; every action is audited --- ## Quick Setup Add the varsafe MCP server to your AI tool's configuration. ### Claude Code Run the `claude mcp add` command: ```bash claude mcp add --scope user --transport http varsafe https://api.varsafe.dev/mcp ``` `--scope user` registers the server globally for every project (written to `~/.claude.json`). Drop it to add the server to the current project's `.mcp.json` instead, which you can commit to share with teammates. Prefer to manage the config by hand? Drop this into a `.mcp.json` at the repo root: ```json { "mcpServers": { "varsafe": { "type": "http", "url": "https://api.varsafe.dev/mcp" } } } ``` :::warning[Do not use `~/.claude/.mcp.json`] That path is **not** read by Claude Code. User-scope servers live in `~/.claude.json` (managed by `claude mcp add --scope user`); project-scope servers live in a `.mcp.json` file at the repository root. ::: ### Cursor Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global): ```json { "mcpServers": { "varsafe": { "url": "https://api.varsafe.dev/mcp" } } } ``` On the first tool call, your browser opens a consent screen. Pick your teams and scope preset, and you're connected. :::tip[No API token needed] The OAuth flow handles authentication for you — there is nothing to create or paste. Reach for a token only when a browser on the same machine is not an option; see below. ::: ### Codex ```bash codex mcp add varsafe --url https://api.varsafe.dev/mcp codex mcp login varsafe ``` `codex mcp login` opens the same consent screen up front instead of on the first tool call. ### VS Code (Copilot agent mode) ```bash code --add-mcp '{"name":"varsafe","type":"http","url":"https://api.varsafe.dev/mcp"}' ``` This writes the server into your user profile. For a single repository, put the same entry under `servers` in `.vscode/mcp.json`. ### What your agent is told When a client connects, the server sends instructions that the client passes to its agent: never write secret values into `.env` files or commits; run commands through `varsafe run` with the project named, and never run one that prints a secret; create keys with `varsafe_generate_secret` so the value never passes through the agent; ask you to run `varsafe set` for values only you have; and read a value into the conversation only when you ask for it. There is nothing to install: it applies to every client above. --- ## Connect With an API Token The OAuth flow opens a browser and waits for the redirect to come back to the machine your MCP client is running on. That machine and the browser have to be the same one. If you start the flow on a headless server over SSH and open the link on your laptop, the callback lands on your laptop's `localhost`, where nothing is listening, and authorization never completes. An API token connects to the same hosted endpoint without a browser. It is also how you run your agent as a **different account from your CLI** — the token is independent of whatever identity `varsafe login` stored in your keychain. ### 1. Create a token with MCP scopes In the dashboard, go to **API Tokens**, create a token, and grant it the MCP scopes the agent needs. A token without MCP scopes is refused by the MCP endpoint — existing tokens are unaffected and cannot drive an agent until you say so. Scopes are bounded by the token's access level and by your own role: a read-only token cannot hold `secrets:write`, and narrowing a token to read-only later removes that scope automatically. ### 2. Point your client at the hosted endpoint with the token attached ```bash claude mcp add --scope user --transport http varsafe https://api.varsafe.dev/mcp \ --header "Authorization: Bearer vs_at_..." ``` Or in configuration: ```json { "mcpServers": { "varsafe": { "type": "http", "url": "https://api.varsafe.dev/mcp", "headers": { "Authorization": "Bearer vs_at_..." } } } } ``` Because the request already carries a credential, the endpoint never asks for authorization — your client shows the server as connected rather than prompting you to authenticate. :::warning[The token is a long-lived secret] It sits in a configuration file in plaintext unless your client supports environment-variable expansion. Scope it narrowly, and revoke it from the **API Tokens** page when the machine no longer needs access — revoking stops MCP access immediately. ::: Actions taken through a token are audited as the **token**, not as the person who created it. The creator's team membership still governs the token: if they leave the team or are deactivated, the token stops working. --- ## Local Stdio Alternative If you prefer to run the MCP server as a local subprocess — useful for self-hosted instances, inspection, or clients that cannot reach a remote MCP URL — use the `@varsafe/mcp-server` npm package (it also installs a `varsafe-mcp` binary if you prefer a global install over `npx`). ### 1. Create an API token In the dashboard, go to **API Tokens**, select the team, and click **Create API Token**. Copy the token — it's only shown once. ### 2. Add the local stdio config **Claude Code** — run: ```bash claude mcp add --scope user --env VARSAFE_API_TOKEN=vs_at_... varsafe -- npx -y @varsafe/mcp-server ``` Or commit a project-scope `.mcp.json` at the repo root: ```json { "mcpServers": { "varsafe": { "command": "npx", "args": ["-y", "@varsafe/mcp-server"], "env": { "VARSAFE_API_TOKEN": "vs_at_..." } } } } ``` **Cursor** — add to `.cursor/mcp.json`: ```json { "mcpServers": { "varsafe": { "command": "npx", "args": ["-y", "@varsafe/mcp-server"], "env": { "VARSAFE_API_TOKEN": "vs_at_..." } } } } ``` ### 3. Environment variables | Variable | Required | Default | Description | | ------------------- | -------- | ------------------------- | ------------------------------------------------------------------------- | | `VARSAFE_API_TOKEN` | Yes | — | varsafe API token (`vs_at_...`). `VARSAFE_TOKEN` is accepted as an alias. | | `VARSAFE_API_URL` | No | `https://api.varsafe.dev` | Override the API base URL (self-hosted instances, staging, etc.). | | `VARSAFE_MCP_DEBUG` | No | unset | When set to any truthy value, logs every API call to stderr. | ### Trade-offs - **Hosted** is faster to set up (no token), auto-refreshes, and gets new tools without any update. **Preferred for most users.** - **Local stdio** keeps the token under your control, works offline against self-hosted instances, and is inspectable — every file is readable in the `@varsafe/mcp-server` package on npm or GitHub. - **Both** share the same audit trail server-side, so you can switch freely. --- ## Consent Screen When your AI tool connects for the first time, your browser opens a consent screen where you control exactly what the agent can access. ### Scope Presets Choose how much access to grant: | Preset | What the agent can do | | ---------------- | ------------------------------------------------------------------- | | **Read Only** | List projects, environments, and secret keys (no values) | | **Secrets Full** | Everything in Read Only + read secret values, write secrets, export | | **Full Access** | Everything in Secrets Full + audit log access | ### Granular Restrictions You can further limit what the agent accesses: - **Team selection** — Choose which teams to grant access to - **Project restrictions** — Limit to specific projects within a team - **Environment restrictions** — Limit to specific environments (e.g., only `development`, never `production`) When no project or environment restriction is set, the agent can access all projects and environments in the granted teams — subject to your own role permissions. ### Client Verification The consent screen shows whether the connecting client is a verified application: - **Verified** (green shield) — Pre-registered clients like Claude Code (Anthropic) and Cursor - **Third-party** — Dynamically registered MCP clients; review carefully before approving --- ## Available Tools :::warning[Write tools modify real secrets] `varsafe_set_secret` and `varsafe_unset_secret` change your actual secrets. If you're using Full Access or Secrets Full presets, your AI agent can write to any environment it has access to — including production. Use [environment restrictions](#granular-restrictions) to limit scope. ::: --- ## Scopes Reference | Scope | Description | | --------------------- | ----------------------------------------- | | `identity:read` | View user identity and granted teams | | `projects:read` | List projects and environments | | `secrets:read` | List secret keys and metadata (no values) | | `secrets:read_values` | Read decrypted secret values | | `secrets:write` | Create, update, and delete secrets | | `secrets:run` | Export secrets for injection | | `audit:read` | View audit logs | --- ## Managing Connections ### Viewing Active Connections Go to **MCP** in the dashboard sidebar to see all active agent connections. Each connection shows: - Client name and verification status - Team access - Granted scopes - Connection date and last-used date ### Revoking Access Click **Revoke** on any connection to immediately disconnect the agent. The agent will need to re-authorize through the consent screen to reconnect. ### Auto-Expiration Connections that haven't been used for **90 days** are automatically expired. Re-authorize by using the agent again — the consent flow will re-trigger. --- ## Security Model ### OAuth 2.1 with PKCE MCP uses the OAuth 2.1 authorization code flow with Proof Key for Code Exchange (PKCE S256). This means: - No client secrets are stored on your machine - Authorization codes are single-use (5-minute expiry) - Access tokens are short-lived (1 hour) and auto-refreshed - Refresh tokens are rotated on every use (old tokens are invalidated) ### Audit Trail Every MCP tool call is logged in the audit trail, just like CLI and dashboard actions. You can see: - Which agent performed the action - Which team, project, and environment were accessed - The specific operation (list, read values, write, export) - Timestamp and IP address ### Rate Limits MCP tools are rate-limited per connection: | Category | Limit | Burst | | ----------------- | ------- | ----- | | Read operations | 120/min | 30 | | Write operations | 60/min | 15 | | Export operations | 20/min | 5 | When rate-limited, the agent receives a `429` response with a `Retry-After` header. ### Permission Boundaries MCP grants are bounded by your own permissions: - If you're a **Developer**, the agent can't write to protected environments — even with `secrets:write` scope - If you restrict the grant to specific projects, the agent can't access other projects - Your role's access level is the ceiling; MCP scopes can only narrow it further --- ## Example Workflows ### Ask your agent to check for missing secrets > "Compare the secrets between staging and production for the api-backend project. Are there any keys missing in production?" The agent uses `varsafe_list_projects` to find the project, then `varsafe_diff_secrets` to compare environments. ### Set a secret from your editor > "Set the `OPENAI_API_KEY` to `sk-proj-abc123` in the development environment of my api project." The agent uses `varsafe_set_secret` to create or update the secret. ### Export secrets for a local script > "Export all secrets matching `DB_*` from the staging environment." The agent uses `varsafe_export_secrets` with the `includePattern` parameter to filter by glob. --- ## Troubleshooting ### No MCP connections' after setup The consent flow only triggers on the first tool call, not when the config is added. Ask your AI agent to do something with varsafe (e.g., "list my varsafe projects") to trigger the authorization. ### Consent screen doesn't open Make sure you're logged into the varsafe dashboard in the same browser that will handle the OAuth redirect. The MCP server redirects to your dashboard for consent. ### Agent says 'access denied' for a project Check your [grant restrictions](#granular-restrictions). If you limited the grant to specific projects or environments during consent, the agent can't access anything outside those restrictions. Revoke the connection and re-authorize with broader access. ### Agent stopped working after a while Connections auto-expire after 90 days of inactivity. The agent will re-trigger the consent flow automatically — just use it again. ### Rate limit exceeded MCP tools are rate-limited. Wait for the `Retry-After` period and try again. If you're hitting limits regularly, batch your requests or space them out. --- ## Related Documentation - [CLI Reference](/cli) — Managing secrets from the command line - [API Tokens](/reference/api-tokens) — Programmatic access for CI/CD - [Security Model](/reference/security) — How your secrets stay safe - [Core Concepts](/concepts) — Teams, projects, and environments --- # Single Sign-On (SSO) Source: https://docs.varsafe.dev/guides/sso ## Prerequisites - **Team plan** — SSO is a Team plan feature - **Owner or Admin role** — only owners and admins can configure SSO - A configured identity provider (Okta, Azure AD, Google Workspace, OneLogin, etc.) ## SAML 2.0 Setup ### 1. Get your SP metadata In the dashboard, go to **Teams → your team → SSO**. Click **Configure SSO** and select **SAML 2.0**. The SP metadata section shows: | Field | Value | | ------------- | -------------------------------------------------------- | | **Entity ID** | `https://varsafe.dev/api/auth/sso/sp/{providerId}` | | **ACS URL** | `https://varsafe.dev/api/auth/sso/callback/{providerId}` | Copy these values into your identity provider's SAML app configuration. ### 2. Configure your IdP In your identity provider, create a new SAML application and paste the Entity ID and ACS URL from above. Then collect: - **Entry Point URL** — Your IdP's SSO login URL - **Certificate** — The IdP's signing certificate in PEM format - **Issuer** (optional) — The IdP's entity ID, if different from entry point ### 3. Enter IdP details in varsafe Back in the dashboard SSO settings, fill in: 1. **Entry Point URL** — Paste the IdP SSO URL 2. **Certificate** — Paste the full PEM certificate (including `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----`) 3. **Issuer** — Optional. Enter if your IdP requires it Click **Save** to create the SSO configuration. ### 4. Set your email domain Enter the email domain your team uses (e.g., `acme.com`). varsafe uses this domain to route login requests to your IdP. Routing stays off until you prove you control that domain — otherwise any team could claim `acme.com` and capture its users' logins. Verification is a DNS TXT challenge you complete yourself: 1. **Issue a challenge.** varsafe returns a record name and value: ``` _varsafe-challenge.acme.com TXT varsafe-verify= ``` 2. **Publish that TXT record** in your DNS. 3. **Verify.** varsafe resolves the record and marks the domain verified. Routing activates at that point, and users on that domain are redirected to your IdP. The challenge is valid for **7 days**, and issuing a new one rotates the token so a stale or cached record cannot be replayed. A domain can only be verified by one team — if another team already verified it, verification fails with a conflict rather than silently transferring the domain. :::note Until verification succeeds, your SSO configuration is saved but dormant: members are not redirected to your IdP and continue signing in normally. Nothing breaks while a DNS change propagates. ::: --- ## OIDC Setup ### 1. Create an OIDC application in your IdP Register a new application with your identity provider. Set the redirect URI to: ``` https://varsafe.dev/api/auth/sso/callback/{providerId} ``` ### 2. Collect credentials From your IdP, collect: - **Discovery URL** — The OpenID Connect discovery endpoint (e.g., `https://accounts.google.com/.well-known/openid-configuration`) - **Client ID** — The application client ID - **Client Secret** — The application client secret ### 3. Configure in varsafe In the dashboard, go to **Teams → your team → SSO**. Click **Configure SSO** and select **OIDC**. 1. **Discovery URL** — Paste the discovery endpoint 2. **Client ID** — Paste the client ID 3. **Client Secret** — Paste the client secret Click **Save**. ### 4. Set your email domain Enter the email domain for SSO routing, then have it verified — same as SAML setup above. --- ## How SSO Login Works Once SSO is enabled and your domain is verified: 1. User enters their email on the varsafe login page 2. varsafe checks the email domain against configured SSO providers 3. If a match is found, the user is redirected to the IdP login page 4. After IdP authentication, the user is redirected back to varsafe 5. A varsafe session is created automatically Users with SSO-enabled domains are routed to SSO automatically — no separate login URL needed. --- ## Managing SSO ### Enable / Disable Toggle SSO on or off from **Teams → your team → SSO**. Disabling SSO does not delete the configuration — team members fall back to email/password or OAuth login. ### Update Configuration Click **Edit** in the SSO settings panel to update IdP details (certificate rotation, new endpoints, etc.). ### Delete SSO Click **Delete** to permanently remove the SSO configuration. Team members will need to use another login method. :::warning Deleting SSO is permanent. Members who only used SSO will need to set a password or add a passkey before they can sign in again. ::: --- ## Troubleshooting ### Clock skew errors SAML assertions have a time window. varsafe allows up to 5 minutes of clock skew. If your IdP's server clock is further off, sync it with NTP. ### Certificate format The certificate must be in PEM format. If you have a `.cer` or `.der` file, convert it: ```bash openssl x509 -inform DER -in cert.cer -out cert.pem ``` Paste the full PEM including the `BEGIN` and `END` lines. ### Domain mismatch SSO routing matches the email domain exactly. If your team uses multiple domains (e.g., `acme.com` and `acme.io`), configure each domain in the SSO settings. ### User not redirected to IdP - Check that the domain has been verified — routing stays inactive until varsafe verifies domain ownership (see [Set your email domain](#4-set-your-email-domain)) - Confirm the user's email matches the configured domain exactly - Check that SSO is enabled (not just configured) ### SAML assertion failures - Verify the ACS URL in your IdP matches varsafe's ACS URL exactly - Check that the Entity ID matches - Ensure the certificate hasn't expired ### OIDC errors - Verify the discovery URL is accessible and returns valid JSON - Confirm the redirect URI in your IdP matches varsafe's callback URL - Check that the client secret is correct --- # Troubleshooting Source: https://docs.varsafe.dev/guides/troubleshooting Real failure modes, matched to the exact message the CLI prints. Each section starts with the error, explains the cause, and gives the fix. ## Invalid or expired token ``` Invalid or expired token ``` Printed when the API rejects the token. Two distinct causes: **1. The token is actually invalid** — it was revoked, rotated (rotation invalidates the old token immediately), or mistyped. Create or copy a fresh token from the dashboard's **API Tokens** page. **2. The token is fine, but your saved context points at a team the token cannot access.** The CLI sends the active team from your saved context with every request. API tokens are scoped to one team — if a leftover context references a different team, a perfectly valid token is rejected. This bites when you switch between a personal login and a CI token on the same machine. Fix by clearing the stale context, then retry: ```bash # If a .varsafe file exists in the directory (or any parent), remove it — # it takes priority over the global context rm ./.varsafe # Then clear the global saved context varsafe use --clear # Confirm the token is accepted (reads VARSAFE_TOKEN from the environment) varsafe whoami ``` :::info[Where context lives] `varsafe use` saves your project/environment selection to a `.varsafe` file at the git repository root (when inside a repo) or to the CLI's global store otherwise. The local file wins over the global context. `varsafe use --clear` clears the global store only — a local `.varsafe` file must be deleted manually. ::: ## Browser login hangs or never completes `varsafe login` opens your browser to authenticate. On headless machines, over SSH, or when the browser session gets stuck, use one of the non-browser paths: ```bash # API token, prompted with masked input (not visible in shell history) varsafe login --token-prompt # API token piped in from a secret manager secret-manager read varsafe-token | varsafe login -t - ``` There is no email/password path — passwords are only ever entered in the browser. On a machine with no keychain at all (most CI runners), skip `login` entirely and export `VARSAFE_TOKEN` instead; see [CI/CD usage](/guides/ci). The browser flow expires after a few minutes if it is not completed — if it times out or the confirmation code expires, rerun the login with one of the options above. ## `OS secure storage unavailable` `varsafe login` stores its credential in the operating system keychain. When that fails, the built-in diagnostic tells you which half is broken: It checks two things independently — that the native keyring addon loads, and that a real write/read round-trip through the OS keychain succeeds — and prints which one failed. Add `--json` for machine-readable output. The usual cause on a server or CI image is a temp directory mounted `noexec`. The CLI is a single-file binary that extracts its native keyring addon to `$TMPDIR` and loads it from there; if that directory forbids execution, the addon cannot load and the message appears even though the keychain itself is fine. The CLI detects this and re-executes itself once with a usable temp directory, so it normally self-heals. If it does not, point `TMPDIR` at an exec-able directory you own: ```bash TMPDIR="$HOME/.cache/varsafe-tmp" varsafe doctor --addon-only ``` On a CI runner there is usually no keychain at all, and that is expected — do not run `varsafe login` there. Export `VARSAFE_TOKEN` instead and skip login entirely; see [CI/CD usage](/guides/ci). ## `Not signed in` ``` Not signed in Run `varsafe login` to authenticate, or set VARSAFE_TOKEN env var for CI/CD ``` No stored session and no token in the environment. Run `varsafe login`, or export `VARSAFE_API_TOKEN` (alias: `VARSAFE_TOKEN`) — every command picks the env var up without a login step. ## `varsafe list` shows nothing, or the wrong secrets You are looking at a different project or environment than you think. The CLI resolves context in this order: command flags (`-p`/`-i`/`-e`) → local `.varsafe` file → global saved context. Check and fix: ```bash # Show the current saved context varsafe use # Re-select interactively varsafe use -p my-api -e development # Or bypass context entirely for one command varsafe list -p my-api -e development ``` If a teammate deleted the project or environment your context references, commands fail with: ``` Project not found. It may have been deleted. Run `varsafe use` to update. ``` ## My app received the literal text `${DB_PASS}` The secret is a **literal** whose value happens to contain `${DB_PASS}`, not a composed secret. varsafe never expands `${...}` in an ordinary value — see [Values are stored, not interpreted](/concepts#values-are-stored-not-interpreted). Check which it is: ```bash varsafe list --reveal ``` The `Kind` column reads `composed` or `literal`. To convert a literal into a composition, rewrite it: ```bash varsafe set DATABASE_URL --template 'postgres://${DB_PASS}@db.internal/app' ``` If the secret already reads `composed` and your process still got `${...}`, something in your pipeline is asking for the **source** representation — check for a stray `--source` on the read. ## `cannot resolve secrets: ... does not exist in this environment` A composed secret references a key that is missing from this environment. Resolution is all-or-nothing, so the whole read fails rather than returning a partial value — the message names the composed secret and the reference it could not satisfy. Usually this means the composition was promoted to an environment that does not have its inputs yet. Create the missing key, or check you are pointed at the right environment: ```bash varsafe status varsafe list ``` ## `Other secrets are composed from this one` You are deleting a secret something else points at, and varsafe will not guess what should happen to the dependents. Say which: ```bash varsafe unset DB_PASS --cascade # delete the dependents too varsafe unset DB_PASS --flatten # keep them working by copying this value into each ``` If you are deleting the secret **because it leaked**, use `--cascade`. `--flatten` would copy the compromised value into every dependent, where it becomes an ordinary literal nobody will think to rotate. See [Composed Secrets](/guides/composition#deleting-a-secret-that-others-reference). ## `Project is required` in scripts and CI ``` Project is required. Use -p or -i to specify. ``` In a non-interactive session (CI, cron, piped output) the CLI cannot prompt, so the project and environment must come from flags or a saved context. Pass `-i` with the project ID for rename-proof automation — see [CI/CD Pipelines](/guides/ci). ## Installer refuses your platform ``` Unsupported architecture: ``` The installer maps `x86_64`/`amd64` to x64 and `arm64`/`aarch64` to arm64, on Linux and macOS, and picks a musl build when it finds a musl loader. Anything else — 32-bit ARM on an older Raspberry Pi OS, i686, s390x — has no published artifact and stops here. Linux arm64 **is** supported, on both glibc and musl. If you are on Graviton, a modern Raspberry Pi, or an ARM CI runner and hit this, re-fetch `install.sh` — an installer predating the arm64 builds refuses the platform outright. The installer prints the artifact it settled on, for example `Detected platform: linux-arm64-musl`, which is the fastest way to confirm it identified your machine correctly. ## Alpine or musl: "Error loading shared library libstdc++.so.6" ``` Error loading shared library libstdc++.so.6: No such file or directory ``` The musl builds link `libstdc++.so.6` and `libgcc_s.so.1` dynamically, and stock Alpine ships neither. Without them the binary dies inside the dynamic loader, so what you get is dozens of mangled C++ symbol names and nothing identifying varsafe as the cause. The dependency comes from Bun's own musl runtime, so it cannot be removed on our side. You should not normally see this from a current installer. Running as root it installs both libraries for you; otherwise it stops and prints the command rather than leaving you with a binary that cannot start: ```bash apk add --no-cache libstdc++ libgcc ``` If you are not root and would rather the installer handle it, re-run with `VARSAFE_INSTALL_PREREQS=1` and it will use `sudo`. It does not do that on its own — a script piped from the internet should not change your system packages just because `sudo` happens to be available. The installer also runs the downloaded binary once before putting it on your `PATH`, so an install that would fail on first run fails during installation instead, with the reason. If you are seeing this error at all, you likely installed with an older `install.sh` — re-fetch it. ## Pointing the CLI at a self-hosted API By default the CLI talks to `https://api.varsafe.dev`. To target a self-hosted or staging instance, either: ```bash # Per-invocation or CI: env var takes priority over everything VARSAFE_API_URL=https://varsafe.internal.example.com varsafe whoami # Or persist it at login time varsafe login --api-url https://varsafe.internal.example.com ``` If commands fail with `Cannot connect to server`, verify the URL is reachable from the machine (`curl` the API host) — the CLI falls back to cached project/environment data where it can, but secret fetches need a live connection. ## Rate limits The API rate-limits requests and answers `429` with a `Retry-After` header when a client exceeds its budget. If a script hits limits, space out calls or batch work — one `varsafe run` per deploy instead of a `varsafe get` per variable. ## Where to see what happened Every secret read, write, export, and login is recorded in the append-only audit trail: dashboard → **Audit Log**. Filter by actor, action, or date to reconstruct exactly what a token, teammate, or AI agent did — useful for confirming whether a "missing" secret was deleted, renamed, or never existed. See the [Dashboard Guide](/guides/dashboard#audit-log). ## Still stuck? - `VARSAFE_DEBUG=1` makes the CLI write diagnostic details to its debug log - Check the exact error message against this page's headings — the CLI prints stable, grep-able messages - [Contact support](https://varsafe.dev/support) with the command, the full error output, and your CLI version (`varsafe --version`) --- # Philosophy Source: https://docs.varsafe.dev/philosophy ## The Problem Environment variables shouldn't be a topic of conversation. They're infrastructure — they should work and get out of your way. But they don't. Instead: - Teams share `.env` files over Slack - Secrets get committed to repositories - Production credentials exist on developer laptops - No one knows who has access to what - Rotation means editing files across multiple systems This isn't a tooling problem. It's a design problem. ## Our Approach ### Principle 1: Encrypted Pipeline Your secrets follow a secure path: 1. **Encrypted at rest** — Industry-standard encryption in our secure storage backend 2. **Encrypted in transit** — TLS between all components 3. **Ephemeral in memory** — `varsafe run` injects secrets into your process environment; nothing is written to disk Secrets written to files can be: - Committed to version control - Backed up without encryption - Discovered by file system searches - Left behind on disposed hardware That's why varsafe injects secrets directly into your process's environment by default: ```bash # Recommended: direct injection varsafe run -- npm run dev ``` When you need files, you can export — but it's an explicit choice, and env-format exports are [encrypted by default](/guides/encrypted-env): ```bash # When needed: export to file varsafe export -o .env ``` ### Principle 2: Observable by Default Security through obscurity doesn't work. Instead, we make everything visible: - Every secret access is logged - Every authentication event is recorded - Every permission change is audited - Every action has an actor If someone accesses production secrets at 3am, you'll see it. If a departed employee's credentials are still active, you'll see it. If someone exports your secrets, you'll see it. Trust is earned through observable, enforceable behavior. ### Principle 3: Secure Defaults, Minimal Footguns The default configuration should be secure. You shouldn't have to be a security expert to avoid common pitfalls. The secure choice is the default one: - Production is protected by default (writes restricted to owner/admin — see [Roles & Permissions](/reference/roles)) - Session tokens are hashed before storage (a database leak doesn't expose sessions) - Audit logs are append-only (can't cover tracks) - Rate limiting is enabled by default (can't brute force) If you need to do something risky, you have to explicitly ask for it. ### Principle 4: CLI-First Developers live in their terminal. A web dashboard requires context switching, clicking around, copying and pasting. The primary interface is the CLI: ```bash varsafe login # One time varsafe run -- npm run dev # Every time ``` The dashboard exists for management tasks — creating teams, inviting members, viewing audit logs. But the daily workflow is just two commands. ### Principle 5: Simple Context Management If you have one project, it's auto-selected. With multiple projects, set your context once and the CLI remembers it: ```bash varsafe use -p my-api -e development # Commands use that context varsafe list varsafe run -- npm run dev ``` Override anytime with explicit flags: ```bash varsafe list -p other-project -e staging ``` No configuration files. No complex setup. ## What We Don't Do ### We Don't Manage Configuration Configuration and secrets are different things. Configuration is how your app behaves. Secrets are credentials that should never be visible. varsafe manages secrets. For configuration, use environment variables directly, config files, or feature flag services. ### We Don't Automate Deployment varsafe provides secrets. It doesn't deploy your code, run your CI/CD pipeline, or manage your infrastructure. We integrate with your existing deployment tools. We don't replace them. ## Design Decisions :::tip[Why not build our own crypto?] Cryptography is hard. Key management is hard. We use battle-tested encryption primitives and secure storage infrastructure. Our focus is on the developer experience, not on reimplementing cryptography. ::: ### Why Not Just Use .env Files? They work. Until they don't. `.env` files are: - Easy to commit accidentally - Hard to rotate across environments - Invisible (who has access?) - Inconsistent (is this the latest version?) That's why varsafe defaults to in-memory injection. But when you need files — for CI pipelines, Docker builds, or local tooling — `varsafe export` generates them from the same source of truth. Every access is audited, and your `.gitignore` keeps them out of version control. ### Why Version History? Secrets change. Sometimes they need to change back. Every change creates a new version. You can see exactly what changed, when, and by whom. If something goes wrong, rollback to any previous state with a single command. No more "what was the old API key?" or "who deleted that secret?" ## What Success Looks Like When varsafe is working well: - New team members don't ask "where's the .env file?" - Rotation happens without a fire drill - Compliance audits are a CSV export, not a project - Security reviews find no secrets in repositories - Developers think about features, not credentials Secrets become invisible infrastructure. That's the goal. ## Feedback We're building varsafe for developers. If something doesn't feel right, tell us: **[Contact form](https://varsafe.dev/contact)** We read everything. We respond to everything. We're building this for you. --- # API Tokens Source: https://docs.varsafe.dev/reference/api-tokens API tokens give CI/CD pipelines, automation scripts, and service accounts programmatic access to secrets without an interactive login. This page is the token reference; for step-by-step pipeline setup, see the [CI/CD guide](/guides/ci). ## Overview | Property | User Session | API Token | | -------------- | ----------------------------- | ---------------------------------------- | | Authentication | Browser-based login | Bearer token in environment | | Scope | All teams you belong to | Exactly one team (optionally narrowed) | | Expiration | 7 days, rolling renewal | Fixed, set at creation (plan-capped) | | Audit trail | Shows user email | Shows the token as actor | | Best for | Interactive CLI use | Programmatic access | API tokens page ## Token Properties - **Team-scoped** — a token belongs to exactly one team and can never access another team's data - **Name** — 1–100 characters, for identification (e.g., "GitHub Actions - Production Deploy") - **Access level** — `read` (the default) or `write`. A read-only token can read secrets but is refused on every write. See [Access level](#access-level) - **Project scoping** (optional) — restrict the token to specific projects; unscoped tokens can access all of the team's projects - **Environment scoping** (optional) — restrict the token to specific environments by slug (e.g., only `staging`); unscoped tokens can access all environments :::warning[Project and environment scopes multiply — they are not pairs] The two lists are checked independently, so a token scoped to projects `api`, `web` and environments `staging`, `production` reaches **all four** combinations — including `api/production` and `web/production`. There is no way to express "`api` in production and `web` in staging" in one token; that needs two tokens. ::: - **Expiration** (always set) — every token expires. If you do not choose a lifetime, the plan maximum is applied: | Plan | Maximum token lifetime | | --------- | ---------------------- | | Developer | 90 days | | Team | 365 days | - **Format** — tokens start with the `vs_at_` prefix; only a salted hash is stored server-side - **Last-used tracking** — each authenticated request records the timestamp, IP address, and user agent, visible in the dashboard :::info Active tokens per team are capped at a high abuse-prevention limit (500 on Developer, 2,000 on Team — raised on request). ::: ## Creating a Token Only **owners and admins** can create and manage tokens ([roles reference](/reference/roles)). 1. Go to your team settings in the dashboard 2. Navigate to **API Tokens** 3. Click **Create API Token** 4. Enter a descriptive name 5. Choose **Read-only** under Access if the token never needs to write 6. Optionally scope the token to specific projects and environments 7. Optionally set a shorter expiration than the plan maximum 8. Click **Create** and **copy the token immediately** :::danger The token value is shown exactly once, at creation. It is stored only as a hash — if you lose it, rotate or create a new one. ::: ## Access level Every token carries an action ceiling, evaluated separately from its project and environment scoping. | Level | Effect | | --- | --- | | `read` | Read secrets within the token's scope. Creating, updating, rotating, rolling back and deleting are all refused. **The default** when no level is requested | | `write` | Read secrets, and create, update, rotate, roll back or delete them, within the token's scope. Granted only when asked for | Choose read-only for anything that only consumes secrets — a CI job that injects configuration at deploy time, a service that reads config at boot, a script that inventories keys across projects. :::warning[Read-only protects integrity, not confidentiality] A read-only token still reads every secret **value** in its scope. It cannot damage your secrets, but if it leaks, everything it could read is exposed. Narrow the scope as well as the level, and keep the expiry short. ::: The level also does not reach past the API. It stops writes to varsafe; it does not stop the machine holding the token from writing what it read to a local file — `varsafe export --plain` works with a read-only token. A token may also always revoke *itself* (`DELETE /me/token`), which is deliberate: a compromised credential should be able to kill itself without an admin. Set it in the dashboard when creating the token. Driving it over the API instead means `POST /teams/{teamId}/api-tokens` with an `accessLevel` field: ```json { "name": "ci-inject", "accessLevel": "read" } ``` Omitting the field yields a `read` token; send `"accessLevel": "write"` for a token that changes secrets. :::note Everything under `/teams/{teamId}/api-tokens` — creating, listing, editing, rotating, revoking, deleting — requires an interactive owner or admin **session**, the same one the dashboard uses. Those routes do not accept a Bearer token, so **an API token cannot mint or edit another token**. The one exception is a token revoking itself through `DELETE /me/token`, which is Bearer-authenticated by design. ::: The request body is strict: a misspelled key such as `accessLevell` is rejected with `400` rather than ignored. That matters more here than elsewhere — dropping it would answer a request for a *narrower* credential with a full read-and-write one. To confirm what a token you already hold can do, ask the API with the token itself: ```bash curl -sH "Authorization: Bearer $VARSAFE_TOKEN" https://api.varsafe.dev/me/verify ``` The response carries `accessLevel` for token principals. ### Narrowing an existing token A token can be changed to read-only after the fact — no rotation window, and the token already in circulation keeps working with the narrower ceiling. Do it from the token's row in the dashboard, or with `PATCH /teams/{teamId}/api-tokens/{tokenId}` and `{"accessLevel":"read"}` from an admin session. The change applies to every request authorized **after** it commits. A write that already passed its authorization check runs to completion, so narrowing is not a way to abort work in flight — revoke the token if you need to stop it now. This ratchets one way only: widening `read` back to `write` is refused, so create a new token instead. Tokens minted for **headless CLI logins** are the exception — their access level cannot be changed at all (renaming still works). The level is derived from the CLI's granted scopes when the device is exchanged, so the next login on that machine would restore write if the new grant includes `secrets:write`, silently undoing the narrowing. Re-issue those with narrower scopes rather than editing them. ## MCP scopes A token can also authenticate an AI agent against the hosted MCP endpoint — useful on a machine with no browser, or when the agent should act as a different account than your CLI. See the [MCP guide](/guides/mcp) for client configuration. This is **opt-in per token**. A token with no MCP scopes is refused by the MCP endpoint, so existing tokens gain nothing until you grant them scopes explicitly. What you can grant is bounded twice over: - **By the token's access level.** A read-only token cannot hold `secrets:write`. Narrowing a token to read-only later removes that scope in the same change, so the ceiling and the agent surface can never disagree. - **By your own role.** You cannot grant a token more MCP authority than you hold yourself — the same role limits that apply on the consent screen. Like the access level, the scope set ratchets one way: scopes can be removed, never added. Removing all of them switches the agent surface off while leaving the token working for the HTTP API. Actions taken through a token are audited as the token, not as the person who created it. The creator's membership still governs it — if they leave the team or are deactivated, the token stops working everywhere, MCP included. ## Using a Token Set the token in the environment; the CLI picks it up automatically. `VARSAFE_API_TOKEN` is the primary variable, `VARSAFE_TOKEN` is an accepted alias: ```bash export VARSAFE_API_TOKEN=vs_at_vsafe_example varsafe run -p my-api -e production -- ./deploy.sh ``` For a persisted login (saved to the encrypted credential store), use one of the safe input paths: ```bash # Secure prompt — masked input, nothing in shell history varsafe login -T # Stdin pipe — from a CI secret or secure store echo "$VARSAFE_TOKEN" | varsafe login -t - # Environment variable VARSAFE_TOKEN=vs_at_vsafe_example varsafe login ``` :::warning Avoid `varsafe login -t ` with the literal value — it lands in shell history and `ps` output. ::: See [CLI Authentication](/reference/authentication#api-token) for all input methods, and the [CI/CD guide](/guides/ci) for GitHub Actions, GitLab CI, and other pipeline recipes. ## Rotation Rotation revokes the old token and issues a new one with the same name and scoping in a single step: 1. Go to team settings → **API Tokens** 2. Click **Rotate** next to the token 3. Copy the new token 4. Update your CI/CD secrets :::warning The old token is invalidated immediately — pipelines using it fail until they receive the new value. Revoked tokens cannot be rotated. ::: ## Revocation If a token is compromised or no longer needed: 1. Go to team settings → **API Tokens** 2. Click **Revoke** next to the token 3. Confirm revocation Revocation is immediate and permanent. ## Security Properties - **Hashed at rest** — varsafe stores a salted hash, never the token value - **Shown once** — the plaintext value exists only in the creation response - **Always expiring** — no forever-tokens; the plan cap bounds the damage window of a leak - **Scoping narrows blast radius** — a token scoped to one project and one environment can leak nothing else - **Audited** — creation, update, rotation, revocation, and deletion each produce an audit event (`api_token.*`), and every secret access by a token is logged with its identity ### Best Practices 1. **One token per purpose** — separate tokens for separate pipelines 2. **Scope tightly** — restrict to the project and environment the pipeline actually needs 3. **Short lifetimes** — pick the shortest expiration your rotation cadence supports 4. **Immediate revocation** — revoke tokens the moment they are no longer needed 5. **Secure storage** — keep tokens in your CI/CD secrets manager, never in code ## Troubleshooting ### Invalid token - Verify the token is correct (no extra spaces or line breaks) - Check whether the token was revoked or has expired (team settings → API Tokens) - Ensure `VARSAFE_API_TOKEN` (or `VARSAFE_TOKEN`) is set in the environment the command runs in - Try `varsafe login -T` to paste the token with masked input ### Token not authorized - The token may belong to a different team than the project you are targeting - The token may be scoped to other projects or environments — check its scoping in the dashboard ### Rate limited Token requests share the standard authenticated API rate limits (per-second, per-10-second, and per-minute windows). If a pipeline is throttled, reduce request frequency or batch work; for sustained higher limits, [contact support](https://varsafe.dev/contact). ## Multiple Environments **Option 1: One token, environment scoping open, select at run time** ```bash varsafe run -p my-api -e staging -- ./deploy.sh varsafe run -p my-api -e production -- ./deploy.sh ``` **Option 2: Separate tokens per environment** Create one token scoped to `staging` and another scoped to `production`. Each pipeline can only touch its own environment, and the audit trail separates them cleanly. :::tip Prefer option 2 for production pipelines — environment scoping is enforced server-side, so a misconfigured staging job cannot read production secrets. ::: ## Security Checklist - [ ] Tokens stored in CI/CD secrets, not in code - [ ] Each pipeline has its own token - [ ] Tokens scoped to the narrowest project/environment set that works - [ ] Unused tokens revoked - [ ] Expirations set to the shortest workable lifetime - [ ] Audit logs reviewed regularly --- # Authentication Source: https://docs.varsafe.dev/reference/authentication varsafe has two authentication surfaces: the **dashboard** (browser sessions, including email/password) and the **CLI** (browser-assisted login or API tokens — no password ever reaches the terminal). This page is the reference for both, plus the account-protection features layered on top: passkeys, two-factor authentication, device trust, and session management. ## Dashboard Login Methods | Method | Description | | ------------------- | ------------------------------------------------------------------------------- | | **Email/password** | Standard email and password login | | **Google OAuth** | Sign in with your Google account | | **GitHub OAuth** | Sign in with your GitHub account | | **GitLab OAuth** | Sign in with your GitLab account | | **Bitbucket OAuth** | Sign in with your Bitbucket account | | **Passkey** | Passwordless login with biometrics or a security key | | **SSO** | SAML 2.0 or OIDC through your company's IdP. See [Single Sign-On](/guides/sso) | ### OAuth Login Click the provider button on the login page. You are redirected to the provider to authorize varsafe, then returned with a session created automatically. If an account with your email already exists and the provider confirms the email is verified, the OAuth identity is linked to it. --- ## Passkeys Passkeys provide passwordless, phishing-resistant authentication using biometrics (Touch ID, Face ID) or hardware security keys (YubiKey). Passkey credentials are bound to the varsafe domain via WebAuthn. ### Adding a Passkey 1. Navigate to **Profile → Security** 2. Click **Add passkey** 3. Enter a name (e.g., "MacBook Pro Touch ID", "YubiKey 5") 4. Follow your browser's WebAuthn prompt 5. Confirm with biometrics or a security key tap You can register multiple passkeys for redundancy. ### Passkey-Only Mode Passkey-only mode disables password login entirely, requiring a passkey for every sign-in. This is the strongest protection against phishing and credential theft. **To enable:** 1. Register at least **2 passkeys** (for recovery) — enabling fails with fewer 2. Go to **Profile → Security** 3. Toggle **Passkey-only mode** on **To disable:** 1. Go to **Profile → Security** 2. Toggle **Passkey-only mode** off 3. Enter your password to confirm :::warning With passkey-only mode enabled, password login returns an error. Make sure you have at least two working passkeys before enabling. ::: ### Removing a Passkey 1. Go to **Profile → Security** 2. Find the passkey in the list 3. Click **Remove** --- ## Two-Factor Authentication (2FA) Add a second layer of protection with TOTP (Time-based One-Time Password). ### Enable 2FA 1. Go to **Profile → Security** 2. Click **Enable 2FA** 3. Enter your password 4. Scan the QR code with an authenticator app (Google Authenticator, Authy, 1Password, etc.) 5. Enter the 6-digit verification code 6. **Save your backup codes** — store them somewhere safe ### Using 2FA After entering your email and password, you are prompted for a 6-digit code from your authenticator app. Codes rotate every 30 seconds. ### Backup Codes Backup codes are single-use codes for when you lose access to your authenticator app. Each code can only be used once. Store them in a password manager or another secure location. ### Disable 2FA 1. Go to **Profile → Security** 2. Click **Disable 2FA** 3. Enter your password and a valid TOTP code to confirm --- ## Device Trust When you sign in with email/password from a device varsafe does not recognize, it sends a one-time verification code to your email. Once verified, the device is marked as trusted and skips email verification on future logins. ### How It Works 1. You sign in with email/password from an unrecognized device 2. varsafe sends a one-time code to your email 3. Enter the code to complete sign-in 4. The device is trusted for future logins Device trust is skipped for: - **Passkey login** — passkeys are already device-bound - **OAuth login** — the provider handles device verification - **First login after registration** — the device used to register is trusted automatically ### Managing Trusted Devices There is no trusted-device screen in the dashboard yet — the **Profile → Security** page lists active *sessions*, which is a different thing: signing a session out does not untrust the device it came from. Trusted devices are managed through the API: | Action | Request | | --- | --- | | List your trusted devices | `GET /me/trusted-devices` | | Untrust one | `DELETE /me/trusted-devices/{id}` | | Untrust every device | `DELETE /me/trusted-devices` | These act on your own devices and need your session, so they are called from a signed-in browser rather than with an API token. Untrusting a device means the next login from it requires email verification again. --- ## Session Management ### Viewing Sessions 1. Go to **Profile → Security → Active Sessions** 2. See all active sessions with: - Device and browser info - IP address - Last active time - Whether it is the current session ### Revoking Sessions - Click **Revoke** next to any session to end it immediately - Click **Revoke all other sessions** to keep only your current session Revocation is immediate — the revoked session cannot make further requests. ### Session Limits Each plan has a maximum number of concurrent sessions per user: | Plan | Max Sessions | | --------- | ------------ | | Developer | 5 | | Team | Unlimited | When you hit the session limit, you are prompted to revoke an existing session before signing in. ### Session Properties - **Duration** — a session expires 7 days after its last renewal - **Rolling renewal** — any authenticated request more than 1 hour after the last renewal silently extends the session by another 7 days, so active users stay signed in and idle sessions expire within a week - **Revocation** — changing or resetting your password revokes all sessions --- ## CLI Authentication The CLI supports two authentication modes: a browser-assisted login for humans, and API tokens for machines. `varsafe login` uses the browser flow by default. ### Browser Login (default) ```bash varsafe login ``` What happens: 1. The CLI requests a **human-readable confirmation code** (e.g., `brave-otter-374-jade-alice`) and prints it along with a dashboard URL 2. Your browser opens to the dashboard, where you sign in (if needed) and confirm that the code on screen matches the one in your terminal 3. The CLI waits on a **server-sent event stream** for the confirmation; if the stream is unavailable it falls back to polling 4. On confirmation, the CLI receives a session and stores it in the local credential store The confirmation code expires after **5 minutes**. Run `varsafe login --force` to re-authenticate when a session already exists. A CLI login lasts **180 days** from the moment you approve it, and ends sooner if the machine goes **90 days** without using it. When it ends, the next command asks you to run `varsafe login` again. You can revoke a login at any time from your profile in the dashboard. :::info The CLI has no email/password login. Passwords are entered in the browser, never in the terminal — so a headless machine authenticates with an API token, not with credentials. Account registration is a dashboard flow. ::: ### API Token For CI/CD and automation, use an [API token](/reference/api-tokens). There are several ways to pass a token, and passing it as a plain CLI argument is not one of them — the CLI refuses that outright. #### Secure prompt (recommended for interactive use) ```bash varsafe login -T # or varsafe login --token-prompt ``` Prompts for the token with masked input — nothing is saved to shell history. #### Stdin pipe ```bash echo "$VARSAFE_TOKEN" | varsafe login -t - # or from a file varsafe login -t - < /dev/shm/token.txt ``` The `-` argument tells the CLI to read the token from stdin. #### Environment variable (recommended for CI/CD) This is the method to reach for in a pipeline, and it involves no `login` step at all. The CLI accepts `VARSAFE_API_TOKEN`, with `VARSAFE_TOKEN` as a fallback alias: ```bash export VARSAFE_API_TOKEN=vs_at_vsafe_example varsafe run -- npm run dev ``` When the variable is set, the CLI authenticates with it instead of a stored user session. Inject it from your CI secrets store — see the [CI/CD guide](/guides/ci). :::warning `varsafe login` — in every form — persists the credential to the operating system keychain. A CI runner has no keychain, so `login` fails there regardless of how the token is supplied. In CI, set the environment variable and skip `login`. ::: #### Why there is no `--token ` flag {/* illustrative — this command intentionally fails */} ```bash # Rejected, by design varsafe login --token "$VARSAFE_TOKEN" ``` An inline value would be visible in shell history and to any process that can read `ps` output, so the CLI refuses it and points you at the alternatives above. `--token` with no value means "prompt me"; `--token -` means "read stdin". ### Credential Storage CLI credentials are kept in an **encrypted local store** (SQLite with AES-256-GCM encryption) under `~/.varsafe`. Credentials are never written in plaintext. --- ## Password Reset 1. Click **Forgot password?** on the login page 2. Enter your email address 3. Check your email for a reset link (valid for 1 hour) 4. Click the link and set a new password :::info Password reset revokes all existing sessions. You need to sign in again on all devices. ::: --- # API and machine-readable files Source: https://docs.varsafe.dev/reference/machine-readable varsafe publishes its public surface in formats a program can read directly. Point a client generator, an HTTP client, or an AI agent at these instead of scraping the docs. ## REST API | What | Where | | --- | --- | | API base URL | `https://api.varsafe.dev` | | OpenAPI 3.1 specification | [varsafe.dev/openapi.json](https://varsafe.dev/openapi.json), also served by the API at [api.varsafe.dev/openapi.json](https://api.varsafe.dev/openapi.json) | | Rendered API reference | [API reference](/api) — one page per operation, with curl, JavaScript and Python samples | Present a credential as `Authorization: Bearer ` — usually an [API token](/reference/api-tokens). Which credentials a route accepts is not uniform, so read each operation's `security` field; operations that also accept a CLI credential list the scopes it needs under `x-varsafe-cli-scopes`. Every operation's success and error bodies are described by JSON Schemas under `components.schemas`, so a generated client gets typed results. Objects list the properties sent today; ignore any you do not recognise, because a later release may add one. Errors return `{ "statusCode", "code", "message" }`. Branch on `code`; `message` is for humans and may change. The MCP endpoint's authentication errors use the OAuth shape `{ "statusCode", "error", "error_description" }` instead. ## Files for AI agents | File | Contents | | --- | --- | | [varsafe.dev/llms.txt](https://varsafe.dev/llms.txt) | What varsafe is, when to use it, and links to every interface below | | [docs.varsafe.dev/llms.txt](https://docs.varsafe.dev/llms.txt) | An index of every documentation page, including the API reference | | [docs.varsafe.dev/llms-full.txt](https://docs.varsafe.dev/llms-full.txt) | The full documentation as a single Markdown file | | [varsafe.dev/.well-known/mcp.json](https://varsafe.dev/.well-known/mcp.json) | Where the MCP endpoint lives and how to authorize against it | Any documentation page is also available as Markdown: append `.md` to its URL, for example [`/getting-started.md`](https://docs.varsafe.dev/getting-started.md). To give an agent scoped, audited access to secrets rather than documentation, connect it over MCP — see the [MCP guide](/guides/mcp). --- # Operations Guide Source: https://docs.varsafe.dev/reference/operations Day-two operations for teams running on varsafe: rotating secrets, recovering from bad changes, managing keys and tokens, and getting audit data out. Procedures assume the role required for the action ([roles reference](/reference/roles)). ## Secret Rotation ### Why Rotate? - Compromised credentials - Employee departure - Regular security hygiene - Compliance requirements ### Rotation via Dashboard **Requires:** developer role or above (owner/admin in protected environments). 1. Navigate to **Secrets** 2. Select the project and environment 3. Click the rotate icon next to the secret 4. Either let varsafe **auto-generate** a new value or enter one manually 5. Review the preview step 6. Confirm rotation The old value is immediately replaced, and a `secret.rotated` audit event is recorded. Applications using `varsafe run` receive the new value on their next start. ### Rotation via CLI For scripted rotation, pipe the new value in. A `production` environment is protected, so add `-y` to confirm up front in a non-interactive job: ```bash printf %s "$NEW_DATABASE_URL" | varsafe set DATABASE_URL --stdin -p my-api -e production -y ``` ### Rotation Best Practices A reasonable default schedule: | Secret Type | Rotation Frequency | | ------------------ | ------------------ | | API keys | Every 90 days | | Database passwords | Every 90 days | | Encryption keys | Annually | | After incident | Immediately | ### Handling Running Applications Applications read secrets at startup, so a rotation takes effect on the next start: 1. **Stateless apps** — restart to pick up new secrets 2. **CI/CD** — each run fetches fresh secrets automatically --- ## Version History & Rollback Every secret change creates a new version. Two recovery paths exist: rolling back a **single secret** and rolling back an **entire environment** to the state before a given operation. ### Single-Secret Rollback 1. Navigate to **Secrets**, select project and environment 2. Open the secret's **History** 3. Pick the version to restore and confirm The restore is recorded as a `secret.rolled_back` audit event. ### Environment Rollback Use this for bulk mistakes — a bad import, an accidental bulk delete: 1. Navigate to **Secrets**, select project and environment 2. Click **History** to see the environment's operations 3. Click **Rollback** next to the target operation 4. Review the **preview**: - Secrets to delete (created by the operation) - Secrets to restore (deleted by the operation) - Secrets to revert (updated by the operation) 5. Confirm Rollback restores the environment to its state *before* the selected operation and records an `environment.rolled_back` audit event. --- ## Encryption Key Management Each environment has an X25519 keypair used for [encrypted `.env` exports](/guides/encrypted-env). Private keys are stored server-side, encrypted by KMS — varsafe manages the environment keys for you. ### Auto-Rotation on Loss of Access By default, varsafe automatically rotates the keypair of every environment whose private key someone had accessed (tracked via `environment.key_accessed` audit events) as soon as they lose the ability to export keys — removed from the team, leaving, or downgraded out of owner/admin. Scheduled deactivation is not covered: it takes effect after a grace period, so there is no moment at deactivation time when rotating would help. They therefore cannot decrypt exports sealed *after* the rotation, even if they kept a file copy. Rotation is not retroactive: files sealed before it still open with the retained old key. If a key reached someone it should not have, rotate the underlying secrets too, not just the keypair. To switch between automatic and manual rotation: team settings → **Key rotation on loss of access** (owner/admin only). The change is recorded as a `team.settings_updated` audit event. ### After a Rotation Existing encrypted `.env` files keep working: the CLI transparently re-encrypts stale files against the new keypair on the next `varsafe run --env-file` use. Key lifecycle actions are all audited (`environment.key_generated`, `environment.key_rotated`, `environment.key_deleted`). Suggested key rotation: | Key Type | Rotation Frequency | | ----------------------- | --------------------------------- | | Environment keypairs | Auto on member removal, or annually | | After suspected exposure | Immediately | --- ## API Token Management Full reference: [API Tokens](/reference/api-tokens). Operationally: - **Create** one token per pipeline, scoped to its project and environment, with the shortest workable expiration - **Rotate** on schedule: dashboard → team settings → **API Tokens** → **Rotate**; the old token is invalidated immediately - **Revoke** compromised or unused tokens immediately; review last-used timestamps to find dead tokens --- ## Audit Log Management ### Event Catalog | Category | Actions | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------- | | Auth | `login`, `logout`, `login_failed`, `session_revoked`, `2fa_enabled`, `2fa_disabled`, `passkey_added`, `passkey_removed`, `passkey_only_enabled`, `passkey_only_disabled`, `device_trusted`, `device_untrusted`, `login_otp_verified`, `risky_login_detected` | | Secrets | `created`, `updated`, `deleted`, `accessed`, `rotated`, `exported`, `rolled_back` | | Environments | `created`, `updated`, `deleted`, `rolled_back`, `key_generated`, `key_accessed`, `key_access_denied`, `key_rotated`, `key_deleted` | | Projects | `created`, `updated`, `deleted` | | Teams | `created`, `updated`, `deleted`, `settings_updated`, `member_invited`, `member_added`, `member_removed`, `member_left`, `member_role_changed`, `member_deactivated`, `member_reactivated`, `ownership_transferred`, `invite_revoked` | | API tokens | `created`, `updated`, `rotated`, `revoked`, `deleted` | | SSO | `configured`, `updated`, `deleted` | | Billing | `email_updated`, subscription lifecycle events | | MCP | grant lifecycle, session lifecycle, and tool usage events | Actions are namespaced as `category.action` (e.g., `secret.accessed`, `team.member_removed`). ### Retention | Plan | Retention | | --------- | --------- | | Developer | 7 days | | Team | 90 days | Events past the retention window are pruned. For longer retention, export audit logs regularly and store them in your own compliance archive. ### CSV Export 1. Navigate to **Audit Log** 2. Apply any filters needed 3. Click **CSV** (the download button in the toolbar) Like the compliance exports, this writes the events currently loaded — filter to what you need before exporting. ### Alert-Worthy Events Configurable alerting is not currently exposed in the dashboard — review the audit log (or feed exports into your SIEM) and watch for these events: | Event | Concern | | ------------------------------ | ------------------------ | | Multiple `auth.login_failed` | Brute force attempt | | `secret.accessed` (production) | Verify authorized access | | `team.member_invited` (admin role) | Privilege escalation | | `auth.2fa_disabled` | Security downgrade | | `environment.key_access_denied` | Unauthorized key access attempt | | Unfamiliar IP addresses | Unauthorized access | --- ## Compliance Exports Structured JSON exports of the audit trail, formatted for common review frameworks. **Requires the Team plan and owner/admin role.** These exports support your compliance program — they are not certifications. There are two paths to a compliance export, and they do not produce the same file. **From the dashboard** — open **Audit Log**, then **Export** and pick a format. This builds the JSON in your browser from the events **currently on screen**, which is one page of 25. Filter first, and treat it as a sample or a spot check rather than a period export: | Format | Contains | | --- | --- | | **SOC 2** | Every loaded event, tagged `SOC2 Type II Audit Trail` | | **GDPR Article 30** | Only loaded events targeting a user, plus `auth.*` actions | | **HIPAA** | Only loaded events whose action mentions a secret or authentication | **From the API** — `POST /audit/export` produces the real period export: it queries the range server-side rather than the page you are looking at, adds a member access matrix for SOC 2, and includes the IP address on every event for HIPAA. It is capped at 50,000 events per export, so narrow the range for a busy team rather than requesting a year at once. :::note `POST /audit/export` requires an interactive owner or admin session — the audit endpoints do not accept a Bearer token, so this cannot be scripted with an API token. ::: --- ## Access Reviews ### Quarterly Access Review 1. **List team members:** - Dashboard → Teams → expand each team - Note members and their roles 2. **Review for:** - Departed employees (should be removed) - Role appropriateness (least privilege — see [roles](/reference/roles)) - Deactivated or inactive accounts 3. **Take action:** - Remove departed members (this auto-rotates environment keypairs they had key access to) - Downgrade excessive roles - Document review completion ### Reviewing API Tokens 1. Go to team settings → **API Tokens** 2. For each token, check: is it still needed, who created it, when was it last used (timestamp, IP, and user agent are tracked) 3. Revoke unused tokens --- ## Incident Response :::danger Act immediately — every hour of delay widens the blast radius. Rotate or revoke first, investigate second. :::
### Compromised Secret 1. **Rotate immediately** — Dashboard → Secrets → rotate, or `varsafe set` for automation 2. **Review audit logs** — who had access? When? From which IP addresses? 3. **Assess impact** — which systems use this secret? Was there unauthorized access? 4. **Document the incident** — timeline, actions taken, lessons learned
### Compromised Account 1. **Revoke sessions immediately** — Profile → Security → revoke all sessions 2. **Reset the password** — or enable passkey-only mode 3. **Review audit logs** — what did the account access? Were any secrets exported? 4. **Rotate affected secrets** — assume compromise; rotate everything the account could read 5. **Report to the team** — notify security, document for compliance
### Compromised API Token 1. **Revoke immediately** — team settings → API Tokens → Revoke 2. **Create a new token** — scoped at least as tightly as the old one 3. **Update configurations** — CI/CD systems, automated scripts 4. **Review token usage** — check audit logs and the token's last-used IP/user agent for unauthorized access
--- ## Troubleshooting Operational errors — sign-in, permissions, context resolution, rate limits — are collected in the [troubleshooting guide](/guides/troubleshooting), with the actual message text you would see and how to clear it. One case specific to this page: **`Action not permitted`** on an operation you believe your role allows. Check, in order, that your team membership is active, that your role permits the action ([roles reference](/reference/roles)), and that the environment is not protected — developers get read-only access in a protected environment, and viewers get none. --- # Roles & Permissions Source: https://docs.varsafe.dev/reference/roles Every team member has exactly one role. Roles control three things: **secret access** (read/write, per environment), **team management** (members, settings, API tokens), and **billing**. Use this page to pick the least-privileged role that covers what a member needs. ## The Six Roles | Role | Summary | | ------------- | -------------------------------------------------------------------------- | | **Owner** | Full access including team deletion and ownership transfer | | **Admin** | Full access to secrets, environments, and team management | | **Developer** | Read/write secrets in non-protected environments, read-only in protected | | **Operator** | Read-only access to all environments including protected | | **Viewer** | Read-only access to non-protected environments | | **Billing** | Billing management only — no access to secrets | ## Permission Matrix ### Secrets "Write" covers creating, updating, deleting, and rotating secrets, plus environment rollback. ### Team and Account Management ● full access · ◐ partial or read-only · ○ none ## Protected Environments Any environment can be marked **protected** in its settings. In a newly created project, **production is protected by default**. Protection restricts writes to owners and admins and narrows read access: - **Developer** — keeps read access, loses write access - **Operator** — read access everywhere is unchanged (already read-only) - **Viewer** — loses access entirely (protected environments are hidden) Use protection for any environment where an accidental change has production impact. ## Assigning Roles - Roles are assigned when inviting a member and can be changed later from the team page by an owner or admin - The default role for a new invite is **developer** - **Owner is not assignable.** Every team has exactly one owner — the assignable roles are admin, developer, operator, viewer, and billing ### Ownership Transfer The owner can transfer ownership to another member from the team settings. After the transfer, the previous owner becomes an admin, and the change is recorded in the audit log as a `team.ownership_transferred` event. ## Deactivated Members A deactivated member keeps their role but loses all access — secret reads and writes, team management, and billing — until reactivated. Deactivation and reactivation are recorded in the audit log. ## API Tokens Are Not Role-Scoped [API tokens](/reference/api-tokens) do not carry a member role. A token grants read/write access to the secrets of its team, optionally narrowed to specific projects and environments. Only owners and admins can create or manage tokens. --- # Security Model Source: https://docs.varsafe.dev/reference/security This page describes varsafe's security architecture as it is actually built. Every claim here is scoped: where a protection has limits, the limits are stated. ## Security Architecture Overview Every request follows the same path: 1. **TLS** — all connections are encrypted in transit 2. **Validate** — the session token or API token is hashed and verified 3. **Authorize** — role and environment permissions are checked in the application layer 4. **Store** — secret references and metadata go to the database; secret values go to the vault ## Secrets Storage ### Where Secret Values Live Secret values are stored in a dedicated secrets vault (Scaleway Secret Manager), encrypted at rest with KMS-managed keys. The application database stores **references only** — a pointer to the vault entry, never the value: **In the database:** - Secret keys (names, not values) - Vault references and version numbers - Timestamps and team/project/environment relationships - Audit events **In the vault:** - The actual secret values, versioned ### The Access Model, Honestly varsafe is a **server-side secrets manager, not an end-to-end encrypted system**. The varsafe API decrypts secret values in order to serve authorized requests — that is what makes `varsafe run` work. What protects your secrets is layered access control, not client-held keys: {/* claim-lint-ok — denies the E2E claim; the rule matches the phrase regardless of negation */} - Vault access is restricted to the varsafe API - Every read is authenticated, authorized against your [role](/reference/roles), and audited - Values are encrypted at rest (vault + KMS) and in transit (TLS) The same applies to [encrypted `.env` files](/guides/encrypted-env): each environment has an X25519 keypair, and **the server manages the environment keys** — private keys are stored server-side, encrypted by KMS. Encrypted `.env` files protect the file at rest and in transit, so they can be committed. Getting the private key needed to read one is authorized by the varsafe API and restricted to an interactive owner or admin session — but be clear about what that means: once the API hands the key over, the holder can decrypt every file sealed under it, offline, for as long as they keep it. Treat a private-key export as granting durable read access to that environment, and rotate the underlying secrets — not just the key — if it lands somewhere it should not have. ### Cross-System Write Safety Secret writes span two systems (the vault and the database). varsafe records each operation in a durable write-ahead log before touching either system, so a crash mid-operation is detected and recovered rather than leaving the vault and database silently inconsistent. ### `run` vs `export` - **`varsafe run`** injects secrets directly into the child process environment. Nothing is written to disk. - **`varsafe export`** writes a file — deliberately, at your request. Env-format exports are encrypted by default; plaintext requires the explicit `--plain` flag. ## Authentication ### Session Tokens varsafe uses opaque session tokens: - **Generation** — cryptographically secure random bytes - **Storage** — tokens are hashed before storage; a database leak does not expose usable sessions - **Lifetime** — sessions expire 7 days after their last renewal, with rolling renewal on activity (at most hourly). A stolen idle cookie dies within a week of its last legitimate use. - **Cookies** — `HttpOnly`, `Secure` (in production), `SameSite=Lax` ### API Tokens API tokens (`vs_at_` prefix) are stored as salted hashes, always carry an expiration, and can be scoped to specific projects and environments. See [API Tokens](/reference/api-tokens). ### Multi-Factor Authentication **TOTP (Time-based One-Time Password):** - Standard TOTP algorithm (RFC 6238) - Works with Google Authenticator, Authy, 1Password, etc. - Single-use backup codes for recovery **Passkeys (WebAuthn):** - Biometric authentication (Touch ID, Face ID) or hardware security keys - Phishing-resistant — credentials are bound to the varsafe domain - **Passkey-only mode** disables password login entirely (requires at least 2 registered passkeys) **Device Trust:** - Email/password logins from unrecognized devices require a one-time email code - Verified devices are remembered; passkey and OAuth logins skip the check (already device-bound) - Manage trusted devices from **Profile → Security** See [Authentication](/reference/authentication) for setup procedures. ## Authorization Authorization is enforced in the application layer on every request: the actor's team membership and role are checked against the resource's team, project, and environment. ### Role-Based Access Control Six roles (owner, admin, developer, operator, viewer, billing) control secret access, team management, and billing. Protected environments further restrict writes to owners and admins. The full matrix lives in [Roles & Permissions](/reference/roles). By default, production is marked as protected in new projects. Any environment can be protected via its settings. ### API Token Authorization API tokens are scoped to a single team, optionally narrowed to specific projects and environments — scoping is enforced server-side on every request. Tokens have their own audit identity. ## Data Isolation ### Multi-Tenant Architecture varsafe isolates teams with two independent layers: 1. **Application-layer scoping** — every query is scoped to the teams the actor belongs to; ownership is checked before any read or write 2. **PostgreSQL row-level security (RLS)** — high-risk tables (secret references, environment keypairs, API tokens, and others) additionally carry database-enforced RLS policies keyed to the request's team, so even a defective query cannot cross tenant boundaries Secret values in the vault are namespaced per team. ### Namespace Structure ## Audit Trail Every security-relevant action is logged with actor, target, team, IP address, and user agent. ### Audit Properties - **Append-only** — the application only ever inserts audit events; there is no API to modify or delete them - **Complete** — every secret access, export, and mutation is logged, including access by API tokens - **Retention-bounded** — events are retained per plan (7 days on Developer, 90 days on Team) and pruned after the window; export regularly if you need a longer archive - **Exportable** — CSV export from the dashboard, plus structured JSON exports formatted for SOC 2, GDPR Article 30, and HIPAA reviews (Team plan) ### Composed secrets disclose their whole closure Reading one composed secret reveals the value of every secret it embeds, so the audit event records the full **closure** — reading `DATABASE_URL` is logged as access to `DATABASE_URL`, `DB_USER` and `DB_PASS`, not just the key requested. A compromise review that saw only the requested key would clear a credential that in fact leaked. Two consequences worth planning around: - **Granting read access to a composed secret grants read access to its inputs**, in effect. There is no way to read the resolved value without learning what it was built from. Composition does not narrow disclosure; it narrows *duplication*. - **A composed read's closure is always recorded in full.** One secret may resolve against at most 64 others, which is deliberately below the 200-name cap on a single audit event — so the names of every credential a composed read disclosed are retained, not truncated. Exceeding that bound is refused at resolution time rather than logged incompletely. (Whole-environment exports of more than 200 secrets still record a `keysTruncated` flag with an exact total; that is a bulk read where the caller asked for everything.) References never cross an environment, project, or team boundary, so a composed secret's closure is always inside the tenant the reader was already authorized for. See [Operations](/reference/operations#audit-log-management) for the event catalog and export procedures. ## Network Security ### TLS All connections — dashboard, CLI, and API — use TLS. ### Rate Limiting Authentication endpoints have dedicated per-IP limits: | Endpoint | Limit | | ----------------- | ------------------------ | | Sign-in | 10 per minute | | Sign-up | 10 per minute | | Forgot password | 5 per 5 minutes | | Two-factor verify | 10 per minute | | Other auth routes | 100 per minute | General API traffic is throttled across three windows (per second, per 10 seconds, per minute — 10/50/200 for unauthenticated traffic). Authenticated users receive a 5x multiplier. ## Incident Response ### Compromise Response If a secret is compromised, act immediately: 1. **Rotate** — use the rotation workflow to issue a new value 2. **Revoke** — invalidate any leaked sessions or API tokens 3. **Audit** — review the audit log for unauthorized access 4. **Report** — document the incident for compliance See [Operations → Incident Response](/reference/operations#incident-response) for the full runbooks. ### Session Revocation Sessions can be revoked: - By the user (logout, or per-session from Active Sessions) - On password change or reset (all sessions) - Automatically (expiration) Revocation is immediate — no waiting for token expiry. ## Security Best Practices
### For developers 1. **Never commit plaintext secrets** — use `varsafe run`, or encrypted exports when a file is unavoidable 2. **Use passkeys** — phishing-resistant, stronger than passwords 3. **Enable 2FA** — protects password-based accounts 4. **Review audit logs** — watch for anomalies
### For admins 1. **Principle of least privilege** — viewer or operator for most members, developer only where writes are needed 2. **Protect sensitive environments** — protection restricts writes to owners and admins 3. **Regular access reviews** — remove departed members; their key access triggers automatic keypair rotation 4. **Scope API tokens** — one token per pipeline, narrowed to its project and environment
### For security teams 1. **Export audit logs** — feed CSV or JSON exports into your SIEM before the retention window prunes them 2. **Test incident response** — simulate a compromised secret and a compromised token 3. **Review token inventory** — expirations, scoping, last-used timestamps
## Compliance varsafe is **designed to support** common compliance programs — it is not a substitute for your own certification, and this page makes no certification claims. | Framework | What varsafe provides | | ----------- | ---------------------------------------------------------------------------- | | **SOC 2** | Append-only audit trail, role-based access control, encryption at rest/in transit, access-review reports | | **GDPR** | Audit export formatted for Article 30 processing records | | **HIPAA** | Access controls, per-access audit logging, structured access export | | **PCI DSS** | Centralized secrets management with access logging | See [Operations](/reference/operations#compliance-exports) for export procedures. ### Not yet - **No SOC 2 report.** varsafe has not been through a SOC 2 audit. - **The audit log is not tamper-evident** against someone holding database credentials. The application cannot rewrite it, but it is not hash-chained; tamper evidence is designed and not shipped. - **No alert delivery yet** to Slack, Microsoft Teams or PagerDuty. ## Reporting Security Issues Security vulnerabilities should be reported privately: **[Contact form](https://varsafe.dev/contact)** — choose the "Security Issue" category. Please do not discuss vulnerabilities publicly before they are resolved, and do not test against production systems without authorization. We aim to acknowledge reports within 24 hours and provide fixes within 90 days.