feat(transport): production-boundary signing advisory, secret-key proof, runbook (#110)
Owner decisions on #110: one signing key per environment; TrustedKeys stay hand-edited; Signing:ProductionBoundary (new, default off) logs a startup warning and reports a Degraded bundle-signing-policy /health/ready entry while RequireSignedBundles is off; the private key is read via ${secret:} (proved end to end, never logged); cluster pull decided not to be built; differential deletions stay deferred. Adds docs/operations/bundle-signing-runbook.md. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.ScadaBridge.Transport;
|
||||
using ZB.MOM.WW.ScadaBridge.Transport.Signing;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Host.Health;
|
||||
|
||||
/// <summary>
|
||||
/// /health/ready entry (ScadaBridge#110): Degraded while a cluster marked
|
||||
/// <c>Transport:Signing:ProductionBoundary</c> still accepts unsigned bundles. Degraded maps
|
||||
/// to HTTP 200 in the shared health tier, so it is visible on the Overview dashboard without
|
||||
/// taking the node out of rotation. Exposes only booleans — never key material.
|
||||
/// </summary>
|
||||
public sealed class BundleSigningPolicyHealthCheck : IHealthCheck
|
||||
{
|
||||
private readonly IOptionsMonitor<TransportOptions> _options;
|
||||
|
||||
/// <summary>Initializes the check.</summary>
|
||||
/// <param name="options">Transport options.</param>
|
||||
public BundleSigningPolicyHealthCheck(IOptionsMonitor<TransportOptions> options)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<HealthCheckResult> CheckHealthAsync(
|
||||
HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var signing = _options.CurrentValue.Signing;
|
||||
var data = new Dictionary<string, object>(StringComparer.Ordinal)
|
||||
{
|
||||
["productionBoundary"] = signing.ProductionBoundary,
|
||||
["requireSignedBundles"] = signing.RequireSignedBundles,
|
||||
["signingEnabled"] = signing.Enabled,
|
||||
["trustedKeys"] = signing.TrustedKeys.Count,
|
||||
};
|
||||
|
||||
var advisory = BundleSigningPolicyAdvisory.Evaluate(signing);
|
||||
return Task.FromResult(advisory is null
|
||||
? HealthCheckResult.Healthy("Bundle signing policy OK.", data)
|
||||
: HealthCheckResult.Degraded(advisory, data: data));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user