* Include user email in successful login logs
* Modified disable account log to display Email instead of Display Name
---------
Co-authored-by: Alejandro Olmos <aolmos@trevenque.es>
* Update GHA and pre-commit
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update admin diagnostics
Added a check if the templates are overridden and return which specific folder, `admin`, `email` or `scss`.
This way we could more quickly point users to possible outdated templates which they are using.
Also updated the Support String to use some emojis so we should be able to quicker see if there is something wrong.
Just checking `true` or `false` could be difficult sometimes, and sometimes what we had as `false` wasn't bad either.
Also adjusted the eslint comments so it will work with the latest version of eslint.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Fix updating collections for a cipher
The newer clients expect a `cipherDetails` response on the `collections-admin` endpoints.
Without it, the client will cause an error and stops handling the update correctly.
This will fix this by returning the cipher json.
Fixes#7545Fixes#7546
Signed-off-by: BlackDex <black.dex@gmail.com>
* Cache CSS file in a different way
Currently we set a cache ttl of 24 hours, and users need to do a force refresh if there is anything changed to the CSS file.
In the past we have had several issue reported which were related to a still cached CSS file.
This commit will change the caching and also cache the generated CSS file in memory.
Instead of letting the browser cache it for 24 hours we generate an ETag, this is just a hash of the contents.
This ETag is returned by the browser during a request, and we can match this, and if so, just return a `304` `Not Modified`.
If the ETag is not known, we return the new content.
This should make simple refreshes by clients get updated settings or a new version of Vaultwarden which has other CSS entries get updated instantly.
If a user does a hard refresh, we will not receive the ETag and the content will be served.
The same goes if someone has the `reload_templates` feature enabled, since then we should not cache anyway.
If someone adjust settings via the `/admin` interface, the cache will be invalidated and a new CSS will be generated.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Fix showing events for a specific user
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update crates and adjust code.
- Updated opendal and adjusted code where needed.
- Updated yubico_ng and adjusted code where needed.
This version now supports using an own HttpClient and it pulls in no reqwest dependency anymore.
Now it will use our own client which uses custom hickory DNS and other features.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update web-vault to v2026.7.0
Signed-off-by: BlackDex <black.dex@gmail.com>
* Fix hadolint warnings
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
A request addressed to an administrator is answered through the organization
and stays open for a week, so counting it as the pending request of its device
hid the short lived one the user was actually being shown and made approving
from another device fail for as long as it was open. It is now excluded, along
with requests past their window, as upstream does.
The stand-in page in the admin panel asked an administrator of an
organization for their vault master password inside the panel of the server
operator, which are two different roles, and it carried its own partial
login: no Argon2, no two-step login, and no fingerprint of the asking device
to compare against.
The page belongs where that already exists. The web vault has the whole
login stack and, in its AGPL part, the same unwrap the approval needs, since
account recovery does it too. Upstream keeps only the page itself in the
licensed part; the navigation entry and the string for it are already in
every build.
So the endpoints stay and the page goes, to be added to the web vault build
instead. The notification mail points at the route it lives under there. A
member who loses every trusted device is not stranded meanwhile: account
recovery gets them back in under the same conditions, at the price of a new
master password.
Findings from a review of the earlier commits.
A key rotation dropped the device key pairs along with the wrapped user
keys, so the devices listed in `/devices/update-trust` could never be
trusted again and silently lost their trust on every rotation. Those key
pairs are wrapped with the device key, which a rotation does not touch, so
they now stay and the re-wrap works. The invalidation also moved ahead of
the new user key: failing after it had been written left devices handing
out a key that no longer opens the vault.
The three key blobs are now checked against the shape of an `EncString`
instead of only for emptiness, as upstream does, which also keeps
multi-megabyte values out of those columns.
`/auth-requests/admin-request` is rate limited and reuses the open request
of a device instead of adding one per attempt, which mailed every
administrator again each time. Only confirmed memberships are asked, and
answered: an invitation that was never accepted is not a membership yet, a
revoked one is not one anymore, and neither should learn the address, the
IP and the device of the asker. `UserDecryptionOptions` reports the same
condition, so no way out is announced that would be refused when taken.
The bulk endpoints skip what they cannot answer rather than failing the
whole call after having answered everything before it, matching upstream,
and are bounded. The purge does its work per type in the database instead
of reading the table and deleting row by row, with indexes to go with it.
Also: approving an auth request is limited to the newest one of a device,
`GET /auth-requests/<id>` refuses an expired request like the anonymous
lookup already did, the approval notification addresses the device that
asked rather than the administrator's, and accepting an invitation while
enrolling is tied to the trusted device flow it was meant for.
An account that has no keys yet and belongs to no organization cannot finish
the trusted device flow. The clients enroll into account recovery as the last
step of creating an account that way, unconditionally, and there is nothing to
enroll into: the enrollment starts by fetching the organization's public key,
which answers 401 without a membership. What the user sees is the screen for a
new account, a failure halfway through it, and on the next attempt a refusal
because the account keys are already written.
Not offering the options for that one combination sends the client to setting
a master password instead, which works and leaves the door open: the account
can trust a device on its very next login. Everything else is unchanged. An
account that already has keys goes through none of this, whether it belongs to
an organization or not, so the master password first route and an account that
already trusts a device are unaffected.
The real fix is to have members in an organization by the time they first sign
in, as upstream requires. This only makes the case where that did not happen
end somewhere other than a dead end.
Stand-in for the page of the same name in the admin console, which lives in
the part of bitwarden/clients that is not AGPL licensed and is therefore in
no web vault build. Without it the endpoints added earlier had no caller
outside of scripts.
The server cannot answer these requests by itself, and that is the point:
it keeps the organization's private key encrypted with the organization key,
and that one exists only as RSA envelopes addressed to each administrator.
So the page ships markup and the whole chain runs in the browser against the
regular API:
master password -> master key PBKDF2-SHA256
-> own user key AES from profile.key
-> own private key AES from profile.privateKey
-> organization key RSA from profile.organizations[].key
-> org private key AES from reset-password-details
-> member's user key RSA from reset-password-details
-> encryptedUserKey RSA for the public key of the request
The master password itself never leaves the page; what goes out is the same
login hash any sign-in sends. WebCrypto covers all of it except HKDF-Expand,
which is done by hand because WebCrypto always runs the extract step first
and Bitwarden uses the master key directly as the pseudorandom key.
The page asks for the account of an administrator of the organization, not
for the admin token, and says so: the two are not the same person on every
installation, and the admin panel has held no user key material until now.
Completes the trusted device flow for the case it was missing: a member who
unlocks with a trusted device, has no other device of their own left to ask,
and therefore has no way back into their vault. They can now turn to the
administrators of their organization, who hand them their own user key
encrypted for the key pair of the asking device.
New `atype` on auth_requests, mirroring bitwarden/server's AuthRequestType.
It decides who may answer a request and how long it stays open: 15 minutes
between the user's own devices, a week for an administrator, and half a day
for their answer once given. The purge job applies that per type instead of
dropping everything after 15 minutes, and both the answer and the anonymous
lookup now refuse an expired request, which they did not before.
POST /auth-requests/admin-request ask, one request per org
GET /organizations/<id>/auth-requests what is waiting for an answer
POST /organizations/<id>/auth-requests/<id> approve or deny one
POST /organizations/<id>/auth-requests/deny deny several
POST /organizations/<id>/auth-requests answer several
Asking requires authentication, so the anonymous `POST /auth-requests` now
refuses the type. Answering goes through the organization the request was
addressed to and needs admin rights there; the asking user cannot answer
their own request through `PUT /auth-requests/<id>`, which would make the
whole detour pointless. A denial is saved but not announced, so a request
that did not come from the member does not learn that it was seen. The
administrator's view leaves out the access code, which is the asking
device's proof and none of their business.
The administrators are mailed when a request arrives, the member when one of
their devices was let in, so an approval nobody asked for does not pass
unnoticed.
Two fixes without which none of this is reachable from a client:
- `UserDecryptionOptions.TrustedDeviceOption` reported `HasAdminApproval`
and `HasManageResetPasswordPermission` as a flat false. The clients
decide on `hasAdminApproval || hasMasterPassword` whether a login is a
returning user or a brand new one, so a member without a master password
was shown the screen for creating an account, on every device but the
one they first trusted. Both are now derived from the account recovery
enrollment and the role.
- `PUT /organizations/<id>/users/<id>/reset-password-enrollment` demanded a
master password whenever a key was supplied. An account that unlocks
with a trusted device has none, and the clients send nothing but the key
when they enroll during registration, so enrolling was impossible for
exactly the accounts that need it most. Upstream carves out the same
exception, keyed on the organization's SSO configuration rather than on
a server-wide setting as here. Enrolling now also accepts a pending
invitation, as upstream does, so a just-provisioned member does not stay
invited forever with nobody able to confirm them.
Lets a user unlock their vault after an SSO login with a key stored on a
trusted device instead of a master password, what Bitwarden calls trusted
device encryption. Off by default, enabled with
`SSO_TRUSTED_DEVICE_ENCRYPTION`, which requires `SSO_ENABLED`.
The client generates a key pair plus a device key that never leaves the
device and stores three blobs on the device row: the user key wrapped for
the device public key, that public key wrapped with the user key, and the
device private key wrapped with the device key. The server only keeps and
returns them, so a stored trust is worthless without the device itself.
A device counts as trusted only while all three are present.
Endpoints, matching bitwarden/server's DevicesController:
PUT|POST /devices/<identifier>/keys trust a device
POST /devices/<identifier>/retrieve-keys the public halves, for rotation
POST /devices/update-trust re-wrap after a key rotation
POST /devices/untrust drop the trust of some devices
POST /devices/lost-trust client reports a drifted trust
`GET /devices`, `GET /devices/identifier/<id>` and the login response now
report the real trust state instead of a hard-coded `isTrusted: false`.
An SSO login response carries `UserDecryptionOptions.TrustedDeviceOption`,
which is what makes the clients offer the flow at all. Upstream ties this to
the SSO configuration of an organization; SSO is configured for the whole
server here, so the setting decides it, and members of no organization can
use it too. A password login never gets these options. When the setting is
turned off again, devices that are still trusted keep receiving their keys
with `IsTdeOffboarding` set, so their owner can still unlock and set a
master password instead of being locked out.
`HasAdminApproval` and `HasManageResetPasswordPermission` are always false:
approval of a new device by an organization admin is not implemented, and
announcing it would leave the client waiting for a request nobody can
answer. A new device is unlocked by approving it from an already trusted
device or with the master password.
Two adjacent fixes this depends on:
- `POST /accounts/keys` refused to notice that an account already has a
key pair and would happily replace it, which makes every existing
cipher undecryptable. It now only accepts keys for an account that has
none, and tolerates a repeat of the same keys. The trusted device flow
is what makes this reachable in practice: a client that misjudges an
existing account as new posts a fresh key pair here.
- `POST /accounts/set-password` keyed "account already initialized" off
the key pair, so an account created without a master password could
never gain one. It now keys off the master password itself and refuses
to replace an existing key pair, which is what the check was guarding.
Rotating the account keys leaves every device holding a wrapped copy of the
previous user key. Those are dropped; the rotating device keeps its own
private key so its client can re-wrap the new user key right away via
`/devices/update-trust`. A client that skips that call ends up with no
trusted device rather than a broken unlock.
Covers dani-garcia/vaultwarden#7034.
The bundled web vault (2026.6.4) requires seven query parameters in the
accept-organization URL and rejects the invite client-side when any of them is
null, showing only "Unable to accept invitation" without sending a request to
the server.
send_invite() never appended initOrganization, and appended
orgUserHasExistingUser only for users who already had an account, so every
organization invitation e-mail produced a link that could not be accepted.
Web vault 2026.4.1 (shipped with 1.36.0) read these parameters null-safely,
which is why this only appeared in 1.37.0.
Fixes#7481
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
- Updated API response to more closely match v2026.6.0+ server versions.
- Updated all the crates
- Updated Rust to v1.97.1
- Updated the web-vault to v2026.6.4
- Updated GitHub Actions
Signed-off-by: BlackDex <black.dex@gmail.com>
* Misc updates and fixes
- Updated Rust to v1.96.1
- Updated all the crates
- Updated GitHub Actions
- Updated the web-vault to v2026.6.2
- Updated Alpine to v3.24
- Fixed several clippy lints
- The `send` UUID wrappers didn't need the special namespace anymore since an updated crate, so removed this extra mod.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update MSRV to v1.94.1
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
* ci: remove dead BASE_TAGS reference in release bake step
steps.determine-version doesn't exist in docker-build; the expression
resolves to empty string. The HCL default (testing) would have
applied, but it's moot - the bake uses push-by-digest=true so tags are
only set in merge-manifests. Dead code.
* ci: replace unsecured curl hadolint download with an official action
hadolint/hadolint-action uses a Docker-based runner with hadolint
pre-bundled in ghcr.io/hadolint/hadolint:v2.14.0-debian,so no binary
downloaded at runtime. Pinning the action to a commit SHA covers the
Dockerfile that specifies the image version, closing the supply-chain
gap from the previous unverified curl | sudo install.
Split {debian,alpine}: the action takes a single dockerfile argument,
so debian and alpine are linted separately.
* ci: pin ubuntu-latest to ubuntu-24.04 in merge-manifests and zizmor
ubuntu-latest is a moving target that can silently change the runner OS
on the next GitHub-side update. All other jobs in this repo already pin
to ubuntu-24.04; this makes merge-manifests and zizmor consistent.
* ci: return BASE_TAGS - it's needed for bake step
The /api/sync response serialized a Send hide_email field directly from
Option<bool>, so a NULL value in the sends table (the column is
Nullable<Bool> with no default) produced "hideEmail": null.
The Bitwarden Android client deserializes SyncResponseJson.Send.hideEmail
as a non-null Kotlin Boolean and aborts the entire sync with a
JsonDecodingException when it encounters null. Web, desktop and CLI
clients coerce null to false, so only accounts with at least one Send
are affected and only on Android.
Default None to false at the serialization boundary, matching the
official Bitwarden server where hideEmail is non-nullable. This needs no
database migration and fixes both legacy NULL rows and any future NULLs.
The hide_email field stays Option<bool> internally.
- Updated Rust to v1.96.0
- Updated all the crates, and adjusted code where needed
- Fixed some nightly reported clippy lints
- Updated all the GitHub actions
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update to Rust 2024 Edition
Updated to the Rust 2024 Edition and added and fixed several lint checks.
This is a large change which, because of the extra lints, added some possible fixes for issues.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Reorder and merge imports
Signed-off-by: BlackDex <black.dex@gmail.com>
* Remove "db_run!" macro calls where possible
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update crates and gha
Updated all the crates
Updated GitHub Actions
Signed-off-by: BlackDex <black.dex@gmail.com>
* Fix restoring revoked user
A new endpoint is used to restore a revoked user.
This commit fixes that.
Fixes#7224
Signed-off-by: BlackDex <black.dex@gmail.com>
* Update datatables
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>
* Panic on unrecognised DATABASE_URL instead of silent SQLite fallback
Previously, any DATABASE_URL that did not match the mysql: or postgresql:
prefix was silently treated as a SQLite file path. This caused data loss
in containerised environments when the URL was misconfigured (typos,
quoting issues), as vaultwarden would create an ephemeral SQLite database
that was wiped on restart.
Now, an explicit sqlite:// prefix is supported and used as the default.
Bare paths without a recognised scheme are still accepted for backwards
compatibility, but only if the database file already exists. If not, the
process panics with a clear error message.
Relates to #2835, #1910, #860.
* Use err!() instead of panic!() for unrecognised DATABASE_URL
Follow the established codebase convention where configuration
validation errors use err!() to propagate gracefully, rather than
panic!(). The error propagates through from_config() and is caught
by create_db_pool() which logs and calls exit(1).
* Use 'scheme' instead of 'prefix' in DATABASE_URL messages
Per review feedback, 'scheme' is the more accurate term for the
sqlite:// portion of the URL.
* deps: upgrade the reqwest stack to 0.13
The reqwest 0.13 rustls feature selects the aws-lc provider. Use
rustls-no-provider instead, add rustls 0.23 with the ring provider, and
install that provider at process startup. This keeps Vaultwarden on the
existing ring crypto provider while giving reqwest, OpenDAL and lettre a
process-wide rustls provider.
Disable openidconnect default features and provide a small
AsyncHttpClient wrapper around Vaultwarden's shared reqwest client
builder. This preserves custom DNS, request blocking, timeouts and the
no-redirect OIDC behavior without openidconnect enabling its own reqwest
stack.
Upgrade yubico_ng to 0.15.0 and OpenDAL to 0.56.0. OpenDAL 0.56 also
moves S3 signing to reqsign 3, so switch the optional S3 dependencies
from reqsign/anyhow to reqsign-core and reqsign-aws-v4 and adapt the AWS
SDK credential bridge to the new ProvideCredential API.
Adjust the local OpenDAL call sites for the 0.56 API: use the FS_SCHEME
constant for filesystem checks and replace deprecated remove_all() with
delete_with(...).recursive(true) for Send file cleanup.
* storage: add OpenDAL S3 URI options
OpenDAL S3 storage accepts bucket and root path data today, but
serverless deployments also need URI query parameters to describe provider
behavior in one DATA_FOLDER value.
Update OpenDAL to 0.56.0 and build S3 operators with
S3Config::from_uri(). Keep Vaultwarden's AWS SDK credential chain by
installing a reqsign provider when the URI does not explicitly request
OpenDAL-native credential handling.
Move path handling and operator construction into storage.rs so S3-specific
parsing, credential setup, and URI path manipulation stay out of
configuration handling. Local filesystem behavior is unchanged, and S3
child paths are derived before query strings.
- Update crates including fixing a regression of Diesel
- Update web-vault to v2026.4.1
- Adjusted the README to address the secure context and needing HTTPS
Fixes#7132Closes#7137
Signed-off-by: BlackDex <black.dex@gmail.com>
Keeping the default behaviour of SQLite being built statically,
so as not to break anyone's workflow, but allowing for downstream
packagers to link dynamically against SQLite (where it's fine because
that's the point of package managers).
Note that SQLite is still *not* enabled by default, thanks to the `?` operator.
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
`Cipher::to_json()` returns `Result<Value, Error>` but its match arm for
unknown `atype` values called `panic!("Wrong type")` instead of
propagating an error. This means if a cipher with an invalid/unknown type
ends up in the database (via direct DB edits, data migration issues, or
future type additions in the upstream Bitwarden protocol), the entire
server process would crash on the next sync request.
Replace the `panic!` with `err!()` so callers receive a proper `Err` and
can handle or log it gracefully without taking down the server.
Co-authored-by: easonysliu <easonysliu@tencent.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
The collection update endpoints (post_collections_update and
post_collections_admin) call .unwrap() on cipher.organization_uuid
in four places. If a user-owned cipher without an organization
somehow reaches these code paths, the server would panic.
Extract the organization UUID early with a descriptive error message
instead of relying on .unwrap(), preventing potential panics and
providing a clear API error response.
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
* Add archiving
* Update Diesel macros and remove unnecessary SUPPORTED_FEATURE_FLAG
* Add IF EXISTS to down.sql migratinos
* Rename migration folders, separate logic based on PR threads
* Ensure SSO token is only usable on the same client
This commit adds an extra check via cookies to ensure the same browser/client is used to request and provide the SSO token.
Previously it would be able to provide a custom link which attackers could use to steal data.
While an attacker would still need the Master Password to be able to decrypt or execute specific actions, they were able to fetch encrypted data.
Solved with some help of Claude Code.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Check email-verified on SSO login/create
This commit prevents possible account takeover via SSO which doesn't check/validate or provide validated status of the email.
It was checked at other locations, but was skipped here.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Prevent data disclosure via SSO endpoints
This commit prevents some data disclosure and user enumeration by only returning the fake SSO identifier.
Since we do not check the identifier anywhere useful, returning the fake one is just fine.
During an invite to an org, that link contains the correct UUID and will be used for the master password requirements.
For anything else, server admins should set the `SSO_MASTER_PASSWORD_POLICY` env variable.
Signed-off-by: BlackDex <black.dex@gmail.com>
* Adjust admin layout to fix issues when SSO is enabled
Signed-off-by: BlackDex <black.dex@gmail.com>
---------
Signed-off-by: BlackDex <black.dex@gmail.com>