Operations guide
Operational reference — secret rotation, version history and rollback, key management, audit events, retention, exports, and incident response.
Each procedure names the role it needs (roles reference).
| I need to | Go to |
|---|---|
| Replace a secret’s value | Secret Rotation |
| Undo a change to one secret | Single-secret rollback |
| Undo a bad import or bulk delete | Environment rollback |
| Offboard someone | Access Reviews |
| Get audit data into a SIEM or an auditor’s hands | Audit Log Management, Compliance Exports |
| Respond to a leak | Incident Response |
Secret rotation
Requires: owner, admin or developer; owner or admin in a protected environment.
Rotation via dashboard
- Open Secrets and pick the project and environment
- Open the secret’s actions menu and choose Rotate
- Under Value generation, choose Auto-generate (32 random bytes, base64-encoded) or Custom value
- Click Continue, check the summary, and click Confirm Rotation
The new value replaces the old one at once, the old one stays in the secret’s history, and the audit log records secret.rotated. Programs started with varsafe run get the new value the next time they start.
Rotation via CLI
For scripted rotation, pipe the new value in. production is protected, so a non-interactive job adds -y to confirm up front, and a job using an API token needs a write token created for production:
printf %s "$NEW_DATABASE_URL" | varsafe set DATABASE_URL --stdin -p my-api -e production -y
A value set from the CLI is recorded as secret.updated (or secret.created for a new key).
Rotation best practices
Rotate the credential at its source (the database, the payment provider) first, then store the new value in varsafe. A reasonable schedule:
| Secret type | Rotate |
|---|---|
| API keys | Every 90 days |
| Database passwords | Every 90 days |
| Encryption keys | Every year |
| Anything someone who left could have seen | When they leave |
| Anything involved in an incident | Immediately |
Handling running applications
Programs read their environment when they start, so a rotation takes effect on the next start:
- Long-running services: restart or redeploy them
- CI/CD: each run fetches current values, so nothing to do
Version history & rollback
Every save to an environment, of one secret or many, creates a new version of it, and each environment keeps its last 10. A value can be restored while the version that saved it is still among them. There are two ways back: one secret, or a whole environment.
Single-secret rollback
- Open Secrets and pick the project and environment
- Open the secret’s History (Version History)
- Click Restore on the version you want and confirm
The audit log records secret.rolled_back.
Environment rollback
For bulk mistakes, such as a bad import or an accidental bulk delete:
- Open Secrets and pick the project and environment
- Click Environment history in the toolbar
- Click Rollback to before on the operation to undo
- Read the Rollback Preview: the secrets that will be deleted (the operation created them), restored (it deleted them) and reverted (it changed them)
- Click Confirm Rollback
The environment returns to its state before that operation, and the audit log records environment.rolled_back, filed under the environment and naming the keys it deleted (deletedKeys), restored (restoredKeys) and reverted (revertedKeys).
Encryption key management
Each environment has an X25519 keypair used to seal encrypted .env files. varsafe generates and stores the keys, with the private keys encrypted by a key-management service, and no private key leaves varsafe.
| Event | When | Who |
|---|---|---|
environment.key_generated |
An environment’s first varsafe export creates its key |
Anyone allowed to read the environment’s values |
environment.sealed_values_opened |
varsafe run --env-file opens a sealed file through the API; recorded with the variable names and never the values |
Anyone allowed to read the environment’s values |
environment.key_rotated |
The environment’s keys are rotated | An owner or admin signed in to the dashboard |
environment.key_deleted |
The environment’s keys are deleted; names every key it deleted (keyIds), those rotated out included |
An owner or admin signed in to the dashboard |
API tokens, CLI logins and agents are refused for both rotation and deletion. The dashboard has no control for either yet, so in practice neither can be done today; the API routes exist for when it does.
Files sealed before a rotation keep opening, and the CLI re-encrypts them to the new key. Once an environment’s keys are deleted, no file sealed to them opens again; the next export creates a new key.
Earlier versions let an owner or admin download an environment’s private key (environment.key_accessed), rotated the keys such a person had downloaded when they lost access, and had a team setting to turn that off. With no private key leaving varsafe there is nothing left to rotate, so the rotation and its setting are gone; their records (environment.key_rotation_skipped, environment.key_rotation_failed, team.settings_updated) remain in older audit logs and are no longer written.
API token management
Full reference: API tokens. In practice:
- Create one token per pipeline, read-only unless it writes, scoped to its project and environment, with the shortest lifetime you can rotate on
- Rotate from Access → API tokens, token menu → Rotate. The old token stops working at once; the new one keeps the old expiry date
- Revoke compromised or unused tokens from the same menu. Each row shows when a token was last used, from which IP address and with which client, so an idle token, or one used from somewhere unexpected, stands out
Audit log management
Every value read and every change is in the Audit Log, with the time it happened, the actor (a person, a token or an agent), the team, the target, and the IP address and user agent of the request. A record keeps the time of its action even when the log takes it later, after a retry or a recovery. A signed-in request’s source (dashboard, CLI, API token or AI agent) is the credential it carried, never a header the caller chose; what varsafe does on its own is system. Any active member can read their team’s audit log.
Event catalog
Actions are named category.action, for example secret.accessed or team.member_removed.
| Category | Actions |
|---|---|
auth |
registered, profile_updated, login, login_failed, login_refused, second_factor_failed, reauthentication_failed, two_factor_locked, logout, session_revoked, session_renewed, sessions_viewed, password_changed, password_reset_requested, password_reset, password_reset_failed, credential_revoked, verification_email_sent, email_verified, credential_rejected, revoked_credential_used, 2fa_enabled, 2fa_disabled, two_factor_secret_viewed, backup_codes_regenerated, two_factor_device_trusted, passkey_added, passkey_renamed, passkey_removed, passkey_only_enabled, passkey_only_disabled, provider_connected, provider_disconnected, risky_login_detected, login_otp_verified, login_otp_failed, login_otp_resent, device_trusted, device_untrusted, user_deleted |
secret |
created, updated, deleted, renamed, accessed, access_denied, rotated, revealed, exported, compared, rolled_back, versions_pruned |
environment |
created, updated, protection_changed, deleted, duplicated, rolled_back, key_generated, key_accessed, key_access_denied, key_rotated, key_rotation_skipped, key_rotation_failed, key_deleted, sealed_values_opened, sealed_values_open_denied |
project |
created, updated, deletion_requested, deleted |
team |
created, updated, deleted, settings_updated, member_invited, invite_accepted, invite_accept_refused, invite_revoked, member_added, member_removed, member_left, member_role_changed, member_swapped, member_deactivated, members_deactivated, member_reactivated, ownership_transferred, deleted_with_account, access_denied |
api_token |
created, updated, rotated, revoked, deleted, access_denied |
cli |
grant_created, grant_revoked, device_authorization_started, device_authorization_claimed, device_authorization_approved, device_authorization_denied, access_denied |
mcp |
grant_created, grant_revoked, grant_expired, consent_denied, secrets_listed, secrets_injected, tool_refused, token_replay_detected, client_registered |
oauth |
client_created, client_updated, client_secret_rotated, client_deleted |
sso |
configured, updated, deleted, domain_challenge_issued, domain_verified, domain_verification_failed, domain_reverification_missed, domain_verification_revoked, login_refused, assertion_refused |
alert |
anomaly_detected |
audit |
viewed, export_generated, access_review_generated, log_cleaned, actor_anonymized |
billing |
email_updated, portal_opened, checkout_started, payment_failed, customer_unlinked |
subscription |
created, activated, status_changed, cancellation_scheduled, cancellation_withdrawn, canceled, seats_changed |
platform_admin |
stats_viewed, users_viewed, health_viewed, audit_viewed, allowlist_viewed, allowlist_added, allowlist_removed, access_denied |
By subject: The operators’ log · Sign-in, second factor and sessions · CLI logins, API tokens and agents · Invitations and members · Reading and changing secrets · Encryption keys · Single sign-on · Billing · Deleting and duplicating · What a change records · Refused attempts
The operators’ log
A few of these are kept in varsafe’s own operators’ log rather than in a team’s: auth.login_failed and auth.credential_rejected, which no verified account stands behind (a sign-in through a provider’s callback that never proved which team it was for is one of them), auth.password_reset_failed, a password reset refused for its link (one varsafe never sent, or one already used or expired), with the address it came from and at most five a minute from one address, team.deleted_with_account, audit.log_cleaned and audit.actor_anonymized, the platform_admin actions, which record what varsafe’s administrators looked at or changed (a listing of users names its page and what was searched for; adding an address already on the allowlist, or removing one that is not, changes nothing and records nothing), and the OAuth clients the authorization server knows: mcp.client_registered for a client an MCP client registered for itself, which anyone may do (registration: dynamic_registration, with the caller’s address), or one read from its metadata document (metadata_document, recorded by the system), naming the client and at most ten of its callbacks with how many it has; and oauth.client_created, client_updated (naming the fields that changed, never their values), client_secret_rotated and client_deleted for each one an operator manages, never with a secret. Changing a client and the record of it commit together; saving a client as it was records nothing.
Sign-in, second factor and sessions
A refused sign-in to an existing account (auth.login_refused, with the reason; for a passkey, only when the passkey it named is registered and its answer was refused — a bad signature, another site, or a counter behind the stored one, as a copied key shows), a wrong second-factor code (auth.second_factor_failed), two-factor locking after repeated wrong codes (auth.two_factor_locked), a wrong code sent to confirm a sign-in from a new device (auth.login_otp_failed, saying whether it used up the last attempt) and that code sent again (auth.login_otp_resent), and a wrong current password given to re-confirm identity before changing the password, turning two-factor on or off, reading the authenticator secret, making new backup codes or turning passkey-only sign-in off (auth.reauthentication_failed, naming the step) belong to the account: its owner sees them under Security activity, and so do the owners and admins of each of its teams. So do the steps around its second factor: showing the authenticator secret again (auth.two_factor_secret_viewed), new backup codes (auth.backup_codes_regenerated), and a device trusted for thirty days at a two-factor sign-in (auth.two_factor_device_trusted). A backup code spent at a sign-in is that sign-in’s factor. Each email sent to confirm the account’s address (auth.verification_email_sent: at sign-up, and every resend), verifying it (auth.email_verified) and every device trusted for the first time (auth.device_trusted: the device that verified the address, one a sign-in code cleared, or one trusted from the sign-in code page) are the account’s as well; a device trusted again is not a new record.
A password reset is recorded when its link is sent (auth.password_reset_requested, with the address that asked; an address with no account records nothing) and when it is completed; each API token and CLI login it revokes is on the log of every team that held it too (api_token.revoked, cli.grant_revoked, reason password_reset). A session that ends is recorded by why it ended: its holder signing out (auth.logout), expiry, or a revocation with its reason (auth.session_revoked): the owner ending it, a password reset, or a password change signing the other sessions out. The session a renewal replaces is named by the renewal’s record.
A passkey registered (auth.passkey_added) names what its authenticator said of itself: whether the passkey can sync (deviceType) and is backed up, its model (aaguid) and, for a well-known one, its provider (authenticator); renaming one (auth.passkey_renamed) gives the name it had and the name it has. A new name or picture for the account (auth.profile_updated) is on its record and its teams’, since other members see it by them: the name it had and the name it has, and whether the picture changed, never the picture. Saving a passkey’s or the profile’s name as it was records nothing. A team admin looking at a member’s sessions, which shows that member’s devices and the addresses they signed in from, is recorded in that team as auth.sessions_viewed, naming the member and how many sessions it showed.
CLI logins, API tokens and agents
A varsafe login is on record from the machine that asked, named by its address and user agent as varsafe saw them and the host it claimed (requestedFrom): starting one is on the operators’ log (cli.device_authorization_started), since nothing names an account until someone opens it; opening its approval page (cli.device_authorization_claimed) and denying it (cli.device_authorization_denied) belong to the member, who sees them under Security activity, as do the owners and admins of each of their teams; approving it (cli.device_authorization_approved) is on the log of each team it was approved for, where only owners and admins see the requesting machine’s address. The login it creates (cli.grant_created) names, for each team, the scopes it was given there, when it ends (expiresAt) and how the CLI keeps it (credentialProtection). Declining to let an AI agent connect (mcp.consent_denied, naming the app) is the member’s too, and a connection unused for ninety days (mcp.grant_expired) is named by its app. Ending a CLI login is on the log of every team it reached, with why (cli.grant_revoked, reason): its holder signing out from the CLI (signed_out), its owner revoking it from another device (revoked_by_owner), ninety days unused (idle_expiry, recorded by the system), a newer login from the same machine (superseded), a password reset (password_reset), the account’s deletion (account_deletion_attempt), a compromise response (compromise_response), or losing access to the team (membership_removed, member_left, role_changed). api_token.revoked says why the same way: a team owner or admin revoking it (revoked_by_admin), a rotation replacing it (rotated), the token revoking itself at varsafe logout (signed_out), a newer login from the same machine (superseded), a password reset, the account’s deletion, a compromise response, or its creator losing access to the team. api_token.rotated names who made the token it replaced (previousCreatedBy): the new token is the rotating admin’s. A compromise response that removes an account’s password records it as auth.credential_revoked, with the case and the operator who ran it, in the team it was run for; an account without a password records nothing.
Connecting an AI agent (mcp.grant_created) is recorded in each team it was granted, under the app’s own name, with the projects and environments it may reach (* for all of them). An agent listing an environment’s secrets is mcp.secrets_listed, with how many names it saw, never a value. mcp.token_replay_detected records an AI agent’s refresh token or authorization code used a second time; the connection it belonged to was signed out.
Invitations and members
Accepting an invitation records the join (team.member_added) and the invite it came through (team.invite_accepted, naming who sent it and the role). A refused acceptance is recorded as team.invite_accept_refused with the reason (another account’s address, an ownership-granting invitation, or one already used, revoked or expired). While the invitation is still pending, the record goes to the inviting team, since only someone holding its link can cause one; afterwards, only the person who tried sees it.
An invitation is recorded as an invite (team.member_invited, with the role it offers and when it expires), and revoking it names the role it offered (team.invite_revoked). A member who joins says how (team.member_added, joinedBy): added by an admin (direct), through an invite (invite) or by single sign-on (sso). A member removed or leaving names the role they held (team.member_removed, team.member_left). A deactivation names when the member loses access (effectiveAt, the end of the grace period). Changing a member to the role they already have, reactivating an active member, and saving a project’s name as it was record nothing. Signing up records the whole workspace it creates, made by the new member: the team, its subscription, its project and the project’s three environments.
Reading and changing secrets
Reading every value of an environment is recorded by why it was read: shown to its reader (secret.revealed, the dashboard’s values or varsafe list --reveal), or exported (secret.exported, with the format its client says it wrote: compose, sealed for an encrypted file, docker, json or yaml; no format when an older client did not say). Both name the keys, never the values, and both count toward the bulk-export anomaly alert. A record names at most 1,000 keys in each of its lists; keysTruncated says a list was cut, and the counts stay exact. Comparing two environments is recorded once for each (secret.compared, naming the other): it reads every value but answers only whether they match, so it counts toward the bulk-read alert and is not counted as secrets accessed in compliance reports. alert.anomaly_detected records what varsafe’s detectors noticed in a team: one person or token reading or exporting secret values in bulk, or an API token used from two networks at once.
Deleting a secret others are composed from records every secret it changed: the deleted one says its disposition, each secret a cascade deleted is a secret.deleted naming it (cascadedFrom), and each secret a flatten rewrote is a secret.updated naming it (flattenedFrom).
Each change to a secret names the environment it changed (environmentId) and the write that made it (operationId); the secrets one save changed together share its batchId. Reading values by name says why they were read (secret.accessed, purpose): asked for by name (value: varsafe get, an agent’s get), every value to run a process with them (inject: varsafe run), or every value copied into a new environment (duplicate). Renaming a secret in the editor is one record (secret.renamed, from the old name to the new, and valueChanged when the edit gave it a new value too); a swap or a chain of renames is recorded as the new values it leaves under each name. A value varsafe generates for an agent says so (generated), and generating one for a key that has a value is that secret’s rotation (secret.rotated). A write varsafe had to finish after a restart keeps the person who asked for it, says the system completed it, and names the request it came from (requestedFrom: its channel, address and user agent); as with any address, only the team’s owners and admins and the person who made the write see it. Every write keeps the ten newest versions of a secret; the nightly sweep’s decision to destroy old versions beyond those is secret.versions_pruned, by the system, naming the environment, the revisions and how many it keeps. It is recorded before they are destroyed, since that cannot be undone: versions whose record cannot be written are left in place for the next sweep, and a revision a sweep fails to destroy is named again, by the same number, by the next one.
Encryption keys
A key generated for an environment names the environment and why it was made (reason: asked for by hand, requested, or made so an export could be sealed, sealed_export); a rotation names the environment too. environment.key_accessed and environment.key_access_denied appear only on records from before private keys stopped being handed out.
Single sign-on
A single sign-on is the SSO provider’s team’s once the identity provider has vouched for it: sso.assertion_refused when the address it vouched for is outside the team’s verified domain, naming that domain but never the address, and sso.login_refused with the reason any other refusal gave. Removing single sign-on is recorded once, even when two removals race.
Proving control of a single sign-on domain is on record step by step: each challenge issued (sso.domain_challenge_issued, saying when it replaced one still pending, whose record then stops verifying), the verification (sso.domain_verified), and every attempt that failed (sso.domain_verification_failed, with the reason: no challenge outstanding, no matching record, a challenge that expired during the lookup, or a domain already verified by another team). Every daily re-check that finds no record is sso.domain_reverification_missed, with the consecutive misses and the count at which verification is revoked, whether or not revocation is enforced.
Billing
Billing changes reach varsafe from Stripe, so they are recorded by the system. A subscription.canceled says why (reason): the customer asked (user_requested), a newer subscription replaced it (replaced), Stripe’s deletion webhook said so (webhook), or varsafe found it ended when it asked Stripe itself, in the nightly reconciliation or when settling a replacement whose checkout never finished (reconciliation). A cancellation scheduled for the end of the period, typically from the billing portal, is subscription.cancellation_scheduled, with the date the subscription ends (endsAt), recorded again if that date moves; undoing it before then is subscription.cancellation_withdrawn. Opening the billing portal is billing.portal_opened, naming who opened it: the portal is where the plan is canceled or changed, and that record is the only one of the person behind such a change. Every Stripe subscription varsafe cancels for a team is a subscription.canceled with reason replaced, naming it (stripeSubscriptionId): the subscription a newer checkout replaced, and an older checkout completed after the team’s current one, whose subscription is canceled so it does not bill alongside. The team keeps its plan in both cases. Starting a checkout is billing.checkout_started, naming who started it, the plan and the seats it bills; it is recorded once Stripe has opened the session. A payment Stripe could not collect is billing.payment_failed, naming the invoice (invoiceId), the amount due in the currency’s minor unit (amountDue, currency), the attempt (attemptCount) and when Stripe will try again (nextAttemptAt, empty when it will not); it is recorded whether or not the team has a billing email, and notificationQueued says whether an email to that address was queued. A customer deleted in Stripe is billing.customer_unlinked, naming it (stripeCustomerId); the team’s next checkout creates a new one. A billing email change is filed under the team; saving the address already on file changes nothing and records nothing. A change to the number of seats Stripe bills is subscription.seats_changed, with what it billed before (previousQuantity) and now (quantity), recorded from Stripe’s own report of the change, once, whatever made it: a member joining or leaving, the hourly seat check correcting a count that drifted, or someone at Stripe.
Deleting and duplicating
Deleting a team is recorded as team.deleted with everything it took with it (cascade): how many projects, environments, secrets, members and pending invitations, how many API tokens, AI agent connections and CLI logins still worked, and its single sign-on providers, alert rules and alert destinations.
Deleting a project is recorded when it is asked for (project.deletion_requested, with the address and client that asked) and when it is done (project.deleted, once its environments are gone). Each environment it destroys on the way is an environment.deleted with reason: project_deleted, under the person who asked. Duplicating an environment is recorded as environment.duplicated, filed under the copy and naming the environment copied (sourceEnvironment), the secrets copied (keys) and both environments’ protection: protectionDropped is true when a protected environment’s values were copied into an unprotected one, out of its protection’s reach. Each copied secret is also a secret.created in the copy. Every environment.deleted names what went with it: how many secrets (secretCount) and their key names (keys, so a search for one finds it), and every encryption key the environment held (keyIds), after which no file sealed to one of them opens.
What a change records
A change records what it changed: each field under changes, with its value before (from) and after (to), such as a team’s previous name, the plan a cancellation took the team off, or an alert rule’s filters. Turning an environment’s protection on or off is its own action, environment.protection_changed, and names the environment. An alert destination’s configuration holds its webhook address or routing key, so a new one is recorded only as replaced. Configuring single sign-on records the identity provider’s issuer, endpoints and client ID, never the client secret, the certificates or a password written into an address. Saving something without changing it records nothing.
Refused attempts
The access_denied, open_denied and tool_refused actions record a signed-in caller’s refused attempt — a value read or write, a team administration step, a key-management step, an encrypted .env open, an AI-agent tool call, or a credential used on a route its kind cannot use (cli.access_denied for a CLI login, api_token.access_denied for an API token on a route that needs a signed-in person) — with the reason it was refused, even when the caller was only told the resource was not found. One is filed in a team’s log only when the caller demonstrably belongs to it (their membership, or their token’s own team); otherwise only the caller sees it. Each caller’s refusals are recorded five a minute at most; the fifth says so, and the rest are counted.
Retention
| Plan | Visible in the audit log |
|---|---|
| Developer | 7 days |
| Team | 90 days |
Events older than 90 days are deleted for every team, and each cleanup is itself recorded as audit.log_cleaned. Export regularly if you need a longer archive.
Every event reaches the log through a queue that keeps a copy of it, as published: 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.
Exporting the audit log
Requires: owner or admin, on the Team plan.
- Open Audit Log and click Export
- Pick the team (if you belong to several), a period (the last 7, 30 or 90 days, or a custom range) and a format
- Click Download
The file is built on the server from every event of the period, not from the page on screen. CSV has one row per event: event ID, timestamp, action, actor, target type, target name and IP address. A cell a spreadsheet would run as a formula (one starting with =, +, - or @) is prefixed with an apostrophe, so it opens as text. The SOC 2, GDPR and HIPAA formats are JSON reports, described under Compliance Exports. Every export is itself recorded as audit.export_generated. Reading a team’s log is recorded as audit.viewed, once per session and team each day (UTC), whichever page, filter or route it was read by.
Other members are told that only owners and admins export; on the Developer plan, Export offers the upgrade instead.
Events worth watching
varsafe does not send alerts yet. Review the audit log, or feed exports to your SIEM, and look for:
| Event | Why |
|---|---|
auth.revoked_credential_used |
A revoked session, login, token or AI agent connection is still in use somewhere: for an agent, the record names the connection that is no longer active (mcp_grant): revoked, expired, or its member removed or deactivated |
secret.access_denied, team.access_denied, mcp.tool_refused |
Someone is trying what they may not, or a credential is being probed |
cli.access_denied, api_token.access_denied |
A CLI login or token used outside what its client ever does: an outdated client, or someone using it by hand |
secret.accessed, secret.exported or environment.sealed_values_opened in production |
Confirm the reader should have it |
team.member_role_changed to admin, team.member_invited as admin |
Privilege change |
environment.protection_changed to unprotected |
Developers can change that environment again: confirm it was meant |
auth.2fa_disabled, auth.passkey_only_disabled |
Account protection lowered |
auth.two_factor_secret_viewed, auth.backup_codes_regenerated |
An account’s second factor was read or replaced: confirm its owner did it |
auth.provider_connected |
A new way into an account: confirm its owner connected that provider |
alert.anomaly_detected |
A person or token read or exported secrets in bulk, or a token was used from two networks at once |
mcp.token_replay_detected |
An agent’s token was used twice, usually because it was copied: review that connection |
sso.assertion_refused, sso.login_refused |
Sign-ins through your identity provider are refused: an address outside your domain, or a misconfiguration |
auth.second_factor_failed, auth.two_factor_locked |
Someone is guessing an account’s second factor |
auth.login_otp_failed |
Someone with the account’s password is signing in from a new device and guessing the code sent to confirm it: if it was not its owner, change the password |
auth.reauthentication_failed |
Someone holding a signed-in session is guessing its password |
auth.login_refused with method passkey |
A registered passkey’s answer was refused, which is what a copied key looks like: ask its owner, and remove that passkey if they did not try |
| An IP address you do not recognise | Access from outside your usual locations |
A refused sign-in to a member’s account is on their Security activity and, for your team’s owners and admins, on your team’s log (auth.login_refused). Attempts on addresses that have no account, and passkey sign-ins with a passkey no account has registered, stay in varsafe’s operators’ log; if you suspect a wider guessing campaign, contact support.
Compliance exports
Exports of a team’s audit trail for a period, in formats aimed at common reviews. They support your compliance program; they are not certifications. Owners and admins on the Team plan download them from Audit Log → Export (Exporting the Audit Log), or with POST /audit/export. Both are built on the server from every event of the period.
Format (type) |
Contains |
|---|---|
CSV (csv) |
The events themselves, one row each, with the IP address |
SOC 2 (soc2) |
A JSON report: a summary, every event, and the member access matrix (member, role, joined) |
GDPR (gdpr) |
A JSON report: a summary and every event, without IP addresses |
HIPAA (hipaa) |
A JSON report: a summary and every event, each with its IP address |
A report’s summary counts exactly the events it lists, even when events are recorded while it is built. Its secretsAccessed counts every event that handed someone secret values: reads (secret.accessed), exports from the CLI (secret.exported) or by an AI agent (mcp.secrets_injected), and opened sealed files (environment.sealed_values_opened). Its secretsRotated counts every change of a secret’s value: rotations (secret.rotated) and new values set (secret.updated).
The API takes the type, the teamId, and the period as from and to instants: it covers events from from up to, not including, to. Write each instant with your UTC offset (2026-09-01T00:00:00+02:00; Z means UTC). The period can cover at most 366 days, counted in the calendar the instants are written in, so a year of local midnights is accepted even when it spans a clock change. A CSV export answers with the file (text/csv), the others with JSON. It needs an owner or admin dashboard session (it does not accept an API token): another member gets 403, a team on the Developer plan 402, and neither is recorded. An export holds at most 50,000 events, so split a busy team’s year into smaller ranges. Each export is recorded as audit.export_generated.
Access review. On the Team plan, the Access Review button on the Audit Log page produces a JSON report of every member, their role, when they joined, which of development, staging and production their role reaches, and the team’s projects. It is recorded as audit.access_review_generated.
Access reviews
Quarterly access review
- Generate the access review (Team plan) from the Audit Log page, or open each team’s member list on the Teams page
- Check for:
- people who have left (remove them)
- roles wider than the work needs (roles)
- deactivated members who should be removed instead
- Act:
- Remove people who left. This revokes their CLI logins, agent connections and the API tokens they created, in one step
- Rotate first any pipeline token a leaver created, so the pipeline keeps working on a token you own
- Downgrade excessive roles (a role change revokes the same credentials, and they sign in again)
- Record that the review was done
Reviewing API tokens
- Open Access → API tokens
- For each token, ask whether it is still needed, and check its scope, access level, expiry, and when and from where it was last used
- Revoke what is unused
Incident response
Compromised secret
- Rotate at the source, then store the new value: dashboard Rotate, or
varsafe setfrom a script - Restart what uses it
- Review the audit log: who read it, when, from which IP?
- Assess impact: which systems used it, and was it used by someone else?
- Write it up: timeline, actions, follow-ups
Compromised account
- Reset the password. A reset signs the account out everywhere and revokes its CLI logins, agent connections’ refresh tokens and API tokens
- Enable two-factor, or add passkeys
- Review the audit log: what did the account read or export?
- Rotate everything the account could read
- Tell your team, and record it for compliance
Compromised API token
- Revoke it: Access → API tokens → Revoke, or have the token revoke itself with
DELETE /me/token - Create a replacement, scoped at least as tightly
- Update the CI/CD variables and scripts that used it
- Review the audit log for what the token read, and rotate those secrets
Troubleshooting
Errors and their fixes (sign-in, permissions, context, rate limits) are in the troubleshooting guide, with the exact message you would see.
One case specific to this page: an operation you believe your role allows is refused. Check, in order, that your membership is active, that your role allows it (roles reference), and whether the environment is protected: developers can only read a protected environment, and viewers cannot open it at all.