Security model
How varsafe stores, transmits and controls access to your secrets, stated precisely.
This page answers the questions a security review asks about varsafe, in the order it usually asks them. Every statement describes the shipped code. Where a protection has a limit, the limit is on the page, and the last section lists what varsafe does not do yet.
At a glance
| Question | Answer |
|---|---|
| Can varsafe read my secrets? | The API can, to serve authorized requests: values are encrypted in transit and at rest, and decrypted by the API for each authorized read. Details |
| Where are values stored? | In a dedicated secrets vault (Scaleway Secret Manager) in the EU, separate from the application database. |
| How are teams kept apart? | Application checks on every request, plus PostgreSQL row-level security on the tables that hold secrets, tokens, agent grants, invites, SSO settings and the audit log. |
| Who can change production? | Owners and admins: production is a protected environment by default. An API token can change it only when it was created for production. Details |
| Is access logged? | Every value read and every change, append-only, with the actor, the client and the IP address. |
| What happens when someone leaves? | Removing a member revokes their CLI logins, their agent grants and the API tokens they created, in the same step. |
| How do agents get access? | Over MCP, through an OAuth consent screen. The agent gets the scopes the person approved, capped by that person’s role, per team. |
| Certifications? | None yet. No SOC 2 report. |
How a request is handled
Clients
- CLISigned in from the browser
- DashboardA browser session
- AI agentsMCP, with an OAuth 2.1 grant
- PipelinesA scoped API token
varsafe API
- AuthenticateThe session, CLI login, grant or token is verified and must still be live
- AuthorizeTeam membership, role, environment protection and token scope
- AuditEvery value read and change, append-only
Storage
- PostgreSQLKeys, vault references, metadata and the audit log; row-level security per team
- Secrets vaultThe values, versioned: Scaleway Secret Manager in the EU, encrypted at rest
- TLS on every connection: dashboard, CLI, agents and pipelines.
- Authenticate. The credential is checked against its store and must not be expired or revoked.
- Authorize. The actor’s team membership, role, environment protection and, for a token or grant, its scope are checked against the project and environment.
- Audit. Reads of values and every change are recorded.
- Store. Names and metadata go to the database; values go to the vault.
Where secrets are stored
Where secret values live
Secret values live in a dedicated secrets vault, Scaleway Secret Manager in the EU, encrypted at rest by the vault. Each team’s values sit under their own path in the vault. Only the varsafe API holds credentials for it. Each environment’s last 10 saved versions are kept, so a bad change can be rolled back.
The application database holds:
- secret names, never values at rest
- vault references and version numbers
- the team, project and environment each secret belongs to
- the audit log
To serve reads quickly, the API also keeps recently read secret documents in a short-lived cache, in its own memory and in its Valkey instance, for a few minutes (180 seconds by default). Both layers hold only ciphertext: AES-256-GCM, under a data key protected by the key-management service, with each entry bound to its slot so it cannot be moved to another. A write removes the entry, but that removal is best-effort, so for up to the cache lifetime a read can return the value from before the last write.
One exception, stated for completeness: while a write is in flight, the operation record carries the new value encrypted with a key-management service, so a crash between the vault and the database can be recovered. The value is cleared when the operation completes. An operation that fails for good keeps its encrypted value on the record.
Writes stay consistent
A secret write touches two systems, the vault and the database. varsafe records each write as a durable operation before touching either, and a background worker finishes or retries any operation a crash interrupted. A failed write is detected and retried; it does not leave the vault and the database silently disagreeing.
Encrypted .env files
varsafe export writes an encrypted .env by default. Each environment has an X25519 keypair. Each value is sealed with HKDF-SHA256 and AES-256-GCM under a fresh ephemeral key, and bound to its name and environment, so a value cannot be moved to another key or file unnoticed. A sealed file can be committed: it reveals variable names, not values.
The environment keys are managed by varsafe, not by you. The private keys are stored server-side, encrypted by a key-management service, and are not handed out to anyone. varsafe run --env-file sends a file’s encrypted values to the API, which opens them for a caller allowed to read the environment’s values, under the same rule as varsafe run, and records the read as environment.sealed_values_opened, by variable name. If any value in the file was altered, the whole file is refused.
Access is checked each time a file is opened, so someone who loses access to the environment can no longer open its files, even copies they kept. A sealed file is still a snapshot: anyone allowed to read the environment can open it, including values since changed or deleted, for as long as the key that sealed it exists. Deleting the environment’s keys is what stops such files from opening. Each value is tamper-evident, but the file as a whole is not: a removed line, or an older copy put back, still opens.
The access model, honestly
varsafe is a server-side secrets manager, not an end-to-end encrypted system. The varsafe API decrypts secret values to serve authorized requests, which is what makes varsafe run work. Your secrets are protected by layered access control, not by keys only you hold:
- only the varsafe API can reach the vault
- every read is authenticated, checked against your role or the token’s scope, and audited
- values are encrypted in transit (TLS) and at rest (the vault)
Who can read and change what
Access is checked by the API on every request. The dashboard hides what you cannot do, but it does not enforce anything.
Members get access from their role. There are six: owner, admin, developer, operator, viewer and billing. A protected environment accepts changes from owners and admins only. Developers and operators can still read it; viewers and billing members cannot. Production is protected by default in every new project, and an environment with the slug production is closed to viewers even if someone removes its protection. The full matrix is in Roles and permissions.
API tokens belong to one team. They can be narrowed to projects and environments, and to read-only. A token is not a member and has no role, so its scope stands in for one: a write token changes a protected environment only when it was created for that environment, which an owner or admin chose when creating it. A token for all environments reads protected environments but cannot change them. A token keeps that access if its environment is protected later, and older tokens that named environments were converted to the environments those names matched at the time. See API tokens.
AI agents connect over MCP. A person approves each connection on a consent screen. The agent gets only the scopes approved there, capped by that person’s role. Each team named on the consent screen is its own grant, and a tool call runs under one team. A viewer cannot grant an agent any secret scope. Over MCP, protected-environment rules apply against the approving member’s current role, and an agent using an API token follows the token’s rule. A grant expires after 90 days without use, and you can revoke it at any time. See the MCP guide.
Environment keys (for encrypted .env files) are created by an environment’s first export, under the same access as reading its values. Rotating or deleting them takes an interactive owner or admin session; tokens, CLI logins and agents are refused. No one can download a private key.
Tenant isolation
Teams are isolated twice:
- In the application. Every query is scoped to the team the actor is acting for, and ownership is checked before any read or write.
- In the database. PostgreSQL row-level security policies, keyed to the request’s team, cover the data listed below. The API refuses to report ready if its database role could bypass those policies.
Covered by row-level security
- Secret references
- Secret dependencies
- Environment keypairs
- API tokens and their environment scopes
- Agent grants
- Team invites
- SSO providers
- The audit log
Not covered by row-level security
- Projects
- Environments
- Teams
- User sessions
These are protected by the application checks only. Their rows carry no secret values.
Team
acme
Project
api-backend
development
DATABASE_URL
JWT_SECRET
REDIS_URL
staging
DATABASE_URL
JWT_SECRET
REDIS_URL
production
DATABASE_URL
JWT_SECRET
REDIS_URL
Project
web-dashboard
Row-level security: each team's rows are visible only to that team
Team
globex
Its projects, environments and secrets
Unreachable from acme
Composed secrets cannot reference another environment, project or team, so a composed value never pulls data from outside the tenant the reader was already authorized for.
Authentication
Signing in
| Method | Notes |
|---|---|
| Email and password | Optional TOTP two-factor (RFC 6238) with 10 single-use backup codes |
| GitHub, GitLab, Google, Bitbucket | OAuth. A provider sign-in is not silently linked to an existing account with the same email; a signed-in account with a confirmed email connects one explicitly, from its profile |
| Passkeys | WebAuthn, bound to the varsafe domain |
| SAML 2.0 and OIDC single sign-on | Team plan. The email domain must be verified by a DNS record first |
Setup is in Authentication.
Sessions
- Lifetime: 7 days after the last renewal. Activity renews a session at most once an hour, so an idle session expires within a week.
- Cookies:
HttpOnly,Secure,SameSite=Lax. - Revocation: sign-out, per-session revoke from your profile, and password reset. A revoked browser session stops working within 30 seconds.
- Password reset revokes every session and also the account’s CLI logins, agent refresh tokens and API tokens.
CLI logins
The CLI signs in through the browser: the password never reaches the terminal. A CLI login is its own credential, separate from the browser session. It lasts at most 180 days and ends after 90 days unused, and it can be revoked from the dashboard. On the machine it is stored encrypted (AES-256-GCM), with the key in the operating system keychain. On a machine with no keychain, a login falls back to a machine token, stored unencrypted in a directory only the user can read, and bounded instead: one team, the projects and environments approved, at most 30 days.
API tokens
API tokens (vs_at_ prefix) are stored only as a salted SHA-256 hash and shown once. Every token created today expires: 90 days at most on Developer, 365 on Team. A token created before that rule, with no expiry, stops working after 90 days unused. Revocation applies to the next request.
Removing access
| Event | What happens |
|---|---|
| A member is removed, or leaves | Their CLI logins for the team, their agent grants, and the API tokens they created are revoked in the same step. An agent grant that also covered other teams is revoked whole; they reconnect to keep those |
| A member’s role changes | The same revocation runs, so their tokens and grants do not outlive the old role |
| A member is deactivated | Scheduled with a 7-day grace period. Access, and the tokens they created, keep working until it ends, then stop |
| A password is reset | Every session, CLI login, agent refresh token and API token of that account is revoked |
Environment keys after removal. Private keys are no longer handed out, but earlier versions let an owner or admin download one, and a downloaded key decrypts every file sealed under it, offline. varsafe no longer rotates an environment’s keypair when such a person loses access, and rotation would not be retroactive anyway: a downloaded key still opens every file sealed under it, offline, and a value someone already saw stays seen. Rotate the underlying secrets at their source.
Audit trail
Every value read and every change is recorded with the actor (a person, a token or an agent), the team, the target, and, for requests, the IP address and user agent.
- Append-only. The application’s database role cannot update or delete audit rows, and inserts go through a single database function. Two narrow functions change history: the retention cleanup deletes events past the retention window, and deleting a user anonymizes them in every event that names them, theirs and those other people’s actions recorded about them, while the events stay. The address an invitation was sent to stays: it names an address, not an account. Each records itself in the log.
- Retention. Events are visible for 7 days on Developer and 90 days on Team. Events older than 90 days are deleted for every team. Export what you need to keep longer.
- Stored events. Every event reaches the log through a queue that keeps a copy of it: for 30 days once the event is in the log, 90 days for one that never got there, and longer only while a background job that needs it has not finished with it. Deleting an account erases it from the copies already stored the way it is erased from the log, and the events the deletion itself writes, such as leaving its teams, name the person by id only. An event that a request still in flight stores during the deletion keeps the address until it is removed.
- Exports. On the Team plan, owners and admins export any period of a team’s audit log, built on the server from every event of the period: CSV, or JSON formatted for SOC 2, GDPR Article 30 and HIPAA reviews. They can also generate a member access review. See Compliance exports.
Composed secrets
Reading one composed secret reveals 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 only the key requested. A review that saw only the requested key would clear a credential that in fact leaked.
- Read access to a composed secret is, in effect, read access to its inputs. Composition reduces duplication, not disclosure.
- The closure is always recorded in full. One requested key may resolve at most 64 secrets, below the 1,000-name cap on a single audit event, and a deeper composition is refused rather than logged incompletely. A bulk read of more than 1,000 secrets records the first 1,000 names, a
keysTruncatedflag and the exact total.
See Operations for the event catalog.
Machines and pipelines
varsafe runwrites nothing to disk. Values go into the child process’s environment and are gone when it exits.- The CLI’s own token does not leak into the child.
runremovesVARSAFE_API_TOKENandVARSAFE_TOKENfrom the command’s environment unless you pass--pass-credential. A command running as the same user can still callvarsafewith that user’s stored login; the CLI cannot prevent that. - A secret cannot take over the process.
runrefuses to inject variables that control how a program loads code, such asPATH,LD_PRELOAD,DYLD_INSERT_LIBRARIESandNODE_OPTIONS, so write access to one secret does not become code execution on every machine that reads it. - Plaintext files are opt-in.
varsafe exportencrypts the.envformat by default; plaintext needs--plain. - No secret in argv. The CLI refuses an API token passed as a command-line value, which would show in shell history and
ps.
Network and rate limits
All traffic uses TLS. Sign-in endpoints have per-IP limits:
| Endpoint | Limit |
|---|---|
| Sign-in | 10 per minute |
| Sign-up | 10 per minute |
| Password reset request | 5 per 5 minutes |
| Two-factor | 10 per minute |
| Resend verification email | 3 per 5 minutes |
| Other sign-in routes | 100 per minute |
The rest of the API is throttled over three windows:
| Window | Per IP, without a credential | Per signed-in user or token |
|---|---|---|
| 1 second | 10 requests | 50 requests |
| 10 seconds | 50 requests | 250 requests |
| 1 minute | 200 requests | 1,000 requests |
Where data lives
varsafe is hosted in the EU. Every company that processes data for varsafe, what it does and where, is listed on the service providers page.
Compliance
varsafe is designed to support compliance programs. It does not certify you, and this page makes no certification claim.
| Framework | What varsafe provides |
|---|---|
| SOC 2 | Append-only audit trail, role-based access control, encryption in transit and at rest, member access reviews |
| GDPR | EU hosting, an audit export formatted for Article 30 records |
| HIPAA | Access controls, per-access audit logging, an access export with IP addresses |
| PCI DSS | Centralized secrets management with access logging |
What varsafe does not do yet
- No SOC 2 report. varsafe has not been through a SOC 2 audit.
- Not end-to-end encrypted. The API decrypts values to serve them (see above).
- The audit log is not tamper-evident against someone holding database credentials. The application cannot rewrite it, but it is not hash-chained.
- Session tokens are not hashed in the database. API tokens are.
- No new-device check at sign-in. Use two-factor or passkeys.
- No alert delivery to Slack, Microsoft Teams or PagerDuty, and no outbound webhooks. Feed audit exports to your SIEM instead.
- No enforced SSO. Configuring SSO for a domain does not disable password or OAuth sign-in for its users.
If something leaks
- Rotate the secret: dashboard, or
varsafe setfrom a script. - Revoke the leaked session, CLI login, agent grant or API token.
- Review the audit log for what that credential read.
- Record the incident for your compliance program.
Step-by-step runbooks: Operations → Incident response.
Reporting security issues
Report vulnerabilities privately through the contact form, category “Security issue”.
Please do not disclose a vulnerability publicly before it is fixed, and do not test against production systems without authorization. We aim to acknowledge reports within 24 hours and to ship fixes within 90 days.