diff --git a/migrations/mysql/2023-01-06-151600_add_reset_password_support/down.sql b/migrations/mysql/2023-01-06-151600_add_reset_password_support/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/migrations/mysql/2023-01-06-151600_add_reset_password_support/up.sql b/migrations/mysql/2023-01-06-151600_add_reset_password_support/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..326b310692f9cd6bd1cef7dc1fff2b1d7c12986f
--- /dev/null
+++ b/migrations/mysql/2023-01-06-151600_add_reset_password_support/up.sql
@@ -0,0 +1,2 @@
+ALTER TABLE users_organizations
+ADD COLUMN reset_password_key TEXT;
diff --git a/migrations/postgresql/2023-01-06-151600_add_reset_password_support/down.sql b/migrations/postgresql/2023-01-06-151600_add_reset_password_support/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/migrations/postgresql/2023-01-06-151600_add_reset_password_support/up.sql b/migrations/postgresql/2023-01-06-151600_add_reset_password_support/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..326b310692f9cd6bd1cef7dc1fff2b1d7c12986f
--- /dev/null
+++ b/migrations/postgresql/2023-01-06-151600_add_reset_password_support/up.sql
@@ -0,0 +1,2 @@
+ALTER TABLE users_organizations
+ADD COLUMN reset_password_key TEXT;
diff --git a/migrations/sqlite/2023-01-06-151600_add_reset_password_support/down.sql b/migrations/sqlite/2023-01-06-151600_add_reset_password_support/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/migrations/sqlite/2023-01-06-151600_add_reset_password_support/up.sql b/migrations/sqlite/2023-01-06-151600_add_reset_password_support/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..326b310692f9cd6bd1cef7dc1fff2b1d7c12986f
--- /dev/null
+++ b/migrations/sqlite/2023-01-06-151600_add_reset_password_support/up.sql
@@ -0,0 +1,2 @@
+ALTER TABLE users_organizations
+ADD COLUMN reset_password_key TEXT;
diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs
index c0af8f6e276ba44b5ab1870da6a67a179bb083bd..9250f92913692776e283f5960407453873f3c8b2 100644
--- a/src/api/core/organizations.rs
+++ b/src/api/core/organizations.rs
@@ -62,6 +62,7 @@ pub fn routes() -> Vec<Route> {
         get_plans_tax_rates,
         import,
         post_org_keys,
+        get_organization_keys,
         bulk_public_keys,
         deactivate_organization_user,
         bulk_deactivate_organization_user,
@@ -86,6 +87,9 @@ pub fn routes() -> Vec<Route> {
         put_user_groups,
         delete_group_user,
         post_delete_group_user,
+        put_reset_password_enrollment,
+        get_reset_password_details,
+        put_reset_password,
         get_org_export
     ]
 }
@@ -882,6 +886,7 @@ async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, co
 #[allow(non_snake_case)]
 struct AcceptData {
     Token: String,
+    ResetPasswordKey: Option<String>,
 }
 
 #[post("/organizations/<org_id>/users/<_org_user_id>/accept", data = "<data>")]
@@ -909,6 +914,11 @@ async fn accept_invite(
                     err!("User already accepted the invitation")
                 }
 
+                let master_password_required = OrgPolicy::org_is_reset_password_auto_enroll(org, &mut conn).await;
+                if data.ResetPasswordKey.is_none() && master_password_required {
+                    err!("Reset password key is required, but not provided.");
+                }
+
                 // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_user_org_type
                 // It returns different error messages per function.
                 if user_org.atype < UserOrgType::Admin {
@@ -924,6 +934,11 @@ async fn accept_invite(
                 }
 
                 user_org.status = UserOrgStatus::Accepted as i32;
+
+                if master_password_required {
+                    user_org.reset_password_key = data.ResetPasswordKey;
+                }
+
                 user_org.save(&mut conn).await?;
             }
         }
@@ -2460,6 +2475,204 @@ async fn delete_group_user(
     GroupUser::delete_by_group_id_and_user_id(&group_id, &org_user_id, &mut conn).await
 }
 
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+struct OrganizationUserResetPasswordEnrollmentRequest {
+    ResetPasswordKey: Option<String>,
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+struct OrganizationUserResetPasswordRequest {
+    NewMasterPasswordHash: String,
+    Key: String,
+}
+
+#[get("/organizations/<org_id>/keys")]
+async fn get_organization_keys(org_id: String, mut conn: DbConn) -> JsonResult {
+    let org = match Organization::find_by_uuid(&org_id, &mut conn).await {
+        Some(organization) => organization,
+        None => err!("Organization not found"),
+    };
+
+    Ok(Json(json!({
+        "Object": "organizationKeys",
+        "PublicKey": org.public_key,
+        "PrivateKey": org.private_key,
+    })))
+}
+
+#[put("/organizations/<org_id>/users/<org_user_id>/reset-password", data = "<data>")]
+async fn put_reset_password(
+    org_id: String,
+    org_user_id: String,
+    headers: AdminHeaders,
+    data: JsonUpcase<OrganizationUserResetPasswordRequest>,
+    mut conn: DbConn,
+    ip: ClientIp,
+    nt: Notify<'_>,
+) -> EmptyResult {
+    let org = match Organization::find_by_uuid(&org_id, &mut conn).await {
+        Some(org) => org,
+        None => err!("Required organization not found"),
+    };
+
+    let org_user = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org.uuid, &mut conn).await {
+        Some(user) => user,
+        None => err!("User to reset isn't member of required organization"),
+    };
+
+    let mut user = match User::find_by_uuid(&org_user.user_uuid, &mut conn).await {
+        Some(user) => user,
+        None => err!("User not found"),
+    };
+
+    check_reset_password_applicable_and_permissions(&org_id, &org_user_id, &headers, &mut conn).await?;
+
+    if org_user.reset_password_key.is_none() {
+        err!("Password reset not or not correctly enrolled");
+    }
+    if org_user.status != (UserOrgStatus::Confirmed as i32) {
+        err!("Organization user must be confirmed for password reset functionality");
+    }
+
+    // Sending email before resetting password to ensure working email configuration and the resulting
+    // user notification. Also this might add some protection against security flaws and misuse
+    if let Err(e) = mail::send_admin_reset_password(&user.email, &user.name, &org.name).await {
+        error!("Error sending user reset password email: {:#?}", e);
+    }
+
+    let reset_request = data.into_inner().data;
+
+    user.set_password(reset_request.NewMasterPasswordHash.as_str(), Some(reset_request.Key), true, None);
+    user.save(&mut conn).await?;
+
+    nt.send_logout(&user, None).await;
+
+    log_event(
+        EventType::OrganizationUserAdminResetPassword as i32,
+        &org_user_id,
+        org.uuid.clone(),
+        headers.user.uuid.clone(),
+        headers.device.atype,
+        &ip.ip,
+        &mut conn,
+    )
+    .await;
+
+    Ok(())
+}
+
+#[get("/organizations/<org_id>/users/<org_user_id>/reset-password-details")]
+async fn get_reset_password_details(
+    org_id: String,
+    org_user_id: String,
+    headers: AdminHeaders,
+    mut conn: DbConn,
+) -> JsonResult {
+    let org = match Organization::find_by_uuid(&org_id, &mut conn).await {
+        Some(org) => org,
+        None => err!("Required organization not found"),
+    };
+
+    let org_user = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &mut conn).await {
+        Some(user) => user,
+        None => err!("User to reset isn't member of required organization"),
+    };
+
+    let user = match User::find_by_uuid(&org_user.user_uuid, &mut conn).await {
+        Some(user) => user,
+        None => err!("User not found"),
+    };
+
+    check_reset_password_applicable_and_permissions(&org_id, &org_user_id, &headers, &mut conn).await?;
+
+    Ok(Json(json!({
+        "Object": "organizationUserResetPasswordDetails",
+        "Kdf":user.client_kdf_type,
+        "KdfIterations":user.client_kdf_iter,
+        "ResetPasswordKey":org_user.reset_password_key,
+        "EncryptedPrivateKey":org.private_key ,
+
+    })))
+}
+
+async fn check_reset_password_applicable_and_permissions(
+    org_id: &str,
+    org_user_id: &str,
+    headers: &AdminHeaders,
+    conn: &mut DbConn,
+) -> EmptyResult {
+    check_reset_password_applicable(org_id, conn).await?;
+
+    let target_user = match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await {
+        Some(user) => user,
+        None => err!("Reset target user not found"),
+    };
+
+    // Resetting user must be higher/equal to user to reset
+    match headers.org_user_type {
+        UserOrgType::Owner => Ok(()),
+        UserOrgType::Admin if target_user.atype <= UserOrgType::Admin => Ok(()),
+        _ => err!("No permission to reset this user's password"),
+    }
+}
+
+async fn check_reset_password_applicable(org_id: &str, conn: &mut DbConn) -> EmptyResult {
+    if !CONFIG.mail_enabled() {
+        err!("Password reset is not supported on an email-disabled instance.");
+    }
+
+    let policy = match OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::ResetPassword, conn).await {
+        Some(p) => p,
+        None => err!("Policy not found"),
+    };
+
+    if !policy.enabled {
+        err!("Reset password policy not enabled");
+    }
+
+    Ok(())
+}
+
+#[put("/organizations/<org_id>/users/<org_user_id>/reset-password-enrollment", data = "<data>")]
+async fn put_reset_password_enrollment(
+    org_id: String,
+    org_user_id: String,
+    headers: Headers,
+    data: JsonUpcase<OrganizationUserResetPasswordEnrollmentRequest>,
+    mut conn: DbConn,
+    ip: ClientIp,
+) -> EmptyResult {
+    let mut org_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await {
+        Some(u) => u,
+        None => err!("User to enroll isn't member of required organization"),
+    };
+
+    check_reset_password_applicable(&org_id, &mut conn).await?;
+
+    let reset_request = data.into_inner().data;
+
+    if reset_request.ResetPasswordKey.is_none()
+        && OrgPolicy::org_is_reset_password_auto_enroll(&org_id, &mut conn).await
+    {
+        err!("Reset password can't be withdrawed due to an enterprise policy");
+    }
+
+    org_user.reset_password_key = reset_request.ResetPasswordKey;
+    org_user.save(&mut conn).await?;
+
+    let log_id = if org_user.reset_password_key.is_some() {
+        EventType::OrganizationUserResetPasswordEnroll as i32
+    } else {
+        EventType::OrganizationUserResetPasswordWithdraw as i32
+    };
+
+    log_event(log_id, &org_user_id, org_id, headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn).await;
+
+    Ok(())
+}
+
 // This is a new function active since the v2022.9.x clients.
 // It combines the previous two calls done before.
 // We call those two functions here and combine them our selfs.
