[PM-40801] Add option for alternative DataProtection settings on FIPS nodes - #8092
[PM-40801] Add option for alternative DataProtection settings on FIPS nodes#8092eligrubb wants to merge 1 commit into
Conversation
🤖 Bitwarden Claude Code ReviewOverall Assessment: REQUEST CHANGES Re-reviewed the Code Review Details
Resolved since the previous pass:
|
|
|
||
| /// <summary> | ||
| /// Defines how ASP.NET Core data-protection keys are protected at rest. | ||
| /// Migration between types is not supported. |
There was a problem hiding this comment.
StorageManaged silently regenerates the key ring instead of failing fast.
Details and fix
"Migration between types is not supported" is documented here, but nothing enforces it. If StorageManaged is set on a region whose aspnet-dataprotection/keys.xml already holds certificate-wrapped keys, no protect/unprotect certificate is registered, so keys wrapped with the blob-stored certificate can no longer be decrypted. ASP.NET then marks those keys ineligible, generates a fresh key, and the app starts successfully — every value protected with the old keys stops round-tripping, including User.MasterPassword and User.Key (DatabaseFieldProtection, see src/Infrastructure.Dapper/Repositories/UserRepository.cs), so logins break until the config is reverted.
This is the same silent-divergence failure mode that UnprotectCertificateMissingFromBlobStorage_Throws was written to prevent ("pods came up... and auto-generated their own keys — producing a divergent key ring across a rolling deploy").
Suggested guard: when KeyProtectionPolicy == StorageManaged, read keys.xml at startup and throw InvalidOperationException if it exists and contains encryptedSecret — the same signal the new StorageManaged_PersistsUnwrappedKeysWithoutAcquiringCertificates test asserts on.
| if (globalSettings.DataProtection.KeyProtectionPolicy == | ||
| GlobalSettings.DataProtectionSettings.KeyProtectionPolicyType.StorageManaged) | ||
| { | ||
| return; | ||
| } |
There was a problem hiding this comment.
❓ QUESTION: Was the loss of envelope encryption on the key ring confirmed with AppSec?
Context behind the question
This key ring is the wrapping key for Constants.DatabaseFieldProtectorPurpose, which protects User.MasterPassword and User.Key at rest (src/Infrastructure.Dapper/Repositories/UserRepository.cs), plus all DataProtectorTokenFactory tokens.
Under Certificate, unwrapping requires a secret that does not live in the storage account: either CertificatePassword (supplied via configuration, not stored in blob storage) or a certificate installed in the machine store for the thumbprint path. Under StorageManaged, read access to the storage account alone yields directly usable keys — so the PR description's "the key ring data is singularly protected by the connection string secret" holds for the key ring blob but not for the certificate that wraps it.
Two things would help reviewers of this change:
- Was a FIPS-compatible wrapping mechanism (for example Key Vault-based key wrapping) evaluated before removing application-level wrapping entirely, given Bitwarden's preference for keys at rest being wrapped by another key?
- If
StorageManagedis the accepted outcome, a reference to the AppSec/threat-model decision (rather than only the Slack thread) would make the tradeoff auditable later.
There was a problem hiding this comment.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #8092 +/- ##
=======================================
Coverage 62.68% 62.69%
=======================================
Files 2296 2296
Lines 100051 100059 +8
Branches 9004 9005 +1
=======================================
+ Hits 62721 62730 +9
+ Misses 35147 35146 -1
Partials 2183 2183 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-40801
📔 Objective
PKCS#1 v1.5 certificate encryption silently fails when running in a FIPS environment. To support running services in FIPS environments we want the option to adjust DataProtection encryption settings. This PR introduces a new
GlobalSettings:DataProtection:KeyProtectionPolicyenum. If the enum is unset or set toCertificate, the server will continue to use DataProtection via PKCS#1 v1.5 certificate encryption when storing the key ring. If theKeyProtectionPolicyis set toStorageManaged, then certificate-based DataProtection is skipped, in favor of storage-level protection at rest.💭 Implementation decisions
KeyProtectionPolicyshould only be set toStorageManagedfor new or self-hosted regions. So, this PR does not handle migrating key protection fromCertificatetoStorageManaged. WhenKeyProtectionPolicy=StorageManaged, adding certificate handling to the builder is skipped completely, for both Protecting and Unprotecting the key ring.This setting does not decrease user security. Currently, both the key ring and encrypting certificate are stored using the same connection string. With the
StorageManagedsetting on, the key ring is still stored using the connection string. No matter the setting, the key ring data is singularly protected by the connection string secret. Using theCertificatepolicy adds an additional non-FIPS compliant layer. See the related slack discussion for more.Examples of setting new env var
Using a certificate thumbprint:
{ "GlobalSettings": { "Storage": { "ConnectionString": "<azure-storage-connection-string>" }, "DataProtection": { "KeyProtectionPolicy": "Certificate", "CertificateThumbprint": "<certificate-thumbprint>" } } }Or using a certificate stored in Azure Blob Storage:
{ "GlobalSettings": { "Storage": { "ConnectionString": "<azure-storage-connection-string>" }, "DataProtection": { "KeyProtectionPolicy": "Certificate", "BlobName": "dataprotection.pfx", "CertificatePassword": "<certificate-password>" } } }Certificate is the default, so KeyProtectionPolicy may be omitted.
No application-level certificate wrapping:
{ "GlobalSettings": { "Storage": { "ConnectionString": "<azure-storage-connection-string>" }, "DataProtection": { "KeyProtectionPolicy": "StorageManaged" } } }