Add configurable transport security profiles and bind address

Adds Security section to appsettings.json with configurable OPC UA
transport profiles (None, Basic256Sha256-Sign, Basic256Sha256-SignAndEncrypt),
certificate policy settings, and a configurable BindAddress for the
OPC UA endpoint. Defaults preserve backward compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-27 15:59:43 -04:00
parent bbd043e97b
commit 55173665b1
28 changed files with 1092 additions and 87 deletions

View File

@@ -24,6 +24,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
private readonly HistorianDataSource? _historianDataSource;
private readonly AuthenticationConfiguration _authConfig;
private readonly IUserAuthenticationProvider? _authProvider;
private readonly SecurityProfileConfiguration _securityConfig;
private ApplicationInstance? _application;
private LmxOpcUaServer? _server;
@@ -52,7 +53,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
public OpcUaServerHost(OpcUaConfiguration config, IMxAccessClient mxAccessClient, PerformanceMetrics metrics,
HistorianDataSource? historianDataSource = null,
AuthenticationConfiguration? authConfig = null,
IUserAuthenticationProvider? authProvider = null)
IUserAuthenticationProvider? authProvider = null,
SecurityProfileConfiguration? securityConfig = null)
{
_config = config;
_mxAccessClient = mxAccessClient;
@@ -60,6 +62,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
_historianDataSource = historianDataSource;
_authConfig = authConfig ?? new AuthenticationConfiguration();
_authProvider = authProvider;
_securityConfig = securityConfig ?? new SecurityProfileConfiguration();
}
/// <summary>
@@ -69,63 +72,66 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
{
var namespaceUri = $"urn:{_config.GalaxyName}:LmxOpcUa";
// Resolve configured security profiles
var securityPolicies = SecurityProfileResolver.Resolve(_securityConfig.Profiles);
foreach (var sp in securityPolicies)
{
Log.Information("Security profile active: {PolicyUri} / {Mode}", sp.SecurityPolicyUri, sp.SecurityMode);
}
// Build PKI paths
var pkiRoot = _securityConfig.PkiRootPath ?? System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"OPC Foundation", "pki");
var certSubject = _securityConfig.CertificateSubject ?? $"CN={_config.ServerName}, O=ZB MOM, DC=localhost";
var serverConfig = new ServerConfiguration
{
BaseAddresses = { $"opc.tcp://{_config.BindAddress}:{_config.Port}{_config.EndpointPath}" },
MaxSessionCount = _config.MaxSessions,
MaxSessionTimeout = _config.SessionTimeoutMinutes * 60 * 1000, // ms
MinSessionTimeout = 10000,
UserTokenPolicies = BuildUserTokenPolicies()
};
foreach (var policy in securityPolicies)
serverConfig.SecurityPolicies.Add(policy);
var secConfig = new SecurityConfiguration
{
ApplicationCertificate = new CertificateIdentifier
{
StoreType = CertificateStoreType.Directory,
StorePath = System.IO.Path.Combine(pkiRoot, "own"),
SubjectName = certSubject
},
TrustedIssuerCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = System.IO.Path.Combine(pkiRoot, "issuer")
},
TrustedPeerCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = System.IO.Path.Combine(pkiRoot, "trusted")
},
RejectedCertificateStore = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = System.IO.Path.Combine(pkiRoot, "rejected")
},
AutoAcceptUntrustedCertificates = _securityConfig.AutoAcceptClientCertificates,
RejectSHA1SignedCertificates = _securityConfig.RejectSHA1Certificates,
MinimumCertificateKeySize = (ushort)_securityConfig.MinimumCertificateKeySize
};
var appConfig = new ApplicationConfiguration
{
ApplicationName = _config.ServerName,
ApplicationUri = namespaceUri,
ApplicationType = ApplicationType.Server,
ProductUri = namespaceUri,
ServerConfiguration = new ServerConfiguration
{
BaseAddresses = { $"opc.tcp://0.0.0.0:{_config.Port}{_config.EndpointPath}" },
MaxSessionCount = _config.MaxSessions,
MaxSessionTimeout = _config.SessionTimeoutMinutes * 60 * 1000, // ms
MinSessionTimeout = 10000,
SecurityPolicies =
{
new ServerSecurityPolicy
{
SecurityMode = MessageSecurityMode.None,
SecurityPolicyUri = SecurityPolicies.None
}
},
UserTokenPolicies = BuildUserTokenPolicies()
},
SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = new CertificateIdentifier
{
StoreType = CertificateStoreType.Directory,
StorePath = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"OPC Foundation", "pki", "own"),
SubjectName = $"CN={_config.ServerName}, O=ZB MOM, DC=localhost"
},
TrustedIssuerCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"OPC Foundation", "pki", "issuer")
},
TrustedPeerCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"OPC Foundation", "pki", "trusted")
},
RejectedCertificateStore = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"OPC Foundation", "pki", "rejected")
},
AutoAcceptUntrustedCertificates = true
},
ServerConfiguration = serverConfig,
SecurityConfiguration = secConfig,
TransportQuotas = new TransportQuotas
{
@@ -148,6 +154,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
await appConfig.Validate(ApplicationType.Server);
// Hook certificate validation logging
appConfig.CertificateValidator.CertificateValidation += OnCertificateValidation;
_application = new ApplicationInstance
{
ApplicationName = _config.ServerName,
@@ -156,19 +165,34 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
};
// Check/create application certificate
bool certOk = await _application.CheckApplicationInstanceCertificate(false, 2048);
var minKeySize = (ushort)_securityConfig.MinimumCertificateKeySize;
bool certOk = await _application.CheckApplicationInstanceCertificate(false, minKeySize);
if (!certOk)
{
Log.Warning("Application certificate check failed, attempting to create...");
certOk = await _application.CheckApplicationInstanceCertificate(false, 2048);
certOk = await _application.CheckApplicationInstanceCertificate(false, minKeySize);
}
_server = new LmxOpcUaServer(_config.GalaxyName, _mxAccessClient, _metrics, _historianDataSource,
_config.AlarmTrackingEnabled, _authConfig, _authProvider);
await _application.Start(_server);
Log.Information("OPC UA server started on opc.tcp://localhost:{Port}{EndpointPath} (namespace={Namespace})",
_config.Port, _config.EndpointPath, namespaceUri);
Log.Information("OPC UA server started on opc.tcp://{BindAddress}:{Port}{EndpointPath} (namespace={Namespace})",
_config.BindAddress, _config.Port, _config.EndpointPath, namespaceUri);
}
private void OnCertificateValidation(CertificateValidator sender, CertificateValidationEventArgs e)
{
if (_securityConfig.AutoAcceptClientCertificates)
{
e.Accept = true;
Log.Debug("Client certificate auto-accepted: {Subject}", e.Certificate?.Subject);
}
else
{
Log.Warning("Client certificate validation: {Error} for {Subject} — Accepted={Accepted}",
e.Error?.StatusCode, e.Certificate?.Subject, e.Accept);
}
}
/// <summary>