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
- Invite:
POST /emergency-access/invitecreates a record withstatus = Invitedand stores thewait_time_days. The server emails the grantee a JWT invitation token. - Accept:
POST /emergency-access/<id>/acceptvalidates the JWT and callsEmergencyAccess::accept_invite(), transitioning toAcceptedand linking the grantee’s UUID. - Confirm:
POST /emergency-access/<id>/confirmstores thekey_encrypted(the grantor’s vault key encrypted for the grantee) and setsstatus = 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>/initiatesetsstatus = RecoveryInitiatedand records the current timestamp inrecovery_initiated_atandlast_notification_at. - Approve:
POST /emergency-access/<id>/approveallows the grantor to manually bypass the wait time, immediately settingstatus = RecoveryApproved. - Reject:
POST /emergency-access/<id>/rejectresets the status back toConfirmed, 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 - 1days after initiation. - Subsequent reminders: Sent every 24 hours thereafter until the request is approved or rejected.
- Tracking: The
last_notification_atfield 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 (
Invited→Accepted→Confirmed→RecoveryInitiated→RecoveryApproved) to control trust relationships. - Timeout configuration is controlled by the
emergency_request_timeout_schedulecron job (default hourly at minute 7), which auto-approves requests afterwait_time_dayshave 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_allowedtofalseto disable the feature entirely. - File locations: State logic resides in
src/db/models/emergency_access.rs, API endpoints insrc/api/core/emergency_access.rs, and job scheduling insrc/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →