fix(host): distinguish secret-store fault from missing PSK secret (review 2026-10-21 05/U2)

This commit is contained in:
Joseph Doherty
2026-10-22 08:41:54 -04:00
parent 12f815a7ea
commit 4921ebb0c8
2 changed files with 42 additions and 12 deletions
@@ -114,10 +114,9 @@ public sealed class SitePskProvider : ISitePskProvider
}
/// <summary>
/// Reads the key from the secrets store, treating a store fault as "not found" rather than
/// letting it propagate. A host with no master key configured (the development rig) throws
/// from the resolver; that must produce the same clear "key not configured" error as a
/// missing secret, not an opaque cryptographic one.
/// Reads the key from the secrets store. A store fault (unreachable SQL Server, no master
/// key) surfaces as <see cref="SecretStoreUnavailableException"/>, distinct from a secret
/// that is simply missing (null return), and is never cached.
/// </summary>
private async Task<string?> ResolveFromStoreAsync(string secretName, string siteId, CancellationToken ct)
{
@@ -131,10 +130,14 @@ public sealed class SitePskProvider : ISitePskProvider
}
catch (Exception ex)
{
_logger.LogDebug(ex,
"Secret store lookup for '{SecretName}' (site {SiteId}) faulted; treating as not found.",
// A store fault is NOT "secret missing": telling the operator to seed a secret that
// exists (while the shared store is merely unreachable) sends them the wrong way.
// Surface it as a distinct exception type and keep the cause visible at Warning.
_logger.LogWarning(ex,
"Secret store lookup for '{SecretName}' (site {SiteId}) faulted; the key could not be "
+ "read (this is a store fault, not a missing secret).",
secretName, siteId);
return null;
throw new SecretStoreUnavailableException(secretName, siteId, ex);
}
}
@@ -147,3 +150,29 @@ public sealed class SitePskProvider : ISitePskProvider
}
}
}
/// <summary>
/// The secrets store could not be read while resolving a site's gRPC preshared key — a store
/// fault, as opposed to the secret being absent (review 2026-10-21 05/U2).
/// </summary>
public sealed class SecretStoreUnavailableException : InvalidOperationException
{
/// <summary>Initializes the exception.</summary>
/// <param name="secretName">The secret that was being read.</param>
/// <param name="siteId">The site whose key was being resolved.</param>
/// <param name="inner">The underlying store fault.</param>
public SecretStoreUnavailableException(string secretName, string siteId, Exception inner)
: base($"The secret store could not be read while resolving gRPC preshared key '{secretName}' "
+ $"for site '{siteId}' ({inner.GetType().Name}: {inner.Message}); the secret may exist — "
+ "this is a store fault, not a missing secret.", inner)
{
SecretName = secretName;
SiteId = siteId;
}
/// <summary>Gets the secret that was being read.</summary>
public string SecretName { get; }
/// <summary>Gets the site whose key was being resolved.</summary>
public string SiteId { get; }
}