fix(secrets-cli): degrade on malformed master-key source + topology note

This commit is contained in:
Joseph Doherty
2026-07-19 09:08:24 -04:00
parent 83af2b2eaf
commit 9bc1e5852e
2 changed files with 36 additions and 0 deletions
@@ -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
@@ -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()
{ {