6.5 KiB
SCIM v2 provisioning
This fork adds a SCIM v2 provisioning server (RFC 7643 / RFC 7644) so organization membership can be driven from an identity provider. Microsoft Entra ID is the tested IdP. Design details and diagrams: design.md.
What it does, honestly
- Automated invite: assigning a user in the IdP creates the Vaultwarden account (if needed) and org membership, and sends the invite email.
- Automated deprovision: removing a user (or setting
active: false) revokes org access immediately. Restore is lossless. - Group sync: group existence and membership, when
ORG_GROUPS_ENABLED=true. - Not automated: the final Confirm step. End-to-end encryption means the org key must be wrapped for each member by an admin's client; no server can do it. Provisioned users wait in Invited / Accepted until an admin confirms them in the web vault. See design.md for why this is a property of the encryption model, not a missing feature.
Server setup
-
Enable SCIM in the server config (default is off):
SCIM_ENABLED=true # strongly recommended: without it, SCIM changes leave no audit trail ORG_EVENTS_ENABLED=true # optional tuning SCIM_RATELIMIT_SECONDS=1 SCIM_RATELIMIT_MAX_BURST=60The server prints a startup warning if
SCIM_ENABLEDis set whileORG_EVENTS_ENABLEDis not, because every SCIM change would then be invisible in the org event log. -
Generate the per-organization SCIM token (requires an org admin session; re-authenticates with your master password like API key rotation):
curl -s -X POST "$DOMAIN/api/organizations/$ORG_ID/scim/api-key" \ -H "Authorization: Bearer $ADMIN_SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{"masterPasswordHash": "..."}'The response contains the token (
scim_v1.<org>.<secret>) and the tenant URL. The token is shown exactly once; only its sha256 digest is stored. POST again to rotate (the old token stops working immediately), DELETE the same path to disable SCIM for the org, GET/scim/statusto inspect. -
TLS is required in front of the server (Entra refuses plain http). If a reverse proxy sets client IPs, make sure it overwrites the
IP_HEADERheader (defaultX-Real-IP); the SCIM rate limiter keys on that value.
Entra ID setup
-
Entra admin center: Enterprise applications > New application > Create your own application (non-gallery).
-
Provisioning > Automatic:
- Tenant URL:
https://<your-domain>/scim/v2/<org_id> - Secret Token: the
scim_v1...value from step 2 above. - Test Connection, then Save.
- Tenant URL:
-
Attribute mappings (Users):
Entra attribute SCIM attribute Vaultwarden userPrincipalName (or mail) userNameaccount email (lowercased) objectId externalIdmembership external id Switch([IsSoftDeleted], , "False", "True", "True", "False") activerevoke / restore displayName displayNameaccount name (set at creation only) Map
userNamefrom mail rather than userPrincipalName if your UPNs are not routable mailboxes: the value must be a deliverable email address, because it becomes the Vaultwarden login and receives the invite.Recommended: delete the default mappings this server does not sync (
roles,preferredLanguage,title, addresses, phones). They are accepted and ignored, but trimming them keeps the Entra sync log clean. -
Groups: map
displayNameandobjectIdtoexternalId. Members sync as diffs. Assign users before (or together with) their groups; a group member who is not yet provisioned as a user is rejected until the user sync runs. -
Assign users/groups to the app and start provisioning. Entra's initial cycle lists existing users first (
userName eqfilters), then creates.
Behaviour notes and deviations
- DELETE = revoke. Both Entra soft delete (
active: false) and hard DELETE revoke the membership. The row and its keys survive, so restoring a returning user needs no re-confirmation. No destructive operation is exposed to the IdP at all. This is a deliberate deviation from RFC 7644. - Roles are not synced. Everyone provisions as the User role; promote people in the web vault. Vaultwarden's Custom role cannot round-trip through SCIM.
- userName / displayName changes are not synced after creation. The email is the login identity; the display name belongs to the person globally, not to one org's directory. Renames in Entra succeed (accepted and ignored) rather than erroring the sync.
- SCIM user id is the org membership id, not the account id. The same person in two orgs has two SCIM ids: SCIM is org-scoped by construction.
- externalId is unique per org for both Users and Groups, on every write
path (create, PUT, PATCH). A write that would duplicate one is a 409
uniquenessconflict and changes nothing; re-asserting a resource's own externalId succeeds. - A Group PUT that omits
membersleaves the member set unchanged. Only an explicit"members": []clears the group, so a sparse non-Entra client cannot wipe membership by accident (Entra always sends the full list). - Deprovision from the IdP, not just the vault. Because restore is lossless,
a member you revoke in the web vault is silently re-activated on the next sync
if the IdP still shows them active -
active: truereinstates a previously confirmed member to full access with no re-confirmation. To offboard someone, unassign or disable them in Entra. Rotate the org's SCIM token if it may have leaked: it can reinstate any member the org previously confirmed, not only invite and deprovision. - Every SCIM change is written to the org event log (admin-visible) with the
synthetic actor
vaultwarden-scim-...whenORG_EVENTS_ENABLED=true. Token generation, rotation, and deletion are logged too, under the acting admin's own identity.
Operational lifecycle
- Entra assigns user, SCIM creates membership at Invited, invite mail sent.
- User clicks the invite, creates or logs into their account (Accepted).
- An org admin confirms the member in the web vault (Confirmed). This is the manual step; the admin's client wraps the org key for the member.
- Entra unassigns or soft deletes, SCIM revokes immediately. Vault access stops on the member's next sync.
- Re-assignment restores the membership exactly as it was, including the confirmed state, with no new invite or confirmation needed.