7912d793e2
Central re-bound its HTTP surface from ASPNETCORE_URLS keeping only the port, so https://+:443 silently served plaintext HTTP. The parser now keeps the scheme (ParseHttpBindEndpoints); an https entry binds UseHttps with the node certificate from the new ScadaBridge:Node:Tls section (CertificatePath + CertificatePassword, or CertificateThumbprint + StoreName + StoreLocation; default unset) and the node refuses to boot, with a clear error, when https is requested without a certificate. http entries bind exactly as before. Tests: scheme parsing, refuse-to-boot, http endpoint config identical to the legacy inline binding (reflected effective Kestrel listen options), and a real Kestrel HTTPS listener with a generated .pfx serving h2 and HTTP/1.1 by ALPN while plaintext to the TLS port fails. Reverting the fix turns the refuse-to-boot and real-listener tests red. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
74 lines
3.9 KiB
C#
74 lines
3.9 KiB
C#
namespace ZB.MOM.WW.ScadaBridge.Host;
|
|
|
|
public class NodeOptions
|
|
{
|
|
/// <summary>Gets or sets the node role (e.g. Central or Site).</summary>
|
|
public string Role { get; set; } = string.Empty;
|
|
/// <summary>Gets or sets the hostname or IP address this node advertises to the Akka cluster.</summary>
|
|
public string NodeHostname { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Operator-configured semantic node name used to stamp the SourceNode
|
|
/// column on audit rows. Conventional values are <c>node-a</c>/<c>node-b</c>
|
|
/// on site nodes and <c>central-a</c>/<c>central-b</c> on central nodes,
|
|
/// but the value is a free-form label — no validation is enforced.
|
|
/// </summary>
|
|
public string NodeName { get; set; } = string.Empty;
|
|
/// <summary>Gets or sets the site identifier for site nodes; null for central nodes.</summary>
|
|
public string? SiteId { get; set; }
|
|
/// <summary>Gets or sets the Akka.NET remoting port for cluster communication.</summary>
|
|
public int RemotingPort { get; set; } = 8081;
|
|
/// <summary>Gets or sets the gRPC port for the site stream server.</summary>
|
|
public int GrpcPort { get; set; } = 8083;
|
|
/// <summary>
|
|
/// HTTP/2 (h2c) port the CENTRAL node listens on for the site→central
|
|
/// <c>CentralControlService</c> gRPC control plane. Default 8083 — deliberately symmetric
|
|
/// with the site <see cref="GrpcPort"/>, since a node is either central or a site and the
|
|
/// two never share a process. This listener is distinct from central's <c>:5000</c> HTTP/1
|
|
/// surface (Central UI, Management/Inbound API), which stays exactly as-is: gRPC does NOT go
|
|
/// through Traefik (HTTP/1 only). Ignored on site nodes.
|
|
/// </summary>
|
|
public int CentralGrpcPort { get; set; } = 8083;
|
|
/// <summary>
|
|
/// HTTP/1.1 port serving the Prometheus /metrics scrape endpoint on site nodes.
|
|
/// Defaults to 8084 — deliberately distinct from <see cref="RemotingPort"/> (8082)
|
|
/// and <see cref="GrpcPort"/> (8083) so the Kestrel metrics listener never contends
|
|
/// with the Akka remoting port a site node binds.
|
|
/// </summary>
|
|
public int MetricsPort { get; set; } = 8084;
|
|
|
|
/// <summary>
|
|
/// This node's own TLS certificate for the Kestrel HTTP surface (ScadaBridge#108, design
|
|
/// 2026-11-09-tls-production-profile stage 1). Default: nothing set. Required if (and only if)
|
|
/// <c>ASPNETCORE_URLS</c> contains an <c>https://</c> entry; central then refuses to boot
|
|
/// without it. Applies to central's HTTP/1.1+HTTP/2 surface only; the gRPC listener is unchanged.
|
|
/// </summary>
|
|
public NodeTlsOptions Tls { get; set; } = new();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Certificate source for <see cref="NodeOptions.Tls"/>: a .pfx path plus password, OR a
|
|
/// certificate-store thumbprint. Same shape as the cluster's <c>RemotingTls</c> certificate fields.
|
|
/// </summary>
|
|
public sealed class NodeTlsOptions
|
|
{
|
|
/// <summary>Path to a PKCS#12 (.pfx) file with the private key. Set this OR <see cref="CertificateThumbprint"/>.</summary>
|
|
public string? CertificatePath { get; set; }
|
|
|
|
/// <summary>Password for <see cref="CertificatePath"/>; write it as <c>${secret:NAME}</c>, never a literal.</summary>
|
|
public string? CertificatePassword { get; set; }
|
|
|
|
/// <summary>Thumbprint of a certificate in the OS store. Set this OR <see cref="CertificatePath"/>.</summary>
|
|
public string? CertificateThumbprint { get; set; }
|
|
|
|
/// <summary>Store name for <see cref="CertificateThumbprint"/>. Default <c>My</c>.</summary>
|
|
public string StoreName { get; set; } = "My";
|
|
|
|
/// <summary>Store location for <see cref="CertificateThumbprint"/>: <c>local-machine</c> (default) or <c>current-user</c>.</summary>
|
|
public string StoreLocation { get; set; } = "local-machine";
|
|
|
|
/// <summary>Whether any certificate source field is set.</summary>
|
|
public bool IsConfigured =>
|
|
!string.IsNullOrWhiteSpace(CertificatePath) || !string.IsNullOrWhiteSpace(CertificateThumbprint);
|
|
}
|