Vaultwarden Emergency Access: How Timeout and Reminder Configurations Work

Vaultwarden emergency access allows designated contacts to request vault access after a configurable waiting period, enforced by background jobs that send reminders and auto-approve requests based on cron schedules defined in src/config.rs.

Vaultwarden, the open-source Bitwarden-compatible password manager, implements an emergency access system that lets users designate trusted contacts to recover or view their vault after a safety waiting period. Understanding how the emergency access timeout and reminder configurations work requires examining the state machine in src/db/models/emergency_access.rs, the API logic in src/api/core/emergency_access.rs, and the background job schedules in src/config.rs. This guide explains the complete lifecycle from invitation to automatic approval.

How Emergency Access Works

Vaultwarden emergency access establishes a trust relationship between a grantor (the vault owner) and a grantee (the trusted contact). The system supports two distinct access modes that determine what the grantee can do once approved.

Access Modes: View vs. Takeover

  • View: The grantee can read the grantor’s vault items but cannot modify the account or master password. This is suitable for read-only recovery scenarios.
  • Takeover: The grantee gains full control of the grantor’s account, including the ability to reset the master password and delete organizations. This provides complete account recovery.

Both modes enforce a mandatory wait time (specified in days) between when the grantee initiates the request and when access is granted. This waiting period is configurable per invitation via the wait_time_days field.

Feature Toggle

Emergency access is globally gated by the configuration flag emergency_access_allowed. Every endpoint in src/api/core/emergency_access.rs starts with the helper check_emergency_access_enabled(), which aborts with “Emergency access is not enabled.” if the feature is disabled.

fn check_emergency_access_enabled() -> EmptyResult {
    if !CONFIG.emergency_access_allowed() {
        err!("Emergency access is not enabled.")
    }
    Ok(())
}

Source: src/api/core/emergency_access.rs:711-715

The Emergency Access State Machine

The EmergencyAccess model tracks status through a strict enum-driven lifecycle. Each transition is handled by specific API endpoints that validate permissions and update the database record.

Status Definitions

Status Value Description
Invited 0 Initial state after the grantor sends an invitation.
Accepted 1 Grantee has accepted the invitation.
Confirmed 2 Grantor confirmed the invitation and supplied the encrypted key.
RecoveryInitiated 3 Grantee initiated recovery; the timeout clock starts.
RecoveryApproved 4 Request approved (manually or via timeout); access granted.

Invitation and Acceptance Workflow

  1. Invite: POST /emergency-access/invite creates a record with status = Invited and stores the wait_time_days. The server emails the grantee a JWT invitation token.
  2. Accept: POST /emergency-access/<id>/accept validates the JWT and calls EmergencyAccess::accept_invite(), transitioning to Accepted and linking the grantee’s UUID.
  3. Confirm: POST /emergency-access/<id>/confirm stores the key_encrypted (the grantor’s vault key encrypted for the grantee) and sets status = Confirmed.

Source: Invitation logic at src/api/core/emergency_access.rs:998-1014; acceptance at lines 332-371; confirmation at lines 388-426.

Initiating Recovery and Manual Approval

Once the relationship is Confirmed, the grantee can trigger the timeout period:

  • Initiate: POST /emergency-access/<id>/initiate sets status = RecoveryInitiated and records the current timestamp in recovery_initiated_at and last_notification_at.
  • Approve: POST /emergency-access/<id>/approve allows the grantor to manually bypass the wait time, immediately setting status = RecoveryApproved.
  • Reject: POST /emergency-access/<id>/reject resets the status back to Confirmed, canceling the request.

The helper is_valid_request enforces that subsequent view or takeover calls require status = RecoveryApproved and that the grantee UUID matches the stored grantee_uuid.

Source: Initiation at src/api/core/emergency_access.rs:441-467; validation at lines 700-709.

Timeout Configuration and Auto-Approval

The automatic approval mechanism relies on a background job that polls for expired wait times and transitions requests to RecoveryApproved without manual intervention.

The Timeout Job

The emergency_request_timeout_job runs on the schedule defined by emergency_request_timeout_schedule (default: "0 7 * * * *", meaning every hour at minute 7). It queries all records with status = RecoveryInitiated, calculates the expiration time by adding wait_time_days to recovery_initiated_at, and approves any expired requests.

pub async fn emergency_request_timeout_job(pool: DbPool) {
    for mut emer in emergency_access_list {
        let recovery_allowed_at = emer.recovery_initiated_at.unwrap()
            + TimeDelta::try_days(i64::from(emer.wait_time_days)).unwrap();
        if recovery_allowed_at.le(&now) {
            emer.update_access_status_and_save(
                EmergencyAccessStatus::RecoveryApproved as i32,
                &now,
                &conn,
            ).await?;
            // Sends "approved" and "timed out" emails
        }
    }
}

Source: Job implementation in src/api/core/emergency_access.rs; scheduled in src/main.rs:667.

Cron Schedule Configuration

You can customize the check frequency via the emergency_request_timeout_schedule configuration key. The default cron expression "0 7 * * * *" runs hourly at minute 7. To run every 6 hours, you would override this in your environment:

ROCKET_EMERGENCY_REQUEST_TIMEOUT_SCHEDULE="0 0 */6 * * *"

