Replace static user list with GLAuth LDAP authentication. Group membership (ReadOnly, ReadWrite, AlarmAck) maps to granular OPC UA permissions for write and alarm-ack operations. Anonymous can still browse and read but not write. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
15 KiB
Configuration
Overview
The service loads configuration from appsettings.json at startup using the Microsoft.Extensions.Configuration stack. AppConfiguration is the root holder class that aggregates typed sections: OpcUa, MxAccess, GalaxyRepository, Dashboard, Historian, Authentication, and Security. Each section binds to a dedicated POCO class with sensible defaults, so the service runs with zero configuration on a standard deployment.
Config Binding Pattern
The production constructor in OpcUaService builds the configuration pipeline and binds each JSON section to its typed class:
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", optional: true)
.AddEnvironmentVariables()
.Build();
_config = new AppConfiguration();
configuration.GetSection("OpcUa").Bind(_config.OpcUa);
configuration.GetSection("MxAccess").Bind(_config.MxAccess);
configuration.GetSection("GalaxyRepository").Bind(_config.GalaxyRepository);
configuration.GetSection("Dashboard").Bind(_config.Dashboard);
configuration.GetSection("Historian").Bind(_config.Historian);
configuration.GetSection("Authentication").Bind(_config.Authentication);
configuration.GetSection("Security").Bind(_config.Security);
This pattern uses IConfiguration.GetSection().Bind() rather than IOptions<T> because the service targets .NET Framework 4.8, where the full dependency injection container is not used.
Environment-Specific Overrides
The configuration pipeline supports three layers of override, applied in order:
appsettings.json-- base configuration (required)appsettings.{DOTNET_ENVIRONMENT}.json-- environment-specific overlay (optional)- Environment variables -- highest priority, useful for deployment automation
Set the DOTNET_ENVIRONMENT variable to load a named overlay file. For example, setting DOTNET_ENVIRONMENT=Staging loads appsettings.Staging.json if it exists.
Environment variables follow the standard Section__Property naming convention. For example, OpcUa__Port=5840 overrides the OPC UA port.
Configuration Sections
OpcUa
Controls the OPC UA server endpoint and session limits. Defined in OpcUaConfiguration.
| Property | Type | Default | Description |
|---|---|---|---|
BindAddress |
string |
"0.0.0.0" |
IP address or hostname the server binds to. Use 0.0.0.0 for all interfaces, localhost for local-only, or a specific IP |
Port |
int |
4840 |
TCP port the OPC UA server listens on |
EndpointPath |
string |
"/LmxOpcUa" |
Path appended to the host URI |
ServerName |
string |
"LmxOpcUa" |
Server name presented to OPC UA clients |
GalaxyName |
string |
"ZB" |
Galaxy name used as the OPC UA namespace |
MaxSessions |
int |
100 |
Maximum simultaneous OPC UA sessions |
SessionTimeoutMinutes |
int |
30 |
Idle session timeout in minutes |
AlarmTrackingEnabled |
bool |
false |
Enables AlarmConditionState nodes for alarm attributes |
ApplicationUri |
string? |
null |
Explicit application URI for this server instance. Required when redundancy is enabled. Defaults to urn:{GalaxyName}:LmxOpcUa when null |
MxAccess
Controls the MXAccess runtime connection used for live tag reads and writes. Defined in MxAccessConfiguration.
| Property | Type | Default | Description |
|---|---|---|---|
ClientName |
string |
"LmxOpcUa" |
Client name registered with MXAccess |
NodeName |
string? |
null |
Optional Galaxy node name to target |
GalaxyName |
string? |
null |
Optional Galaxy name for MXAccess reference resolution |
ReadTimeoutSeconds |
int |
5 |
Maximum wait for a live tag read |
WriteTimeoutSeconds |
int |
5 |
Maximum wait for a write acknowledgment |
MaxConcurrentOperations |
int |
10 |
Cap on concurrent MXAccess operations |
MonitorIntervalSeconds |
int |
5 |
Connectivity monitor probe interval |
AutoReconnect |
bool |
true |
Automatically re-establish dropped MXAccess sessions |
ProbeTag |
string? |
null |
Optional tag used to verify the runtime returns fresh data |
ProbeStaleThresholdSeconds |
int |
60 |
Seconds a probe value may remain unchanged before the connection is considered stale |
GalaxyRepository
Controls the Galaxy repository database connection used to build the OPC UA address space. Defined in GalaxyRepositoryConfiguration.
| Property | Type | Default | Description |
|---|---|---|---|
ConnectionString |
string |
"Server=localhost;Database=ZB;Integrated Security=true;" |
SQL Server connection string for the Galaxy database |
ChangeDetectionIntervalSeconds |
int |
30 |
How often the service polls for Galaxy deploy changes |
CommandTimeoutSeconds |
int |
30 |
SQL command timeout for repository queries |
ExtendedAttributes |
bool |
false |
Load extended Galaxy attribute metadata into the OPC UA model |
Dashboard
Controls the embedded HTTP status dashboard. Defined in DashboardConfiguration.
| Property | Type | Default | Description |
|---|---|---|---|
Enabled |
bool |
true |
Whether the status dashboard is hosted |
Port |
int |
8081 |
HTTP port for the dashboard endpoint |
RefreshIntervalSeconds |
int |
10 |
HTML auto-refresh interval in seconds |
Historian
Controls the Wonderware Historian connection for OPC UA historical data access. Defined in HistorianConfiguration.
| Property | Type | Default | Description |
|---|---|---|---|
Enabled |
bool |
false |
Enables OPC UA historical data access |
ConnectionString |
string |
"Server=localhost;Database=Runtime;Integrated Security=true;" |
Connection string for the Historian Runtime database |
CommandTimeoutSeconds |
int |
30 |
SQL command timeout for historian queries |
MaxValuesPerRead |
int |
10000 |
Maximum values returned per HistoryRead request |
Authentication
Controls user authentication and write authorization for the OPC UA server. Defined in AuthenticationConfiguration.
| Property | Type | Default | Description |
|---|---|---|---|
AllowAnonymous |
bool |
true |
Accepts anonymous client connections when true |
AnonymousCanWrite |
bool |
true |
Permits anonymous users to write when true |
Users |
List<UserCredential> |
[] |
List of username/password credentials for UserName token authentication |
Each entry in the Users list has two properties: Username (string) and Password (string). The Users list is ignored when Ldap.Enabled is true.
LDAP Authentication
When Ldap.Enabled is true, credentials are validated against the configured LDAP server and group membership determines OPC UA permissions. The Users list is ignored.
| Property | Type | Default | Description |
|---|---|---|---|
Ldap.Enabled |
bool |
false |
Enables LDAP authentication |
Ldap.Host |
string |
localhost |
LDAP server hostname |
Ldap.Port |
int |
3893 |
LDAP server port |
Ldap.BaseDN |
string |
dc=lmxopcua,dc=local |
Base DN for LDAP operations |
Ldap.BindDnTemplate |
string |
cn={username},dc=lmxopcua,dc=local |
Bind DN template ({username} is replaced) |
Ldap.ServiceAccountDn |
string |
"" |
Service account DN for group lookups |
Ldap.ServiceAccountPassword |
string |
"" |
Service account password |
Ldap.TimeoutSeconds |
int |
5 |
Connection timeout |
Ldap.ReadOnlyGroup |
string |
ReadOnly |
LDAP group granting read-only access |
Ldap.ReadWriteGroup |
string |
ReadWrite |
LDAP group granting read-write access |
Ldap.AlarmAckGroup |
string |
AlarmAck |
LDAP group granting alarm acknowledgment |
Permission Model
When LDAP is enabled, authenticated users receive permissions based on their LDAP group membership:
| LDAP Group | Permission |
|---|---|
| ReadOnly | Browse and read nodes |
| ReadWrite | Browse, read, and write tag values |
| AlarmAck | Acknowledge alarms |
Users can belong to multiple groups. The admin user in the default GLAuth configuration belongs to all three groups.
Example with LDAP authentication:
"Authentication": {
"AllowAnonymous": true,
"AnonymousCanWrite": false,
"Users": [],
"Ldap": {
"Enabled": true,
"Host": "localhost",
"Port": 3893,
"BaseDN": "dc=lmxopcua,dc=local",
"BindDnTemplate": "cn={username},dc=lmxopcua,dc=local",
"ServiceAccountDn": "cn=serviceaccount,dc=lmxopcua,dc=local",
"ServiceAccountPassword": "serviceaccount123",
"TimeoutSeconds": 5,
"ReadOnlyGroup": "ReadOnly",
"ReadWriteGroup": "ReadWrite",
"AlarmAckGroup": "AlarmAck"
}
}
Example with static user list (no LDAP):
"Authentication": {
"AllowAnonymous": true,
"AnonymousCanWrite": false,
"Users": [
{ "Username": "operator", "Password": "op123" },
{ "Username": "engineer", "Password": "eng456" }
]
}
Security
Controls OPC UA transport security profiles and certificate handling. Defined in SecurityProfileConfiguration. See Security Guide for detailed usage.
| Property | Type | Default | Description |
|---|---|---|---|
Profiles |
List<string> |
["None"] |
Security profiles to expose. Valid: None, Basic256Sha256-Sign, Basic256Sha256-SignAndEncrypt |
AutoAcceptClientCertificates |
bool |
true |
Auto-accept untrusted client certificates. Set to false in production |
RejectSHA1Certificates |
bool |
true |
Reject client certificates signed with SHA-1 |
MinimumCertificateKeySize |
int |
2048 |
Minimum RSA key size for client certificates |
PkiRootPath |
string? |
null |
Override for PKI root directory. Defaults to %LOCALAPPDATA%\OPC Foundation\pki |
CertificateSubject |
string? |
null |
Override for server certificate subject. Defaults to CN={ServerName}, O=ZB MOM, DC=localhost |
Example — production deployment with encrypted transport:
"Security": {
"Profiles": ["Basic256Sha256-SignAndEncrypt"],
"AutoAcceptClientCertificates": false,
"RejectSHA1Certificates": true,
"MinimumCertificateKeySize": 2048
}
Redundancy
Controls non-transparent OPC UA redundancy. Defined in RedundancyConfiguration. See Redundancy Guide for detailed usage.
| Property | Type | Default | Description |
|---|---|---|---|
Enabled |
bool |
false |
Enables redundancy mode and ServiceLevel computation |
Mode |
string |
"Warm" |
Redundancy mode: Warm or Hot |
Role |
string |
"Primary" |
Instance role: Primary (higher ServiceLevel) or Secondary |
ServerUris |
List<string> |
[] |
ApplicationUri values for all servers in the redundant set |
ServiceLevelBase |
int |
200 |
Base ServiceLevel when healthy (1-255). Secondary receives base - 50 |
Example — two-instance redundant pair (Primary):
"Redundancy": {
"Enabled": true,
"Mode": "Warm",
"Role": "Primary",
"ServerUris": ["urn:localhost:LmxOpcUa:instance1", "urn:localhost:LmxOpcUa:instance2"],
"ServiceLevelBase": 200
}
Feature Flags
Three boolean properties act as feature flags that control optional subsystems:
OpcUa.AlarmTrackingEnabled-- Whentrue, the node manager createsAlarmConditionStatenodes for alarm attributes and monitorsInAlarmtransitions. Disabled by default because alarm tracking adds per-attribute overhead.Historian.Enabled-- Whentrue, the service creates aHistorianDataSourceconnected to the Wonderware Historian Runtime database and registers it with the OPC UA server host. Disabled by default because not all deployments have a Historian instance.GalaxyRepository.ExtendedAttributes-- Whentrue, the repository loads additional Galaxy attribute metadata beyond the core set needed for the address space. Disabled by default to minimize startup query time.
Configuration Validation
ConfigurationValidator.ValidateAndLog() runs at the start of OpcUaService.Start(). It logs every resolved configuration value at Information level and validates required constraints:
OpcUa.Portmust be between 1 and 65535OpcUa.GalaxyNamemust not be emptyMxAccess.ClientNamemust not be emptyGalaxyRepository.ConnectionStringmust not be emptySecurity.MinimumCertificateKeySizemust be at least 2048- Unknown security profile names are logged as warnings
AutoAcceptClientCertificates = trueemits a warning- Only-
Noneprofile configuration emits a warning OpcUa.ApplicationUrimust be set whenRedundancy.Enabled = trueRedundancy.ServiceLevelBasemust be between 1 and 255Redundancy.ServerUrisshould contain at least 2 entries when enabled- Local
ApplicationUrishould appear inRedundancy.ServerUris
If validation fails, the service throws InvalidOperationException and does not start.
Test Constructor Pattern
OpcUaService provides an internal constructor that accepts pre-built dependencies instead of loading appsettings.json:
internal OpcUaService(
AppConfiguration config,
IMxProxy? mxProxy,
IGalaxyRepository? galaxyRepository,
IMxAccessClient? mxAccessClientOverride = null,
bool hasMxAccessClientOverride = false)
Integration tests use this constructor to inject substitute implementations of IMxProxy, IGalaxyRepository, and IMxAccessClient, bypassing the STA thread, COM interop, and SQL Server dependencies. The hasMxAccessClientOverride flag tells the service to use the injected IMxAccessClient directly instead of creating one from the IMxProxy on the STA thread.
Example appsettings.json
{
"OpcUa": {
"BindAddress": "0.0.0.0",
"Port": 4840,
"EndpointPath": "/LmxOpcUa",
"ServerName": "LmxOpcUa",
"GalaxyName": "ZB",
"MaxSessions": 100,
"SessionTimeoutMinutes": 30,
"AlarmTrackingEnabled": false,
"ApplicationUri": null
},
"MxAccess": {
"ClientName": "LmxOpcUa",
"NodeName": null,
"GalaxyName": null,
"ReadTimeoutSeconds": 5,
"WriteTimeoutSeconds": 5,
"MaxConcurrentOperations": 10,
"MonitorIntervalSeconds": 5,
"AutoReconnect": true,
"ProbeTag": null,
"ProbeStaleThresholdSeconds": 60
},
"GalaxyRepository": {
"ConnectionString": "Server=localhost;Database=ZB;Integrated Security=true;",
"ChangeDetectionIntervalSeconds": 30,
"CommandTimeoutSeconds": 30,
"ExtendedAttributes": false
},
"Dashboard": {
"Enabled": true,
"Port": 8081,
"RefreshIntervalSeconds": 10
},
"Historian": {
"Enabled": false,
"ConnectionString": "Server=localhost;Database=Runtime;Integrated Security=true;",
"CommandTimeoutSeconds": 30,
"MaxValuesPerRead": 10000
},
"Authentication": {
"AllowAnonymous": true,
"AnonymousCanWrite": true,
"Users": []
},
"Security": {
"Profiles": ["None"],
"AutoAcceptClientCertificates": true,
"RejectSHA1Certificates": true,
"MinimumCertificateKeySize": 2048,
"PkiRootPath": null,
"CertificateSubject": null
},
"Redundancy": {
"Enabled": false,
"Mode": "Warm",
"Role": "Primary",
"ServerUris": [],
"ServiceLevelBase": 200
}
}