diff --git a/src/config.rs b/src/config.rs
index 46deed546f7b8bb6fe1e5a86cdabf81d808c456f..68a6811c0f3c85b5ddf4ca1586445300bfd5c5d0 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1136,6 +1136,7 @@ where
     reg!("email/email_footer");
     reg!("email/email_footer_text");
 
+    reg!("email/admin_reset_password", ".html");
     reg!("email/change_email", ".html");
     reg!("email/delete_account", ".html");
     reg!("email/emergency_access_invite_accepted", ".html");
diff --git a/src/db/models/event.rs b/src/db/models/event.rs
index 9196b8a81aa0409c7bd9283135cbc204ac331bd4..6431227347c9fa4091ed709f7344ca23da781f3a 100644
--- a/src/db/models/event.rs
+++ b/src/db/models/event.rs
@@ -87,9 +87,9 @@ pub enum EventType {
     OrganizationUserRemoved = 1503,
     OrganizationUserUpdatedGroups = 1504,
     // OrganizationUserUnlinkedSso = 1505, // Not supported
-    // OrganizationUserResetPasswordEnroll = 1506, // Not supported
-    // OrganizationUserResetPasswordWithdraw = 1507, // Not supported
-    // OrganizationUserAdminResetPassword = 1508, // Not supported
+    OrganizationUserResetPasswordEnroll = 1506,
+    OrganizationUserResetPasswordWithdraw = 1507,
+    OrganizationUserAdminResetPassword = 1508,
     // OrganizationUserResetSsoLink = 1509, // Not supported
     // OrganizationUserFirstSsoLogin = 1510, // Not supported
     OrganizationUserRevoked = 1511,
diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs
index caa3335f5c001acc5892825d753b5ceb8357f838..c9fd5c348516132aad364af4c418843563a56157 100644
--- a/src/db/models/org_policy.rs
+++ b/src/db/models/org_policy.rs
@@ -32,7 +32,7 @@ pub enum OrgPolicyType {
     PersonalOwnership = 5,
     DisableSend = 6,
     SendOptions = 7,
-    // ResetPassword = 8, // Not supported
+    ResetPassword = 8,
     // MaximumVaultTimeout = 9, // Not supported (Not AGPLv3 Licensed)
     // DisablePersonalVaultExport = 10, // Not supported (Not AGPLv3 Licensed)
 }
@@ -44,6 +44,13 @@ pub struct SendOptionsPolicyData {
     pub DisableHideEmail: bool,
 }
 
+// https://github.com/bitwarden/server/blob/5cbdee137921a19b1f722920f0fa3cd45af2ef0f/src/Core/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+pub struct ResetPasswordDataModel {
+    pub AutoEnrollEnabled: bool,
+}
+
 pub type OrgPolicyResult = Result<(), OrgPolicyErr>;
 
 #[derive(Debug)]
@@ -298,6 +305,20 @@ impl OrgPolicy {
         Ok(())
     }
 
+    pub async fn org_is_reset_password_auto_enroll(org_uuid: &str, conn: &mut DbConn) -> bool {
+        match OrgPolicy::find_by_org_and_type(org_uuid, OrgPolicyType::ResetPassword, conn).await {
+            Some(policy) => match serde_json::from_str::<UpCase<ResetPasswordDataModel>>(&policy.data) {
+                Ok(opts) => {
+                    return opts.data.AutoEnrollEnabled;
+                }
+                _ => error!("Failed to deserialize ResetPasswordDataModel: {}", policy.data),
+            },
+            None => return false,
+        }
+
+        false
+    }
+
     /// Returns true if the user belongs to an org that has enabled the `DisableHideEmail`
     /// option of the `Send Options` policy, and the user is not an owner or admin of that org.
     pub async fn is_hide_email_disabled(user_uuid: &str, conn: &mut DbConn) -> bool {
diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs
index 331e10070f38710903fe3bdcd125e7f97d6183d1..a6e4be2124dc7f585ee31eab45f326fd9d8e9c1c 100644
--- a/src/db/models/organization.rs
+++ b/src/db/models/organization.rs
@@ -29,6 +29,7 @@ db_object! {
         pub akey: String,
         pub status: i32,
         pub atype: i32,
+        pub reset_password_key: Option<String>,
     }
 }
 
@@ -158,7 +159,7 @@ impl Organization {
             "SelfHost": true,
             "UseApi": false, // Not supported
             "HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(),
-            "UseResetPassword": false, // Not supported
+            "UseResetPassword": CONFIG.mail_enabled(),
 
             "BusinessName": null,
             "BusinessAddress1": null,
@@ -194,6 +195,7 @@ impl UserOrganization {
             akey: String::new(),
             status: UserOrgStatus::Accepted as i32,
             atype: UserOrgType::User as i32,
+            reset_password_key: None,
         }
     }
 
@@ -311,7 +313,8 @@ impl UserOrganization {
             "UseApi": false, // Not supported
             "SelfHost": true,
             "HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(),
-            "ResetPasswordEnrolled": false, // Not supported
+            "ResetPasswordEnrolled": self.reset_password_key.is_some(),
+            "UseResetPassword": CONFIG.mail_enabled(),
             "SsoBound": false, // Not supported
             "UseSso": false, // Not supported
             "ProviderId": null,
@@ -377,6 +380,7 @@ impl UserOrganization {
             "Type": self.atype,
             "AccessAll": self.access_all,
             "TwoFactorEnabled": twofactor_enabled,
+            "ResetPasswordEnrolled":self.reset_password_key.is_some(),
 
             "Object": "organizationUserUserDetails",
         })
diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs
index 27cd24c35f4dfa2f2d197f5929a330218db039d4..cdb3e059dbf8fc500117b70260896536befe290e 100644
--- a/src/db/schemas/mysql/schema.rs
+++ b/src/db/schemas/mysql/schema.rs
@@ -222,6 +222,7 @@ table! {
         akey -> Text,
         status -> Integer,
         atype -> Integer,
+        reset_password_key -> Nullable<Text>,
     }
 }
 
diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs
index 0233e0c9752c75c18b968fccb021b3df45c97f18..6ec8a979de3d5e6bc53b2c2a8b9e2a5fe18cdbe7 100644
--- a/src/db/schemas/postgresql/schema.rs
+++ b/src/db/schemas/postgresql/schema.rs
@@ -222,6 +222,7 @@ table! {
         akey -> Text,
         status -> Integer,
         atype -> Integer,
+        reset_password_key -> Nullable<Text>,
     }
 }
 
diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs
index 391e67001898b8483facd81cf6d741bd82e50715..faaf6fae9ea324303234775f49e9c21379ba7a81 100644
--- a/src/db/schemas/sqlite/schema.rs
+++ b/src/db/schemas/sqlite/schema.rs
@@ -222,6 +222,7 @@ table! {
         akey -> Text,
         status -> Integer,
         atype -> Integer,
+        reset_password_key -> Nullable<Text>,
     }
 }
 
diff --git a/src/mail.rs b/src/mail.rs
index 8ecb11c61fc3b1fdb67e17538d8d4677a35127b1..cffa65fbcb6fb637631b69593f090cfb298dc6e9 100644
--- a/src/mail.rs
+++ b/src/mail.rs
@@ -496,6 +496,19 @@ pub async fn send_test(address: &str) -> EmptyResult {
     send_email(address, &subject, body_html, body_text).await
 }
 
+pub async fn send_admin_reset_password(address: &str, user_name: &str, org_name: &str) -> EmptyResult {
+    let (subject, body_html, body_text) = get_text(
+        "email/admin_reset_password",
+        json!({
+            "url": CONFIG.domain(),
+            "img_src": CONFIG._smtp_img_src(),
+            "user_name": user_name,
+            "org_name": org_name,
+        }),
+    )?;
+    send_email(address, &subject, body_html, body_text).await
+}
+
 async fn send_email(address: &str, subject: &str, body_html: String, body_text: String) -> EmptyResult {
     let smtp_from = &CONFIG.smtp_from();
 
diff --git a/src/static/templates/email/admin_reset_password.hbs b/src/static/templates/email/admin_reset_password.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..8d38177254b3638df8a33bfccc51214170517231
--- /dev/null
+++ b/src/static/templates/email/admin_reset_password.hbs
@@ -0,0 +1,6 @@
+Master Password Has Been Changed
+<!---------------->
+The master password for {{user_name}} has been changed by an administrator in your {{org_name}} organization. If you did not initiate this request, please reach out to your administrator immediately.
+
+===
+Github: https://github.com/dani-garcia/vaultwarden
diff --git a/src/static/templates/email/admin_reset_password.html.hbs b/src/static/templates/email/admin_reset_password.html.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..d9749d22a9d68f08047b0ff7840cd0d410643c56
--- /dev/null
+++ b/src/static/templates/email/admin_reset_password.html.hbs
@@ -0,0 +1,11 @@
+Master Password Has Been Changed
+<!---------------->
+{{> email/email_header }}
+<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+        <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
+            The master password for <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{user_name}}</b> has been changed by an administrator in your <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{org_name}}</b> organization. If you did not initiate this request, please reach out to your administrator immediately.
+        </td>
+    </tr>
+</table>
+{{> email/email_footer }}