Files
lmxopcua/docs/Configuration.md
Joseph Doherty d9463d6998 Remove static Users auth, use shared QualityMapper for historian, simplify LDAP permission checks
- Remove ConfigUserAuthenticationProvider and Users property — LDAP is the only auth mechanism
- Fix historian quality mapping to use existing QualityMapper (OPC DA quality bytes, not custom mapping)
- Add AppRoles constants, unify HasWritePermission/HasAlarmAckPermission into shared HasRole helper
- Hoist write permission check out of per-item loop, eliminate redundant _ldapRolesEnabled field
- Update docs (Configuration.md, Security.md, OpcUaServer.md, HistoricalDataAccess.md)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 19:23:20 -04:00

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:

  1. appsettings.json -- base configuration (required)
  2. appsettings.{DOTNET_ENVIRONMENT}.json -- environment-specific overlay (optional)
  3. 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

LDAP Authentication

When Ldap.Enabled is true, credentials are validated against the configured LDAP server and group membership determines OPC UA permissions.

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 configuration:

"Authentication": {
  "AllowAnonymous": true,
  "AnonymousCanWrite": false,
  "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"
  }
}

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 -- When true, the node manager creates AlarmConditionState nodes for alarm attributes and monitors InAlarm transitions. Disabled by default because alarm tracking adds per-attribute overhead.
  • Historian.Enabled -- When true, the service creates a HistorianDataSource connected 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 -- When true, 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.Port must be between 1 and 65535
  • OpcUa.GalaxyName must not be empty
  • MxAccess.ClientName must not be empty
  • GalaxyRepository.ConnectionString must not be empty
  • Security.MinimumCertificateKeySize must be at least 2048
  • Unknown security profile names are logged as warnings
  • AutoAcceptClientCertificates = true emits a warning
  • Only-None profile configuration emits a warning
  • OpcUa.ApplicationUri must be set when Redundancy.Enabled = true
  • Redundancy.ServiceLevelBase must be between 1 and 255
  • Redundancy.ServerUris should contain at least 2 entries when enabled
  • Local ApplicationUri should appear in Redundancy.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,
    "Ldap": {
      "Enabled": false
    }
  },
  "Security": {
    "Profiles": ["None"],
    "AutoAcceptClientCertificates": true,
    "RejectSHA1Certificates": true,
    "MinimumCertificateKeySize": 2048,
    "PkiRootPath": null,
    "CertificateSubject": null
  },
  "Redundancy": {
    "Enabled": false,
    "Mode": "Warm",
    "Role": "Primary",
    "ServerUris": [],
    "ServiceLevelBase": 200
  }
}