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

```csharp
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 |
| `AlarmFilter.ObjectFilters` | `List<string>` | `[]` | Wildcard template-name patterns (with `*`) that scope alarm tracking to matching objects and their descendants. Empty list disables filtering. See [Alarm Tracking](AlarmTracking.md#template-based-alarm-object-filter) |
| `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 |
| `RuntimeStatusProbesEnabled` | `bool` | `true` | Advises `<Host>.ScanState` on every deployed `$WinPlatform` and `$AppEngine` to track per-host runtime state. Drives the Galaxy Runtime dashboard panel, HealthCheck Rule 2e, and the Read-path short-circuit that invalidates OPC UA variable quality when a host is Stopped. Set `false` to return to legacy behavior where host state is invisible and the bridge serves whatever quality MxAccess reports for individual tags. See [MXAccess Bridge](MxAccessBridge.md#per-host-runtime-status-probes-hostscanstate) |
| `RuntimeStatusUnknownTimeoutSeconds` | `int` | `15` | Maximum seconds to wait for the initial probe callback before marking a host as Stopped. Only applies to the Unknown → Stopped transition; Running hosts never time out because `ScanState` is delivered on-change only. A value below 5s triggers a validator warning |
| `RequestTimeoutSeconds` | `int` | `30` | Outer safety timeout applied to sync-over-async MxAccess operations invoked from the OPC UA stack thread (Read, Write, address-space rebuild probe sync). Backstop for the inner `ReadTimeoutSeconds` / `WriteTimeoutSeconds`. A timed-out operation returns `BadTimeout`. Validator rejects values < 1 and warns if set below the inner Read/Write timeouts. See [MXAccess Bridge](MxAccessBridge.md#request-timeout-safety-backstop). Stability review 2026-04-13 Finding 3 |

### 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 |
| `Scope` | `GalaxyScope` | `"Galaxy"` | Controls how much of the Galaxy hierarchy is loaded. `Galaxy` loads all deployed objects (default). `LocalPlatform` loads only objects hosted by the platform deployed on this machine. See [Galaxy Repository — Platform Scope Filter](GalaxyRepository.md#platform-scope-filter) |
| `PlatformName` | `string?` | `null` | Explicit platform hostname for `LocalPlatform` filtering. When null, uses `Environment.MachineName`. Only used when `Scope` is `LocalPlatform` |

### 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 SDK connection for OPC UA historical data access. Defined in `HistorianConfiguration`.

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `Enabled` | `bool` | `false` | Enables OPC UA historical data access |
| `ServerName` | `string` | `"localhost"` | Single Historian server hostname used when `ServerNames` is empty. Preserved for backward compatibility with pre-cluster deployments |
| `ServerNames` | `List<string>` | `[]` | Ordered list of Historian cluster nodes. When non-empty, supersedes `ServerName` and enables read-only cluster failover. See [Historical Data Access](HistoricalDataAccess.md#read-only-cluster-failover) |
| `FailureCooldownSeconds` | `int` | `60` | How long a failed cluster node is skipped before being re-tried. Zero disables the cooldown |
| `IntegratedSecurity` | `bool` | `true` | Use Windows authentication |
| `UserName` | `string?` | `null` | Username when `IntegratedSecurity` is false |
| `Password` | `string?` | `null` | Password when `IntegratedSecurity` is false |
| `Port` | `int` | `32568` | Historian TCP port |
| `CommandTimeoutSeconds` | `int` | `30` | SDK packet timeout in seconds (inner async bound) |
| `RequestTimeoutSeconds` | `int` | `60` | Outer safety timeout applied to sync-over-async Historian operations invoked from the OPC UA stack thread (`HistoryReadRaw`, `HistoryReadProcessed`, `HistoryReadAtTime`, `HistoryReadEvents`). Backstop for `CommandTimeoutSeconds`; a timed-out read returns `BadTimeout`. Validator rejects values < 1 and warns if set below `CommandTimeoutSeconds`. Stability review 2026-04-13 Finding 3 |
| `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.WriteOperateGroup` | `string` | `WriteOperate` | LDAP group granting write access for FreeAccess/Operate attributes |
| `Ldap.WriteTuneGroup` | `string` | `WriteTune` | LDAP group granting write access for Tune attributes |
| `Ldap.WriteConfigureGroup` | `string` | `WriteConfigure` | LDAP group granting write access for Configure attributes |
| `Ldap.AlarmAckGroup` | `string` | `AlarmAck` | LDAP group granting alarm acknowledgment |

#### Permission Model

When LDAP is enabled, LDAP group membership is mapped to OPC UA session role NodeIds during authentication. All authenticated LDAP users can browse and read nodes regardless of group membership. Groups grant additional permissions:

| LDAP Group | Permission |
|---|---|
| ReadOnly | No additional permissions (read-only access) |
| WriteOperate | Write FreeAccess and Operate attributes |
| WriteTune | Write Tune attributes |
| WriteConfigure | Write Configure attributes |
| AlarmAck | Acknowledge alarms |

Users can belong to multiple groups. The `admin` user in the default GLAuth configuration belongs to all three groups.

Write access depends on both the user's role and the Galaxy attribute's security classification. See the [Effective Permission Matrix](Security.md#effective-permission-matrix) in the Security Guide for the full breakdown.

Example configuration:

```json
"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",
    "WriteOperateGroup": "WriteOperate",
    "WriteTuneGroup": "WriteTune",
    "WriteConfigureGroup": "WriteConfigure",
    "AlarmAckGroup": "AlarmAck"
  }
}
```

### Security

Controls OPC UA transport security profiles and certificate handling. Defined in `SecurityProfileConfiguration`. See [Security Guide](security.md) for detailed usage.

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `Profiles` | `List<string>` | `["None"]` | Security profiles to expose. Valid: `None`, `Basic256Sha256-Sign`, `Basic256Sha256-SignAndEncrypt`, `Aes128_Sha256_RsaOaep-Sign`, `Aes128_Sha256_RsaOaep-SignAndEncrypt`, `Aes256_Sha256_RsaPss-Sign`, `Aes256_Sha256_RsaPss-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:

```json
"Security": {
  "Profiles": ["Basic256Sha256-SignAndEncrypt"],
  "AutoAcceptClientCertificates": false,
  "RejectSHA1Certificates": true,
  "MinimumCertificateKeySize": 2048
}
```

### Redundancy

Controls non-transparent OPC UA redundancy. Defined in `RedundancyConfiguration`. See [Redundancy Guide](Redundancy.md) 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):

```json
"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.
- **`OpcUa.AlarmFilter.ObjectFilters`** -- List of wildcard template-name patterns that scope alarm tracking to matching objects and their descendants. An empty list preserves the current unfiltered behavior; a non-empty list includes an object only when any name in its template derivation chain matches any pattern, then propagates the inclusion to every descendant in the containment hierarchy. `*` is the only wildcard, matching is case-insensitive, and the Galaxy `$` prefix on template names is normalized so operators can write `TestMachine*` instead of `$TestMachine*`. Each list entry may itself contain comma-separated patterns (`"TestMachine*, Pump_*"`) for convenience. When the list is non-empty but `AlarmTrackingEnabled` is `false`, the validator emits a warning because the filter has no effect. See [Alarm Tracking](AlarmTracking.md#template-based-alarm-object-filter) for the full matching algorithm and telemetry.
- **`Historian.Enabled`** -- When `true`, the service calls `HistorianPluginLoader.TryLoad(config)` to load the `ZB.MOM.WW.OtOpcUa.Historian.Aveva` plugin from the `Historian/` subfolder next to the host exe and registers the resulting `IHistorianDataSource` with the OPC UA server host. Disabled by default because not all deployments have a Historian instance -- when disabled the plugin is not probed and the Wonderware SDK DLLs are not required on the host. If the flag is `true` but the plugin or its SDK dependencies cannot be loaded, the server still starts and every history read returns `BadHistoryOperationUnsupported` with a warning in the log.
- **`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.
- **`GalaxyRepository.Scope`** -- When set to `LocalPlatform`, the repository filters the hierarchy and attributes to only include objects hosted by the platform whose `node_name` matches this machine (or the explicit `PlatformName` override). Ancestor areas are retained to keep the browse tree connected. Default is `Galaxy` (load everything). See [Galaxy Repository — Platform Scope Filter](GalaxyRepository.md#platform-scope-filter).

## 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.AlarmFilter.ObjectFilters` is non-empty while `OpcUa.AlarmTrackingEnabled = false` emits a warning (filter has no effect)
- `Historian.ServerName` (or `Historian.ServerNames`) must not be empty when `Historian.Enabled = true`
- `Historian.FailureCooldownSeconds` must be zero or positive
- `Historian.ServerName` is set alongside a non-empty `Historian.ServerNames` emits a warning (single ServerName is ignored)
- `MxAccess.RuntimeStatusUnknownTimeoutSeconds` below 5s emits a warning (below the reasonable floor for MxAccess initial-resolution latency)
- `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`:

```csharp
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

```json
{
  "OpcUa": {
    "BindAddress": "0.0.0.0",
    "Port": 4840,
    "EndpointPath": "/LmxOpcUa",
    "ServerName": "LmxOpcUa",
    "GalaxyName": "ZB",
    "MaxSessions": 100,
    "SessionTimeoutMinutes": 30,
    "AlarmTrackingEnabled": false,
    "AlarmFilter": {
      "ObjectFilters": []
    },
    "ApplicationUri": null
  },
  "MxAccess": {
    "ClientName": "LmxOpcUa",
    "NodeName": null,
    "GalaxyName": null,
    "ReadTimeoutSeconds": 5,
    "WriteTimeoutSeconds": 5,
    "MaxConcurrentOperations": 10,
    "MonitorIntervalSeconds": 5,
    "AutoReconnect": true,
    "ProbeTag": null,
    "ProbeStaleThresholdSeconds": 60,
    "RuntimeStatusProbesEnabled": true,
    "RuntimeStatusUnknownTimeoutSeconds": 15,
    "RequestTimeoutSeconds": 30
  },
  "GalaxyRepository": {
    "ConnectionString": "Server=localhost;Database=ZB;Integrated Security=true;",
    "ChangeDetectionIntervalSeconds": 30,
    "CommandTimeoutSeconds": 30,
    "ExtendedAttributes": false,
    "Scope": "Galaxy",
    "PlatformName": null
  },
  "Dashboard": {
    "Enabled": true,
    "Port": 8081,
    "RefreshIntervalSeconds": 10
  },
  "Historian": {
    "Enabled": false,
    "ServerName": "localhost",
    "ServerNames": [],
    "FailureCooldownSeconds": 60,
    "IntegratedSecurity": true,
    "UserName": null,
    "Password": null,
    "Port": 32568,
    "CommandTimeoutSeconds": 30,
    "RequestTimeoutSeconds": 60,
    "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
  }
}
```
