Secure Bitwarden with cnspec
Scan Bitwarden organization policies, members, collections, and groups against security and compliance best practices with cnspec.
A Bitwarden organization holds the credentials that open everything else, so the rules governing who joins it and what they can reach decide how far a single compromised member gets. cnspec reads a Bitwarden Teams or Enterprise organization through its Public API and evaluates the security policies the organization enforces, the members and their role, status, and two factor enrollment, the collections that structure vault access, and the groups and per member grants that decide who reaches which collection.
cnspec reads organization governance only. It never reads vault item contents, folders, or Send payloads: the Public API it talks to has no endpoint for them.
If you're new to cnspec, start with the Quickstart. For an overview of every identity provider cnspec can scan, see the identity scanning overview.
The Bitwarden resources are experimental. Field names and behavior can change in a future cnspec release.
Prerequisites
To scan Bitwarden with cnspec, you must have:
- cnspec installed on your workstation
- A Bitwarden Teams or Enterprise organization, since the Public API isn't available on Free and Families plans
- An organization API key
Authenticate
cnspec authenticates as the organization, not as a member, using an organization API key. To find it, open the Bitwarden web vault, go to the organization's Settings > Organization info page, and select View API key. The client ID has the form organization.<uuid>, where the UUID is the organization's own ID.
cnspec shell bitwarden --client-id organization.YOUR_ORG_UUID --client-secret YOUR_SECRETSelf-hosted deployments
A self-hosted Bitwarden instance serves the Public API and the identity endpoint from your own domain rather than from Bitwarden's cloud, so point cnspec at both:
cnspec shell bitwarden --client-id organization.YOUR_ORG_UUID --client-secret YOUR_SECRET \
--api-url https://bitwarden.example.com/api --identity-url https://bitwarden.example.com/identity/connect/tokenEnvironment variables
BITWARDEN_CLIENT_ID and BITWARDEN_CLIENT_SECRET supply the credentials, and BITWARDEN_API_URL and BITWARDEN_IDENTITY_URL point at a self-hosted deployment:
export BITWARDEN_CLIENT_ID=organization.YOUR_ORG_UUID
export BITWARDEN_CLIENT_SECRET=YOUR_SECRETWhen these are set, you can omit the matching flags from the commands below.
Connection options
| Option | Description |
|---|---|
--client-id | Organization client ID, of the form organization.<uuid> |
--client-secret | Organization client secret |
--api-url | Public API base URL, for a self-hosted deployment |
--identity-url | Identity token URL, for a self-hosted deployment |
Verify with a quick Bitwarden check
Confirm that cnspec can reach your organization by opening a cnspec shell:
cnspec shell bitwarden --client-id organization.YOUR_ORG_UUID --client-secret YOUR_SECRETcnspec> bitwarden.organization { name planName seats occupiedSeats }
bitwarden.organization: {
name: "Lunalectric"
planName: "Enterprise (Annually)"
seats: 250
occupiedSeats: 187
}If cnspec connects and reports your organization, you're ready to scan.
Scan Bitwarden
cnspec scan bitwarden --client-id organization.YOUR_ORG_UUID --client-secret YOUR_SECRETThe organization is a single asset. There are no child assets to discover.
When a scan completes, cnspec prints a summary of all the checks it ran, grouped by policy, along with a risk score from 0 (no risk) to 100 (highest risk). Failed checks include remediation guidance to help you fix issues. To learn more about reading scan results, read Understand cnspec Results.
Mondoo doesn't yet ship an out-of-the-box Bitwarden policy, so use the checks below as a starting point and create your own policies to meet your specific requirements.
Explore and test checks interactively
Open a cnspec shell to discover resources and try out checks:
cnspec shell bitwarden --client-id organization.YOUR_ORG_UUID --client-secret YOUR_SECRETReview what the organization's plan makes available
A control the plan doesn't include can't be enforced, so these fields bound what the rest of an audit can expect:
cnspec> bitwarden.organization {
name
planName
enabled
useSso
use2fa
usePolicies
useDirectory
useEvents
useResetPassword
}List the security policies and whether they're enforced
policyType selects the control and data carries its configuration, whose shape depends on the type:
cnspec> bitwarden.policies { policyType enabled data }Review the master password rules
The masterPassword policy carries its thresholds in data:
cnspec> bitwarden.policies.where(policyType == "masterPassword") { enabled data }List members, their role, and their security posture
cnspec> bitwarden.members { email role status twoFactorEnabled resetPasswordEnrolled accessAllCollections }Find members without two factor authentication
cnspec> bitwarden.members.where(status == "confirmed" && twoFactorEnabled == false) { email role }Review the collections and who reaches them
cnspec> bitwarden.collections { name groups { name } members { email role } }Review the permissions each grant carries
A grant is more than membership. readOnly, hidePasswords, and manage decide what the member does once inside the collection:
cnspec> bitwarden.collections { name memberAccess { member { email } readOnly hidePasswords manage } }Review the groups and the collections they reach
Every member of a group inherits the group's grants, so a group is the shortest path from one new member to many collections:
cnspec> bitwarden.groups { name accessAll members.length collections { name } }Review who was invited but never finished enrollment
Only a confirmed member holds working access to shared collections. Everyone else is mid onboarding, or was left behind:
cnspec> bitwarden.members.where(status != "confirmed") { email role status }Walk from a resource to its neighbors
Members, groups, and collections reference each other, so a query can start from any of them and walk in either direction:
cnspec> bitwarden.members { email groups { name } collections { name } }
cnspec> bitwarden.groups { name collectionAccess { collection { name } manage } }
cnspec> bitwarden.collections { name groupAccess { group { name } readOnly } }Example security checks
Ensure the organization requires two factor authentication
cnspec> bitwarden.policies.where(policyType == "twoFactorAuthentication").all(enabled == true)
[ok] value: trueEnsure every confirmed member has two factor authentication enabled
The policy governs who can stay in the organization. This check reports who's covered right now:
cnspec> bitwarden.members.where(status == "confirmed").all(twoFactorEnabled == true)
[ok] value: trueEnsure members sign in through single sign-on
cnspec> bitwarden.policies.where(policyType == "requireSso").all(enabled == true)
[ok] value: trueEnsure a member can't belong to another organization
Without the single organization policy, a member can join a second organization and move shared items into it:
cnspec> bitwarden.policies.where(policyType == "singleOrg").all(enabled == true)
[ok] value: trueEnsure items created by a member belong to the organization
cnspec> bitwarden.policies.where(policyType == "organizationDataOwnership").all(enabled == true)
[ok] value: trueEnsure a member can't export their personal vault
cnspec> bitwarden.policies.where(policyType == "disablePersonalVaultExport").all(enabled == true)
[ok] value: trueEnsure the master password policy sets a length
The masterPassword policy's data carries minLength, minComplexity, and the character class requirements:
cnspec> bitwarden.policies.where(policyType == "masterPassword").all(data['minLength'] >= 12)
[ok] value: trueEnsure no member reaches every collection
accessAllCollections bypasses the collection grants entirely, so the member reaches collections nobody granted them:
cnspec> bitwarden.members.none(accessAllCollections == true)
[ok] value: trueEnsure no group reaches every collection
A group with accessAll gives each of its members every collection, including the ones created after they joined:
cnspec> bitwarden.groups.none(accessAll == true)
[ok] value: trueEnsure ownership stays rare
An owner can change every policy the organization enforces:
cnspec> bitwarden.members.where(role == "owner").length <= 2
[ok] value: trueEnsure a revoked member holds no collection access
A revoked member keeps their grants until someone removes them, so revoking access and removing it aren't the same thing:
cnspec> bitwarden.members.where(status == "revoked").all(collections.length == 0)
[ok] value: trueEnsure invitations don't linger
An invitation that's never accepted is a seat nobody watches:
cnspec> bitwarden.members.none(status == "invited")
[ok] value: trueEnsure event logging is available to the organization
Without it, no record survives of who reached which collection:
cnspec> bitwarden.organization.useEvents == true
[ok] value: trueReview who can manage a collection
Managing a collection means granting it to others, so it's the grant that creates more grants:
cnspec> bitwarden.collections { name memberAccess.where(manage == true) { member { email role } } }Learn more
- Bitwarden Resource Pack Reference: every Bitwarden resource and field cnspec can query
- Write Effective MQL: guide to authoring checks and queries