fix(secrets-cli): degrade on malformed master-key source + topology note
This commit is contained in:
@@ -53,6 +53,11 @@ public sealed class SecretsSessionFactory
|
|||||||
// Mirrors the DI extensions' store selection: a configured SQL-Server hub wins, EXCEPT when its
|
// Mirrors the DI extensions' store selection: a configured SQL-Server hub wins, EXCEPT when its
|
||||||
// connection string is an unresolved '${secret:}' reference (the app resolves that from its own
|
// connection string is an unresolved '${secret:}' reference (the app resolves that from its own
|
||||||
// bootstrapped store at startup; the CLI has no such resolver), in which case fall back to SQLite.
|
// bootstrapped store at startup; the CLI has no such resolver), in which case fall back to SQLite.
|
||||||
|
//
|
||||||
|
// The CLI deliberately treats a configured Secrets:SqlServer section as the authoritative target
|
||||||
|
// regardless of the app's replication topology: in hub-replication mode the SQL-Server hub is the
|
||||||
|
// shared source of truth, so editing it here is correct — the change converges to each node's local
|
||||||
|
// store via the sync sweep, rather than being written to one node's local SQLite and racing the hub.
|
||||||
private static (ISecretStore Store, ISecretsStoreMigrator Migrator, string StoreKind) BuildStore(
|
private static (ISecretStore Store, ISecretsStoreMigrator Migrator, string StoreKind) BuildStore(
|
||||||
StoreTarget target, List<string> warnings)
|
StoreTarget target, List<string> warnings)
|
||||||
{
|
{
|
||||||
@@ -99,6 +104,16 @@ public sealed class SecretsSessionFactory
|
|||||||
|
|
||||||
return (provider, new AesGcmEnvelopeCipher(provider));
|
return (provider, new AesGcmEnvelopeCipher(provider));
|
||||||
}
|
}
|
||||||
|
catch (ArgumentOutOfRangeException ex)
|
||||||
|
{
|
||||||
|
// MasterKeyProviderFactory.Create throws this when MasterKeySource is out of range — a
|
||||||
|
// malformed config value. This is operator-facing lockout tooling, so degrade rather than
|
||||||
|
// crash; name the invalid value so the operator can see what to fix.
|
||||||
|
warnings.Add(
|
||||||
|
$"Master-key source '{target.Secrets.MasterKey.Source}' is not a recognized value — " +
|
||||||
|
$"session is degraded (metadata only): {ex.Message}");
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
catch (MasterKeyUnavailableException ex)
|
catch (MasterKeyUnavailableException ex)
|
||||||
{
|
{
|
||||||
// Thrown by every provider (Environment/File/Dpapi) and the base length check when the key
|
// Thrown by every provider (Environment/File/Dpapi) and the base length check when the key
|
||||||
|
|||||||
+21
@@ -95,6 +95,27 @@ public sealed class SecretsSessionFactoryTests : IDisposable
|
|||||||
Assert.Empty(list);
|
Assert.Empty(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Opens_degraded_session_when_master_key_source_is_out_of_range()
|
||||||
|
{
|
||||||
|
StoreTarget target = new TargetConfigReader().Manual(DbPath, new MasterKeyOptions
|
||||||
|
{
|
||||||
|
Source = (MasterKeySource)999,
|
||||||
|
});
|
||||||
|
|
||||||
|
SecretsSession session =
|
||||||
|
await new SecretsSessionFactory().OpenAsync(target, overrideKek: null, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.False(session.KekAvailable);
|
||||||
|
Assert.Null(session.Cipher);
|
||||||
|
Assert.NotEmpty(session.Warnings);
|
||||||
|
Assert.Contains(session.Warnings, w => w.Contains("999", StringComparison.Ordinal));
|
||||||
|
|
||||||
|
// Store is still usable despite the malformed KEK source.
|
||||||
|
IReadOnlyList<SecretMetadata> list = await session.Store.ListAsync(includeDeleted: false, CancellationToken.None);
|
||||||
|
Assert.Empty(list);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Override_kek_upgrades_degraded_session()
|
public async Task Override_kek_upgrades_degraded_session()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user