# Gateway Configuration This document describes every option bound under the `MxGateway` configuration section by `GatewayOptions`. The gateway binds configuration at startup and validates it with `GatewayOptionsValidator`. Startup fails before the server listens when required paths, timeouts, queue sizes, enum values, or protocol values are invalid. ## Configuration Shape ```json { "MxGateway": { "Authentication": { "Mode": "ApiKey", "SqlitePath": "C:\\ProgramData\\MxGateway\\gateway-auth.db", "PepperSecretName": "MxGateway:ApiKeyPepper", "RunMigrationsOnStartup": true }, "Worker": { "ExecutablePath": "src\\ZB.MOM.WW.MxGateway.Worker\\bin\\x86\\Release\\ZB.MOM.WW.MxGateway.Worker.exe", "WorkingDirectory": null, "RequiredArchitecture": "X86", "StartupTimeoutSeconds": 30, "StartupProbeRetryAttempts": 3, "StartupProbeRetryDelayMilliseconds": 250, "PipeConnectAttemptTimeoutMilliseconds": 2000, "ShutdownTimeoutSeconds": 10, "HeartbeatIntervalSeconds": 5, "HeartbeatGraceSeconds": 15, "MaxMessageBytes": 16777216 }, "Sessions": { "DefaultCommandTimeoutSeconds": 30, "MaxSessions": 64, "MaxPendingCommandsPerSession": 128, "DefaultLeaseSeconds": 1800, "LeaseSweepIntervalSeconds": 30, "AllowMultipleEventSubscribers": false }, "Events": { "QueueCapacity": 10000, "BackpressurePolicy": "FailFast" }, "Dashboard": { "Enabled": true, "AllowAnonymousLocalhost": true, "RequireHttpsCookie": true, "SnapshotIntervalMilliseconds": 1000, "RecentFaultLimit": 100, "RecentSessionLimit": 200, "ShowTagValues": false, "GroupToRole": { "GwAdmin": "Admin", "GwReader": "Viewer" } }, "Protocol": { "WorkerProtocolVersion": 1, "MaxGrpcMessageBytes": 16777216 }, "Galaxy": { "ConnectionString": "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;", "CommandTimeoutSeconds": 60, "DashboardRefreshIntervalSeconds": 30, "PersistSnapshot": true, "SnapshotCachePath": "C:\\ProgramData\\MxGateway\\galaxy-snapshot.json" }, "Alarms": { "Enabled": false, "SubscriptionExpression": "", "DefaultArea": "", "ReconcileIntervalSeconds": 30 } } } ``` Environment variables use the normal .NET double-underscore form. For example, `MxGateway__Sessions__MaxSessions=20` overrides `MxGateway:Sessions:MaxSessions`. ## Authentication Options | Option | Default | Description | |--------|---------|-------------| | `MxGateway:Authentication:Mode` | `ApiKey` | Selects public gRPC authentication. Supported values are `ApiKey` and `Disabled`. `Disabled` bypasses API-key verification and is for local development only. | | `MxGateway:Authentication:SqlitePath` | `C:\ProgramData\MxGateway\gateway-auth.db` | SQLite database path for API-key records and audit rows when API-key authentication is enabled. | | `MxGateway:Authentication:PepperSecretName` | `MxGateway:ApiKeyPepper` | Configuration key used to read the HMAC pepper for API-key secret hashing. The dashboard effective configuration redacts this value. | | `MxGateway:Authentication:RunMigrationsOnStartup` | `true` | Runs SQLite auth schema migrations at gateway startup when API-key authentication is enabled. | When `Mode` is `ApiKey`, `SqlitePath` and `PepperSecretName` must be present. `SqlitePath` must be a valid filesystem path. ## Worker Options | Option | Default | Description | |--------|---------|-------------| | `MxGateway:Worker:ExecutablePath` | `src\ZB.MOM.WW.MxGateway.Worker\bin\x86\Release\ZB.MOM.WW.MxGateway.Worker.exe` | Path to the x86 worker executable launched for each gateway session. The path must be valid and point to a `.exe` file. | | `MxGateway:Worker:WorkingDirectory` | `null` | Optional working directory for the worker process. When set, it must be a valid filesystem path. | | `MxGateway:Worker:RequiredArchitecture` | `X86` | Required Portable Executable architecture for the worker. Supported values are `X86` and `X64`; MXAccess parity uses `X86`. | | `MxGateway:Worker:StartupTimeoutSeconds` | `30` | Total startup budget for process launch, startup probe, pipe connect, handshake, and worker readiness. | | `MxGateway:Worker:StartupProbeRetryAttempts` | `3` | Number of retry attempts for transient worker startup probe failures before pipe connection and handshake continue. | | `MxGateway:Worker:StartupProbeRetryDelayMilliseconds` | `250` | Delay between transient startup probe retry attempts. | | `MxGateway:Worker:PipeConnectAttemptTimeoutMilliseconds` | `2000` | Per-attempt timeout used by the worker named-pipe connect retry path. The overall pipe connection still stays under the startup budget. | | `MxGateway:Worker:ShutdownTimeoutSeconds` | `10` | Grace period for worker shutdown before the gateway treats shutdown as failed and may kill the worker process tree. | | `MxGateway:Worker:HeartbeatIntervalSeconds` | `5` | Worker heartbeat send interval and gateway heartbeat check cadence input. | | `MxGateway:Worker:HeartbeatGraceSeconds` | `15` | Maximum age of the last worker heartbeat before the gateway faults the worker. This must be greater than or equal to `HeartbeatIntervalSeconds`. | | `MxGateway:Worker:MaxMessageBytes` | `16777216` | Maximum worker IPC frame payload size in bytes. The validator allows values from `1024` through `268435456`. | `StartupProbeRetryAttempts`, `StartupProbeRetryDelayMilliseconds`, `PipeConnectAttemptTimeoutMilliseconds`, timeout values, heartbeat values, and `MaxMessageBytes` must be positive. `MaxMessageBytes` is intentionally bounded to avoid accidental large allocations from malformed or oversized frames. ## Session Options | Option | Default | Description | |--------|---------|-------------| | `MxGateway:Sessions:DefaultCommandTimeoutSeconds` | `30` | Default timeout used while the gateway waits for a worker command reply when an open-session request does not provide a positive command timeout. | | `MxGateway:Sessions:MaxSessions` | `64` | Maximum number of concurrently open gateway sessions. Session opens reserve a slot atomically before worker creation. | | `MxGateway:Sessions:MaxPendingCommandsPerSession` | `128` | Maximum number of pending worker commands for one session. Excess commands fail fast instead of queueing indefinitely. | | `MxGateway:Sessions:DefaultLeaseSeconds` | `1800` | Initial session lease and refresh duration. Unary client activity extends the lease by this duration. | | `MxGateway:Sessions:LeaseSweepIntervalSeconds` | `30` | Hosted monitor interval for closing expired leases. Active event-stream subscribers keep a session from expiring while the stream remains attached. | | `MxGateway:Sessions:AllowMultipleEventSubscribers` | `false` | Controls whether multiple `StreamEvents` subscribers may attach to one session. `true` is rejected until event fan-out is implemented. | All numeric session options must be greater than zero. The current event stream implementation supports one active subscriber per session; this preserves event ordering and avoids competing consumers. ## Event Options | Option | Default | Description | |--------|---------|-------------| | `MxGateway:Events:QueueCapacity` | `10000` | Capacity for bounded per-session event queues used by the gateway worker event channel and the public gRPC event stream queue. | | `MxGateway:Events:BackpressurePolicy` | `FailFast` | Event backpressure behavior. `FailFast` faults the session on public stream queue overflow. `DisconnectSubscriber` disconnects only the slow stream. | `QueueCapacity` must be greater than zero. With `FailFast`, queue overflow faults the affected worker or session instead of silently dropping MXAccess events. With `DisconnectSubscriber`, public gRPC stream overflow terminates only the affected stream while the MXAccess session remains active. ## Dashboard Options | Option | Default | Description | |--------|---------|-------------| | `MxGateway:Dashboard:Enabled` | `true` | Enables Blazor Server dashboard route mapping. The dashboard mounts at the host root (`/`); there is no separate path-base prefix. | | `MxGateway:Dashboard:AllowAnonymousLocalhost` | `true` | Allows loopback dashboard requests to bypass the dashboard cookie requirement for local development. Remote requests still require dashboard authentication. | | `MxGateway:Dashboard:RequireHttpsCookie` | `true` | Sets the dashboard auth cookie's secure policy. `true` keeps `CookieSecurePolicy.Always` — the cookie is only sent over HTTPS, which matches a production HTTPS deployment. Set to `false` for plain-HTTP dev deployments to use `CookieSecurePolicy.SameAsRequest`; the cookie is still flagged Secure on HTTPS requests, but it can round-trip over HTTP. Browsers drop Secure cookies set over HTTP from non-localhost hosts, so leaving this `true` while serving the dashboard over plain HTTP will break login from any remote browser. | | `MxGateway:Dashboard:SnapshotIntervalMilliseconds` | `1000` | Dashboard snapshot refresh interval used by the snapshot SignalR hub and the pages that subscribe to it. | | `MxGateway:Dashboard:RecentFaultLimit` | `100` | Maximum number of fault summaries projected into each dashboard snapshot. | | `MxGateway:Dashboard:RecentSessionLimit` | `200` | Maximum number of session summaries projected into each dashboard snapshot. | | `MxGateway:Dashboard:ShowTagValues` | `false` | Reserved display control for tag values. The dashboard does not show full tag values by default. | | `MxGateway:Dashboard:GroupToRole` | _(empty)_ | LDAP group → dashboard role mapping. Keys are LDAP group names (short CN or full DN — leading-RDN match). Values must be `Admin` (read/write, API-key CRUD) or `Viewer` (read-only). A user whose LDAP groups don't intersect this map cannot sign in; with no mapping at all, only the loopback bypass admits anyone. | `SnapshotIntervalMilliseconds` must be greater than zero. `RecentFaultLimit` and `RecentSessionLimit` must be greater than or equal to zero. `GroupToRole` values are validated at startup; invalid role names fail validation. Emptiness is allowed (a closed deployment that admits no LDAP users) but practical deployments populate at least one Admin group. ### Authorization policies Three authorization policies are registered out of these options: - `MxGateway.Dashboard.Viewer` — gates the Razor component routes. Satisfied by either dashboard role (Admin or Viewer), by `AllowAnonymousLocalhost` on loopback, or by `Authentication.Mode = Disabled`. - `MxGateway.Dashboard.Admin` — gates write-capable surfaces (API-key CRUD). Satisfied only by the Admin role (same environmental bypasses). - `MxGateway.Dashboard.HubClients` — attached to the SignalR hubs. Accepts either the dashboard cookie scheme or the `MxGateway.Dashboard.HubToken` bearer scheme (used by SignalR's WebSocket upgrade path where the HttpOnly cookie can't be forwarded). ### SignalR hubs When the dashboard is enabled, three hubs are mapped under `/hubs/*`: - `GET /hubs/snapshot` — pushes `DashboardSnapshot` whenever the snapshot service produces a new one. Drives every page that inherits `DashboardPageBase`; replaces the earlier polling loop. - `GET /hubs/alarms` — re-broadcasts the `AlarmFeedMessage` stream from the central alarm monitor to all connected clients (group `__alarms__`). - `GET /hubs/events` — per-session MxEvent feed. Clients call `SubscribeSession(sessionId)` to join `session:{id}`. Events are mirrored from the corresponding gRPC `StreamEvents` call as a fire-and-forget side-effect; the dashboard only sees events while a gRPC client is also subscribed to that session. `GET /hubs/token` (cookie-only) mints a 30-minute data-protected bearer token for the calling user; the Blazor pages use it via `DashboardHubConnectionFactory` to authenticate the SignalR connection. ## Protocol Options | Option | Default | Description | |--------|---------|-------------| | `MxGateway:Protocol:WorkerProtocolVersion` | `1` | Worker IPC protocol version expected by the gateway and worker. This must match `GatewayContractInfo.WorkerProtocolVersion`. | | `MxGateway:Protocol:MaxGrpcMessageBytes` | `16777216` | Public gRPC max send and receive message size in bytes. The same default is used by official clients. The validator allows values from `1024` through `268435456`. | The protocol option is exposed for diagnostics and explicit deployment configuration, not for compatibility negotiation. A mismatch fails validation at startup. ## Galaxy Options | Option | Default | Description | |--------|---------|-------------| | `MxGateway:Galaxy:ConnectionString` | `Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;` | SQL Server connection string for the Galaxy Repository (`ZB`) used by the `GalaxyRepository` browse RPCs. Override in production via `MxGateway__Galaxy__ConnectionString`. | | `MxGateway:Galaxy:CommandTimeoutSeconds` | `60` | Per-command SQL timeout for all Galaxy browse RPCs. | | `MxGateway:Galaxy:DashboardRefreshIntervalSeconds` | `30` | Interval between background refreshes of the dashboard Galaxy summary cache. SQL is hit at most once per interval regardless of dashboard render rate. | | `MxGateway:Galaxy:PersistSnapshot` | `true` | Persists the latest successful Galaxy browse dataset to disk. When `true`, the cache reloads that snapshot at startup so clients can still browse last-known data while the Galaxy database is unreachable. The restored data is served with `Stale` status until a live query confirms it. | | `MxGateway:Galaxy:SnapshotCachePath` | `C:\ProgramData\MxGateway\galaxy-snapshot.json` | File path for the persisted Galaxy browse snapshot. Ignored when `PersistSnapshot` is `false`. The snapshot is written atomically (temp file plus rename). | See [Galaxy Repository Browse](./GalaxyRepository.md) for the RPC surface and behavior. ## Alarm Options | Option | Default | Description | |--------|---------|-------------| | `MxGateway:Alarms:Enabled` | `false` | Gates the gateway's always-on central alarm monitor. When `true`, the gateway opens one gateway-owned worker session dedicated to alarms, caches the active-alarm set, and fans it out to every client through the `StreamAlarms` RPC — no client opens its own session to see alarms. | | `MxGateway:Alarms:SubscriptionExpression` | _(empty)_ | AVEVA alarm-subscription expression the monitor subscribes on startup, in canonical `\\\Galaxy!` form. The literal `Galaxy` provider is correct regardless of the Galaxy database name. When empty and `Enabled` is `true`, the gateway falls back to `\\\Galaxy!` if `DefaultArea` is set. | | `MxGateway:Alarms:DefaultArea` | _(empty)_ | Area name used to compose a default subscription when `SubscriptionExpression` is empty. If both are empty while `Enabled` is `true`, the monitor faults with a configuration diagnostic. | | `MxGateway:Alarms:ReconcileIntervalSeconds` | `30` | How often the monitor reconciles its in-process alarm cache against the worker's authoritative active-alarm snapshot, catching transitions the live poll-and-diff feed missed. Floored at 5 seconds. | The alarm monitor is independent of client sessions: `AcknowledgeAlarm` and `StreamAlarms` are session-less RPCs served by the monitor. ## Host Endpoints and Transport Security (Kestrel) The listening endpoints are **not** part of the `MxGateway` section. The gateway uses the stock ASP.NET Core host (`WebApplication.CreateBuilder`) with no `ConfigureKestrel` call in code, so endpoints come entirely from the standard `Kestrel` configuration section. On the deployed hosts these values are supplied as NSSM environment variables (`Kestrel__Endpoints__...`), not from `appsettings.json`. Two named endpoints are bound: | Endpoint name | Purpose | Protocol requirement | |---|---|---| | `Http` | Public gRPC API (sessions, invoke, events, Galaxy browse) | HTTP/2 | | `Dashboard` | Blazor dashboard and SignalR hubs | HTTP/1.1 (HTTP/2 optional) | Both endpoints share one routing pipeline; the names only select which TCP port serves which traffic. The gRPC endpoint must negotiate **HTTP/2**, which drives the protocol settings below. ### Plaintext (current deployments) Both running hosts (`10.100.0.48` and `wonder-app-vd03`) serve the gRPC port in **cleartext HTTP/2 (`h2c`)**. Because cleartext HTTP/2 has no ALPN to negotiate the protocol, the gRPC endpoint must be pinned to `Http2` with prior knowledge: ```text Kestrel__Endpoints__Http__Url=http://0.0.0.0:5120 Kestrel__Endpoints__Http__Protocols=Http2 Kestrel__Endpoints__Dashboard__Url=http://0.0.0.0:5130 ``` In this mode all client↔gateway traffic — including the `authorization: Bearer mxgw_...` API key and any `WriteSecured` / `AuthenticateUser` payloads — crosses the network **unencrypted**. This is acceptable only on a trusted/isolated network segment. Prefer TLS for anything else. ### TLS To encrypt the gRPC channel, give the `Http` endpoint an `https://` URL and a certificate. Over TLS, ALPN negotiates HTTP/2, so the explicit `Protocols=Http2` pin is no longer required (the default `Http1AndHttp2` works for gRPC over TLS). `appsettings.json` form: ```json { "Kestrel": { "Endpoints": { "Http": { "Url": "https://0.0.0.0:5120", "Certificate": { "Path": "C:\\ProgramData\\MxGateway\\certs\\gateway.pfx", "Password": "" } }, "Dashboard": { "Url": "https://0.0.0.0:5130", "Certificate": { "Path": "C:\\ProgramData\\MxGateway\\certs\\gateway.pfx", "Password": "" } } } } } ``` Equivalent NSSM environment-variable form (how config is delivered on the hosts — see [server deploy mechanics in the project notes]): ```text Kestrel__Endpoints__Http__Url=https://0.0.0.0:5120 Kestrel__Endpoints__Http__Certificate__Path=C:\ProgramData\MxGateway\certs\gateway.pfx Kestrel__Endpoints__Http__Certificate__Password= Kestrel__Endpoints__Dashboard__Url=https://0.0.0.0:5130 Kestrel__Endpoints__Dashboard__Certificate__Path=C:\ProgramData\MxGateway\certs\gateway.pfx Kestrel__Endpoints__Dashboard__Certificate__Password= ``` Certificate sourcing options (any standard ASP.NET Core form is accepted): | Form | Keys | |---|---| | PFX file | `Certificate:Path` (+ `Certificate:Password` if encrypted) | | PEM pair | `Certificate:Path` (cert) + `Certificate:KeyPath` (private key) | | Windows cert store | `Certificate:Subject`, `Certificate:Store` (e.g. `My`), `Certificate:Location` (`LocalMachine`), `Certificate:AllowInvalid` | The certificate's CN/SAN must cover the host name clients dial (or clients must set a server-name override — see below). The dashboard endpoint can keep its own certificate independent of the gRPC endpoint; pair this with `MxGateway:Dashboard:RequireHttpsCookie` (`true`) for production HTTPS. ### Automatic self-signed certificate `mxaccessgw` is an internal tool with no PKI to issue certificates, so requiring an operator to supply one before TLS works pushed deployments toward plaintext. To avoid that, the gateway fills in a self-signed certificate when an HTTPS endpoint is configured without one. **Trigger.** At startup the gateway inspects `Kestrel:Endpoints:*`. If any endpoint has an `https://` URL and no `Certificate` subsection of its own, and no `Kestrel:Certificates:Default` is set, the gateway generates (or loads) a persisted self-signed certificate and wires it in as the HTTPS *default* via `ConfigureHttpsDefaults`. All-plaintext deployments are untouched: when no HTTPS endpoint is configured, no certificate or key material is generated or written. **Generated certificate.** ECDSA P-256, `serverAuth` EKU, validity ≈ `ValidityYears` (default 10 years, with one day of clock-skew slack before `notBefore`). SANs cover `localhost`, the machine name (and its FQDN when resolvable), each entry in `AdditionalDnsNames`, and the loopback addresses `127.0.0.1` and `::1`. **`MxGateway:Tls:*` options.** All optional; the zero-config path needs none of them. | Option | Default | Purpose | |---|---|---| | `Tls:SelfSignedCertPath` | `C:\ProgramData\MxGateway\certs\gateway-selfsigned.pfx` | Where the generated certificate is persisted | | `Tls:ValidityYears` | `10` | Lifetime of the generated certificate (validated 1–100) | | `Tls:AdditionalDnsNames` | `[]` | Extra DNS SANs (e.g. a load-balancer name) | | `Tls:RegenerateIfExpired` | `true` | Replace an expired persisted certificate instead of failing | `ValidityYears` is validated by `GatewayOptionsValidator` (range 1–100); the "HTTPS endpoint configured but no certificate available" fail-fast lives in the bootstrap/provider, because the validator only sees the `MxGateway` section, not `Kestrel:Endpoints`. **Persistence.** The PFX is written with an **empty** export password — a random in-memory password could not be reused across restarts, which the persist-and-reuse model requires. The private key is instead protected at rest by filesystem permissions: a restrictive ACL on Windows (SYSTEM + Administrators, inherited ACEs stripped) on the `certs` directory and file, and mode `0600` on non-Windows. The write is atomic (hardened temp file, then move). The persisted certificate is reused across restarts (stable thumbprint, so CA-pinning clients keep working) and regenerated only when it is missing, expired (and `RegenerateIfExpired` is `true`), or unreadable/corrupt. If the directory is not writable or the ACL cannot be applied, the gateway fails fast with a diagnostic naming the path rather than falling back to an in-memory certificate. **Logging.** On generate or load, the gateway logs the certificate thumbprint, SAN list, and `notAfter` at Information. The PFX bytes, export password, and private key are never logged. **Operator override.** The generated certificate is only the HTTPS *default*. To use a real certificate, configure one explicitly — either per endpoint via `Kestrel:Endpoints::Certificate` (`Path`/`Subject`/`Thumbprint`, etc., as in the table above) or globally via `Kestrel:Certificates:Default`. An explicitly-configured certificate takes precedence, and the gateway then writes no self-signed material. ### Client side Each official client opts into TLS explicitly. For the .NET client (`MxGatewayClientOptions`): | Option | Effect | |---|---| | `UseTls` (default `false`) | Enables TLS. Requires an `https://` endpoint; an `https://` endpoint without `UseTls` fails validation, and vice versa. | | `CaCertificatePath` | Pins a custom root (self-signed / private CA) using `CustomRootTrust` chain validation instead of the OS trust store; the .NET client also enforces the certificate hostname/SAN match on this path. | | `RequireCertificateValidation` (default `false`) | Forces OS/system-trust verification on a TLS connection with no pinned CA. Leave `false` for the lenient default. | | `ServerNameOverride` | SNI / certificate host name override when the dialed host differs from the certificate CN/SAN. | To pair with the auto-generated self-signed certificate above, the clients are **lenient by default**: a TLS connection with no pinned CA accepts whatever certificate the gateway presents. Pin `CaCertificatePath` to verify, or set `RequireCertificateValidation` to force system-trust verification without pinning. The other language clients expose the equivalent options; the exact behavior differs per stack — Python uses trust-on-first-use and Rust is pin-only. See each client README for the as-built behavior. ### Gateway↔worker IPC Transport security here applies only to the public gRPC channel. The gateway↔worker link is a per-session **named pipe** (`mxaccess-gateway-{gatewayPid}-{sessionId}`), not a network socket. It is not TLS-encrypted and does not need to be: it never leaves the local Windows host and is secured by the OS pipe ACL. See [Worker Frame Protocol](./WorkerFrameProtocol.md). ## Related Documentation - [Gateway Process Detailed Design](./GatewayProcessDesign.md) - [Gateway Dashboard Detailed Design](./GatewayDashboardDesign.md) - [Worker Process Launcher](./WorkerProcessLauncher.md) - [Worker Frame Protocol](./WorkerFrameProtocol.md) - [Galaxy Repository Browse](./GalaxyRepository.md)