Source: Default values in src/config.rs:552-557.

Reminder Notification Configuration

To ensure grantors do not miss pending recovery requests, Vaultwarden runs a separate reminder job that sends email notifications starting one day before the timeout expires.

The Reminder Job

The emergency_notification_reminder_job executes on the emergency_notification_reminder_schedule (default: "0 3 * * * *", hourly at minute 3). It identifies requests where the current time has passed recovery_initiated_at + (wait_time_days - 1), then sends a reminder email if at least one day has elapsed since the last notification.

let final_recovery_reminder_at = emer.recovery_initiated_at.unwrap()
    + TimeDelta::try_days(i64::from(emer.wait_time_days - 1)).unwrap();

let next_recovery_reminder_at = if let Some(last) = emer.last_notification_at {
    last + TimeDelta::try_days(1).unwrap()
} else {
    now
};

if final_recovery_reminder_at.le(&now) && next_recovery_reminder_at.le(&now) {
    emer.update_last_notification_date_and_save(&now, &conn).await?;
    // send recovery reminder email
}

Source: src/api/core/emergency_access.rs (job definition) and src/mail.rs (email templates).

Notification Timing Logic

  • First reminder: Sent when the current time exceeds wait_time_days - 1 days after initiation.
  • Subsequent reminders: Sent every 24 hours thereafter until the request is approved or rejected.
  • Tracking: The last_notification_at field prevents spam by enforcing a minimum 24-hour gap between emails.

Configuration Options and Defaults

The behavior of both background jobs is controlled through the CONFIG struct, which maps to environment variables using the ROCKET_ prefix.

Config Key Default Description
emergency_access_allowed true Global toggle to enable or disable the entire feature.
emergency_request_timeout_schedule "0 7 * * * *" Cron for the auto-approval job (hourly at :07).
emergency_notification_reminder_schedule "0 3 * * * *" Cron for the reminder job (hourly at :03).

Source: src/config.rs:552-557.

Practical API Workflow Examples

Inviting a Grantee (Grantor Action)

POST /emergency-access/invite HTTP/1.1
Content-Type: application/json
Authorization: Bearer <grantor-jwt>

{
  "email": "[email protected]",
  "type": "Takeover",
  "waitTimeDays": 7
}

This creates the EmergencyAccess record with status = Invited and triggers mail::send_emergency_access_invite with a JWT link.

Initiating Recovery (Grantee Action)

POST /emergency-access/<uuid>/initiate HTTP/1.1
Authorization: Bearer <grantee-jwt>

This transitions the status to RecoveryInitiated, sets recovery_initiated_at = now(), and starts the timeout countdown.

Automatic Approval (System Action)

No manual API call is required. Once now >= recovery_initiated_at + wait_time_days, the emergency_request_timeout_job automatically updates the status to RecoveryApproved and emails both parties.

Accessing the Vault (Grantee Action)

After approval, the grantee can retrieve the encrypted vault key:

POST /emergency-access/<uuid>/takeover HTTP/1.1
Authorization: Bearer <grantee-jwt>

The server validates the request via is_valid_request, ensuring the status is RecoveryApproved and the type matches Takeover.

Summary

  • Vaultwarden emergency access uses a state machine (InvitedAcceptedConfirmedRecoveryInitiatedRecoveryApproved) to control trust relationships.
  • Timeout configuration is controlled by the emergency_request_timeout_schedule cron job (default hourly at minute 7), which auto-approves requests after wait_time_days have elapsed.
  • Reminder configuration uses emergency_notification_reminder_schedule (default hourly at minute 3) to send daily alerts starting one day before the timeout expires.
  • Global toggle: Set emergency_access_allowed to false to disable the feature entirely.
  • File locations: State logic resides in src/db/models/emergency_access.rs, API endpoints in src/api/core/emergency_access.rs, and job scheduling in src/main.rs.

Frequently Asked Questions

How do I change the default waiting period for emergency access requests?

The waiting period is set per invitation via the waitTimeDays field in the POST /emergency-access/invite request. It is not a global configuration; each grantor specifies how many days (minimum typically 1) they want the grantee to wait before access is granted. The background jobs then respect this value when calculating the approval time.

Can I disable the automatic reminder emails?

While there is no explicit boolean flag to disable reminders, you can effectively stop the reminder job by setting the emergency_notification_reminder_schedule to a cron expression that never runs (e.g., "0 0 31 2 * *" for February 31st). However, note that this also disables the safety notifications that alert grantors to pending takeovers.

What happens if the grantor rejects a recovery request?

If the grantor calls POST /emergency-access/<id>/reject, the system resets the EmergencyAccess record from RecoveryInitiated back to Confirmed (as implemented in src/api/core/emergency_access.rs:514-545). The grantee must initiate a new request if they wish to attempt access again, restarting the wait_time_days countdown from zero.

How does Vaultwarden ensure the grantee cannot access the vault before the timeout expires?

The is_valid_request helper function (lines 700-709 in src/api/core/emergency_access.rs) strictly checks that status == RecoveryApproved before allowing view or takeover endpoints to proceed. Until the emergency_request_timeout_job updates the status or the grantor manually approves, the grantee receives a validation error when attempting to access the vault.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →