10534ec906
TST-27: docs/GatewayConfiguration.md's ShowTagValues row no longer says "Reserved" — it now states what false (default) does (DashboardEventBroadcaster blanks tag values from a deep-cloned MxEvent before the SignalR events-hub mirror), the security relevance (no per-session hub ACL yet, so this redaction is the only thing between a low-trust Viewer and other sessions' tag values), and the honest scope limit (does not cover /browse). WRK-26 (discharges IPC-29): docs/MxAccessWorkerInstanceDesign.md's "Outbound Queues" section rewritten from the stale five-level priority list to the two-class Control/Event scheduler actually shipped, with the collapsed- decision rationale, and the overflow paragraph rewritten to the implemented fail-fast. docs/WorkerFrameProtocol.md gained a "Write Scheduling And Sequencing" section describing HEAD truthfully: WRK-23's peek-stamp-commit sequencing is live, WRK-25's event-batch flush coalescing is not (the drain loop still awaits each event write individually), and WRK-22's cancellation tombstone is not yet defined (noted as pending, not documented as shipped). CLI-42: clients/rust/README.md and docs/ClientPackaging.md document the vendored Rust proto layout matching build.rs — repo-path-first resolution falling back to clients/rust/protos/, the check-codegen.ps1 Check 3 refresh rule, and why cargo package/publish run without --no-verify. CLI-43: docs/style-guides/JavaStyleGuide.md now says Java 17 (Ignition 8.3 baseline), mirroring CLI-12's wording, matching the shipped build.gradle. IPC-28: docs/Grpc.md's exception-mapping prose gained CommandTooLarge -> ResourceExhausted, and the Invoke section gained the oversized-payload sentence, cross-referencing GatewayConfiguration.md's headroom rule. Tracking: TST-27, WRK-26, CLI-42, CLI-43, IPC-28 flipped to Done and IPC-29 marked discharged-by-WRK-26 in 00-tracking.md and the 20/30/50/60 domain registers, with a 2026-08-07 change-log entry. Doc-only change; no source, proto, or test edits.
668 lines
52 KiB
Markdown
668 lines
52 KiB
Markdown
# 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",
|
||
"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": 16842752
|
||
},
|
||
"Sessions": {
|
||
"DefaultCommandTimeoutSeconds": 30,
|
||
"MaxSessions": 64,
|
||
"MaxPendingCommandsPerSession": 128,
|
||
"DefaultLeaseSeconds": 1800,
|
||
"LeaseSweepIntervalSeconds": 30,
|
||
"DetachGraceSeconds": 30,
|
||
"FaultedGraceSeconds": 0,
|
||
"AllowMultipleEventSubscribers": false,
|
||
"MaxEventSubscribersPerSession": 8,
|
||
"WorkerReadyWaitTimeoutMs": 0
|
||
},
|
||
"Events": {
|
||
"QueueCapacity": 10000,
|
||
"BackpressurePolicy": "FailFast",
|
||
"ReplayBufferCapacity": 1024,
|
||
"ReplayRetentionSeconds": 300,
|
||
"MaxSparseArrayLength": 1000000
|
||
},
|
||
"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
|
||
},
|
||
"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` | derived from `CommonApplicationData` (`C:\ProgramData\MxGateway\gateway-auth.db` on Windows, `/usr/share/MxGateway/gateway-auth.db` or the container equivalent elsewhere) | SQLite database path for API-key records and audit rows when API-key authentication is enabled. The code default is built from `Environment.GetFolderPath(SpecialFolder.CommonApplicationData)` so the credential store never lands in the launch working directory on a non-Windows host. `appsettings.json` no longer ships an explicit value (SEC-33): the removed Windows literal was byte-identical to the Windows code default, and a Windows-absolute literal is **not** rooted on a Unix host, so it would have resolved against the CWD there. Deployed hosts still override the path through the NSSM environment (`MxGateway__Authentication__SqlitePath`). |
|
||
| `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 and must be **rooted** (absolute)
|
||
**on the host running the gateway**: the validator uses `Path.IsPathRooted`
|
||
(current OS), so a bare filename — or a foreign-platform literal such as a
|
||
Windows `C:\...` path on a Unix host — fails fast at startup instead of silently
|
||
resolving against the working directory and scattering the credential store by
|
||
launch CWD (SEC-01, SEC-33).
|
||
|
||
## 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` | `16842752` | Maximum worker IPC frame payload size in bytes. The validator allows values from `1024` through `268435456`, and additionally requires this to be at least `MxGateway:Protocol:MaxGrpcMessageBytes` plus a 65536-byte (64 KiB) envelope-overhead reserve. The default is the 16 MiB public gRPC cap plus that reserve. The gateway conveys the negotiated value to the worker in the handshake (`GatewayHello.max_frame_bytes`), so the worker frames to the same limit rather than a hard-coded default. |
|
||
|
||
`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. It
|
||
must also stay above `MaxGrpcMessageBytes` by the envelope-overhead reserve so a
|
||
maximally-sized accepted gRPC payload always fits one worker frame once wrapped
|
||
in a `WorkerEnvelope`; otherwise the oversized outbound write would fault the
|
||
whole session. Startup validation fails fast when the headroom is violated.
|
||
|
||
## 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:DetachGraceSeconds` | `30` | Detach-grace retention window. When positive, a session whose last external (gRPC) event-stream subscriber drops is retained in `Ready` for this many seconds so a client can reconnect; if no external subscriber re-attaches within the window, the lease monitor closes it with `detach-grace-expired`. The internal dashboard mirror does not count as an external subscriber, so a dashboard-only session still enters detach-grace. `0` disables retention and reverts to closing only on normal lease expiry. Must be zero or greater. Reconnect/replay itself is implemented separately (Task 12); this option controls retention and expiry only. The effective close happens within the next sweep cycle after the window elapses — up to `LeaseSweepIntervalSeconds` after expiry. Operators wanting a firm minimum retention bound should set `DetachGraceSeconds` greater than `LeaseSweepIntervalSeconds`. |
|
||
| `MxGateway:Sessions:FaultedGraceSeconds` | `0` | Grace window before the lease monitor reaps a faulted session (killing its worker and freeing the slot). A faulted session is permanently unusable yet otherwise pins a session slot and a live x86 worker until its normal lease expires. `0` (the default) reaps it on the next sweep cycle, bounding blast radius; a positive value keeps the faulted session observable via `GetSessionStatus` for that window before it is reclaimed, with the close reason `faulted-reaped`. Must be zero or greater. |
|
||
| `MxGateway:Sessions:AllowMultipleEventSubscribers` | `false` | Controls whether multiple `StreamEvents` subscribers may attach to one session. When `false` the session refuses a second subscriber with `AlreadyExists`. Set to `true` to enable fan-out via the `SessionEventDistributor`. |
|
||
| `MxGateway:Sessions:MaxEventSubscribersPerSession` | `8` | Maximum number of concurrent `StreamEvents` subscribers per session when `AllowMultipleEventSubscribers` is `true`. Effectively 1 when `AllowMultipleEventSubscribers` is `false`. Must be greater than zero. |
|
||
| `MxGateway:Sessions:WorkerReadyWaitTimeoutMs` | `0` | Bounded time, in milliseconds, the gateway will wait for a worker to reach `Ready` when the session is already `Ready` but the worker state has transiently diverged (e.g. `Handshaking` after a heartbeat blip). Applies only to transient worker states; terminal states (`Faulted`/`Closing`/`Closed`/no worker) fail fast immediately regardless of this setting. `0` (the default) disables the wait and preserves the original fail-fast behavior. Must be greater than or equal to zero. |
|
||
|
||
All numeric session options must be greater than zero.
|
||
|
||
## 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. Gateway-side buffering per session is at most `3 ×` this value: the bounded worker event channel plus the read loop's staging channel, which is bounded at `2 ×` it. Overflow of either bound faults the session with `ProtocolViolation` and kills its worker. Must be between `1` and `int.MaxValue / 2` so the derived `2 ×` staging bound cannot overflow when a worker client is created. |
|
||
| `MxGateway:Events:BackpressurePolicy` | `FailFast` | Per-subscriber event backpressure behavior when a subscriber's bounded event channel overflows. Overflow is isolated to the offending subscriber: it is always disconnected with an `EventQueueOverflow` fault while the session pump and other subscribers keep running. `FailFast` additionally faults the whole session only in the legacy single-subscriber case (the current default mode); with multiple subscribers it degrades to a per-subscriber disconnect so one slow consumer never faults a shared session. `DisconnectSubscriber` disconnects only the slow subscriber in all cases. |
|
||
| `MxGateway:Events:ReplayBufferCapacity` | `1024` | Maximum number of events retained per session in the replay ring buffer, used to re-deliver events a returning subscriber missed (reconnect/reattach). The oldest retained event is evicted once this count is exceeded. `0` disables replay retention. |
|
||
| `MxGateway:Events:ReplayRetentionSeconds` | `300` | Maximum age, in seconds, of an event retained in the replay ring buffer. Entries older than this are evicted regardless of capacity. `0` disables age-based eviction. |
|
||
| `MxGateway:Events:MaxSparseArrayLength` | `1000000` | Maximum `total_length` a sparse-array write (`MxSparseArray`) may declare. A write above this cap is rejected with `InvalidArgument` before the full array is materialized, guarding against a single write forcing a multi-GB allocation. Must be between `1` and `Array.MaxLength`. |
|
||
|
||
`QueueCapacity` must be greater than zero and no greater than `int.MaxValue / 2`
|
||
(the validator rejects a larger value so the derived `2 ×` staging bound cannot
|
||
throw `OverflowException` at session creation); it bounds each per-subscriber event
|
||
channel fed by the session's single event pump, and — at `2 ×` — the worker
|
||
read loop's event staging channel, so a consumer that drains slower than its
|
||
worker produces faults the session at a fixed ceiling instead of growing gateway
|
||
memory (GWC-24). A slow subscriber overflows only its own channel and is always
|
||
disconnected with an `EventQueueOverflow` fault rather than silently dropping
|
||
MXAccess events — the pump, the session, and other subscribers are unaffected. With `FailFast` in the single-subscriber case (the
|
||
default mode), that overflow additionally faults the whole session; with multiple
|
||
subscribers `FailFast` degrades to a per-subscriber disconnect, matching
|
||
`DisconnectSubscriber`, so one slow consumer cannot fault a session shared by
|
||
healthy subscribers. With `DisconnectSubscriber`, overflow terminates only the
|
||
affected stream while the MXAccess session remains active.
|
||
|
||
`ReplayBufferCapacity` and `ReplayRetentionSeconds` must each be greater than or
|
||
equal to zero (either dimension can be disabled with `0`). A returning subscriber
|
||
that asks for events older than the oldest still-retained event is told it missed
|
||
events (a "gap") and must re-snapshot; whatever is still retained is replayed.
|
||
|
||
## 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:CookieName` | _(unset)_ | Dashboard auth cookie name. Leave unset (null/blank) to use the resolved default: `__Host-MxGatewayDashboard` when `RequireHttpsCookie` is `true` (Secure cookie, so the `__Host-` prefix's Secure/no-Domain/`Path=/` guarantees apply), else the plain `MxGatewayDashboard` (a `__Host-` cookie without Secure is silently dropped by browsers). Override it to give a distinct name to a gateway that shares a hostname with another gateway instance: browser cookies are scoped by host+path but **not** by port, so two instances on the same host would otherwise clobber each other's dashboard session under a shared cookie name. An explicit override always wins over the `__Host-` default. Changing it signs out existing dashboard sessions on next deploy. |
|
||
| `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` | Controls whether tag values reach the dashboard's SignalR events hub mirror. `false` (default): `DashboardEventBroadcaster` blanks tag values from a deep-cloned copy of each `MxEvent` before it reaches any hub subscriber — event metadata (tag reference, quality, status, timestamps) still renders; see `docs/GatewayDashboardDesign.md`'s `EventsHub` row for the mechanism. Security-relevant because the per-session hub ACL that would scope a Viewer to specific sessions does not exist yet: with no per-session scoping, this redaction is currently the only thing standing between a low-trust Viewer and other sessions' tag values, so setting this `true` exposes every session's tag values to every authenticated dashboard viewer. The flag gates only the SignalR hub mirror — it does **not** cover the `/browse` live-value display, which remains a separate, still-open residual. |
|
||
| `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. |
|
||
| `MxGateway:Dashboard:DisableLogin` | `false` | Dev/test only. When `true`, replaces the cookie authentication handler with `DashboardAutoLoginAuthenticationHandler`, which auto-authenticates every dashboard request — including requests from remote browsers, not just loopback — as `AutoLoginUser` holding both `Administrator` and `Viewer` roles. No login form, LDAP bind, or cookie is involved. A loud one-time startup warning is logged. Differs from `AllowAnonymousLocalhost`: `DisableLogin` mints a real authenticated principal (so role-gated write affordances appear), whereas `AllowAnonymousLocalhost` satisfies the authorization requirement on loopback only without minting a principal (write affordances stay hidden). Never enable in production. **Production hard-stop (SEC-04):** when the host runs in the `Production` environment and `DisableLogin` is `true`, startup validation fails and the process aborts — the flag is only accepted outside Production, where the one-time startup warning still fires. |
|
||
| `MxGateway:Dashboard:AutoLoginUser` | `(null)` | Username stamped on the synthetic principal when `DisableLogin` is `true`. Default `(null)` — a null or blank value falls back to `multi-role`. Has no effect when `DisableLogin` is `false`. |
|
||
|
||
`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 5-minute data-protected bearer
|
||
token for the calling user; the Blazor pages use it via
|
||
`DashboardHubConnectionFactory` to authenticate the SignalR connection.
|
||
The factory refreshes the token on every (re)connect, so the short lifetime
|
||
(SEC-05) is transparent to clients. The token is not server-side revocable;
|
||
its short lifetime bounds exposure of a captured token (see
|
||
[GatewayDashboardDesign](./GatewayDashboardDesign.md)).
|
||
|
||
## LDAP Options (Dashboard Login)
|
||
|
||
The `MxGateway:Ldap` section configures the LDAP bind used by dashboard `/login`
|
||
(the gRPC API-key model is separate). The shipped defaults are the shared
|
||
dev/test GLAuth posture (`glauth.md`), not a production posture.
|
||
|
||
| Option | Default | Description |
|
||
|--------|---------|-------------|
|
||
| `MxGateway:Ldap:Enabled` | `true` | Enables LDAP-backed dashboard login. |
|
||
| `MxGateway:Ldap:Server` | `localhost` | LDAP server host. Dev points at the shared GLAuth instance (`10.100.0.35`). |
|
||
| `MxGateway:Ldap:Port` | `3893` | LDAP server port. Must be between `1` and `65535`. |
|
||
| `MxGateway:Ldap:Transport` | `None` | Connection transport: `None` (plaintext), `StartTls` (upgrade), or `Ldaps` (implicit TLS). The dev default is `None` because the shared GLAuth instance has LDAPS disabled and binds send cleartext. **Production hard-stop (SEC-06):** when the host runs in the `Production` environment, `Transport` must not be `None` — startup validation fails otherwise. Deployed hosts must set `Ldaps` or `StartTls`. |
|
||
| `MxGateway:Ldap:AllowInsecure` | `true` | Permits a plaintext bind. Must be `true` when `Transport` is `None`; set `false` (with `Ldaps`/`StartTls`) in production. |
|
||
| `MxGateway:Ldap:SearchBase` | `dc=zb,dc=local` | Search base DN. |
|
||
| `MxGateway:Ldap:ServiceAccountDn` | `cn=serviceaccount,dc=zb,dc=local` | Bind DN for the search account. |
|
||
| `MxGateway:Ldap:ServiceAccountPassword` | `${secret:ldap/mxgateway/bind}` | Search-account password. **No longer a committed plaintext value:** `appsettings.json` ships the reference `${secret:ldap/mxgateway/bind}`, which the pre-host `${secret:}` expander resolves at startup from the encrypted secrets store (the code-side design default is now blank, so a missing/unresolved value fails closed rather than falling back to a leaked credential). Seed the value once with `secret set ldap/mxgateway/bind <value>` (the store's master key must be present via `ZB_SECRETS_MASTER_KEY`); startup aborts with `SecretNotFoundException` if the secret is absent. An operator may instead override it directly with the env var `MxGateway__Ldap__ServiceAccountPassword` (double-underscore form) — a plain literal there is used as-is and the secret lookup is skipped. |
|
||
| `MxGateway:Ldap:UserNameAttribute` | `cn` | LDAP attribute holding the login user name. |
|
||
| `MxGateway:Ldap:DisplayNameAttribute` | `cn` | LDAP attribute holding the display name. |
|
||
| `MxGateway:Ldap:GroupAttribute` | `memberOf` | LDAP attribute enumerating group membership (mapped to dashboard roles via `MxGateway:Dashboard:GroupToRole`). |
|
||
|
||
When LDAP is enabled, `Server`, `SearchBase`, `ServiceAccountDn`,
|
||
`ServiceAccountPassword`, and the attribute names must be non-blank, and `Port`
|
||
must be in range. See `glauth.md` for the shared dev instance and the
|
||
dev→production hardening posture.
|
||
|
||
### Production hard-stops key on the exact environment name (SEC-35)
|
||
|
||
Both production hard-stops above — `Dashboard:DisableLogin` and the plaintext
|
||
`Ldap:Transport=None` guard — fire only when `IHostEnvironment.IsProduction()` is
|
||
true, i.e. `ASPNETCORE_ENVIRONMENT` is unset (it defaults to `Production`, which
|
||
covers the NSSM-deployed hosts) or is set to the exact string `Production`. This
|
||
is ASP.NET Core's environment-name convention. A host launched under any other
|
||
name — `Staging`, `Prod`, or a custom label — keeps the permissive dev posture
|
||
and these guards do **not** fire, by design (inverting to "anything but
|
||
Development is production-like" would refuse to boot a legitimate permissive
|
||
staging rig, e.g. one pointed at the plaintext shared GLAuth). A production-like
|
||
deployment must therefore run with the literal `Production` environment name for
|
||
the hard-stops to apply.
|
||
|
||
## Secrets Master Key
|
||
|
||
`${secret:...}` tokens in configuration — currently just
|
||
`MxGateway:Ldap:ServiceAccountPassword` (see above) — are resolved at startup
|
||
from an encrypted local SQLite store, bound from the top-level `Secrets`
|
||
section (a sibling of `MxGateway`, not nested under it):
|
||
|
||
| Option | Default | Description |
|
||
|--------|---------|-------------|
|
||
| `Secrets:SqlitePath` | `mxgateway-secrets.db` | Path to the encrypted secrets store, resolved relative to the app content root when not rooted. |
|
||
| `Secrets:MasterKey:Source` | `Environment` | Key-encryption-key (KEK) provider. `Environment` reads a base64-encoded 32-byte key from an env var; `Dpapi` uses a machine-bound key file instead (see below). |
|
||
| `Secrets:MasterKey:EnvVarName` | `ZB_SECRETS_MASTER_KEY` | Env var name the `Environment` provider reads the KEK from. |
|
||
|
||
The gateway always opens (and, if needed, creates and schema-migrates) this
|
||
store at startup, even when zero `${secret:}` tokens are configured.
|
||
|
||
### Delivering the master key on the NSSM box
|
||
|
||
MxAccessGateway is a single Windows/NSSM box with no clustering, so the
|
||
simplest posture applies: the `Environment` provider delivers
|
||
`ZB_SECRETS_MASTER_KEY` via the NSSM service environment — the same mechanism
|
||
already used for `MxGateway__Ldap__ServiceAccountPassword` overrides and the
|
||
other `AppEnvironmentExtra` entries (see `wonder-app-vd03`/`windev` deploy
|
||
notes). The key is **never committed** to `appsettings.json` or the repo.
|
||
|
||
```text
|
||
nssm get MxAccessGw AppEnvironmentExtra
|
||
nssm set MxAccessGw AppEnvironmentExtra ZB_SECRETS_MASTER_KEY=<base64-32-bytes>
|
||
```
|
||
|
||
`AppEnvironmentExtra` is the same newline-separated `KEY=VALUE` block that
|
||
already carries `ASPNETCORE_ENVIRONMENT`, `MxGateway__Worker__ExecutablePath`,
|
||
and the API-key pepper (see the `wonder-app-vd03`/`windev` deploy notes) —
|
||
`nssm set ... AppEnvironmentExtra` **replaces the entire block**, so always
|
||
`get` first and re-include every existing entry alongside the new key, then
|
||
restart the service for it to take effect.
|
||
|
||
### Seeding a secret
|
||
|
||
Once the master key is in place, seed (or update) a secret on the box with
|
||
the `secret` CLI (`ZB.MOM.WW.Secrets.Cli`). The CLI is config-driven — it
|
||
reads `Secrets:SqlitePath` and the master-key settings from its own
|
||
`appsettings.json` plus environment variables, exactly like the gateway — so
|
||
run it with the same `ZB_SECRETS_MASTER_KEY` set and, if it isn't colocated
|
||
with a config that already points at the gateway's store, an explicit
|
||
`Secrets__SqlitePath` override so both processes read/write the same file:
|
||
|
||
```text
|
||
set ZB_SECRETS_MASTER_KEY=<base64-32-bytes>
|
||
set Secrets__SqlitePath=C:\ProgramData\MxGateway\mxgateway-secrets.db
|
||
secret set ldap/mxgateway/bind <the real LDAP bind password>
|
||
```
|
||
|
||
A mismatched `Secrets:SqlitePath` between the CLI and the gateway silently
|
||
seeds a different store, and the gateway then fails closed at startup as if
|
||
the secret were never set.
|
||
|
||
### Fail-closed
|
||
|
||
If a referenced `${secret:...}` token cannot be resolved — the secret is
|
||
missing from the store, or `ZB_SECRETS_MASTER_KEY` is absent or wrong — the
|
||
gateway refuses to start (`SecretNotFoundException`, or a master-key error
|
||
raised before that). There is no fallback to a blank or leaked credential.
|
||
|
||
### Alternative: DPAPI
|
||
|
||
`Secrets:MasterKey:Source = Dpapi` uses a machine-bound key file instead of an
|
||
env var — stronger at-rest binding (the key never appears in the service
|
||
environment or process listing) at the cost of tying the store to one
|
||
machine, which complicates DR restore (the key file does not travel with a
|
||
backup/restore of the `.db` file alone). Not used on the current NSSM
|
||
deployments; documented here as the alternative posture if the env-var
|
||
exposure becomes a concern.
|
||
|
||
### Rotation / DR
|
||
|
||
Losing the KEK makes the store permanently unrecoverable — there is no
|
||
recovery path other than re-seeding every secret from scratch. Back the KEK
|
||
up in the org secret vault, not just on the box. Full KEK rotation
|
||
(re-wrapping every secret under a new key, `RewrapAll`) is a future item and
|
||
is **not yet available**.
|
||
|
||
## 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`, and requires `MxGateway:Worker:MaxMessageBytes` to exceed this by at least the 64 KiB worker-frame envelope reserve (see the Worker options). |
|
||
|
||
The protocol option is exposed for diagnostics and explicit deployment
|
||
configuration, not for compatibility negotiation. A mismatch fails validation
|
||
at startup.
|
||
|
||
## Security Options
|
||
|
||
Bound from `MxGateway:Security`. These knobs tune the authentication hot path
|
||
(SEC-08 verification cache / last-used coalescing) and the auth-surface rate
|
||
limiting (SEC-11). Defaults are safe; leave them unless profiling or a threat
|
||
model requires otherwise.
|
||
|
||
| Option | Default | Description |
|
||
|--------|---------|-------------|
|
||
| `MxGateway:Security:ApiKeyVerificationCacheSeconds` | `15` | TTL, in seconds, of a cached successful API-key verification. A cache hit within this window skips the per-call SQLite read (and the coupled `last_used` write). Gateway-initiated revoke/rotate/delete invalidate the entry immediately; this TTL is the backstop for out-of-band mutations (a direct DB edit, or a revoke run by the separate `apikey` CLI process). `0` disables the cache. Must be zero or greater. |
|
||
| `MxGateway:Security:ApiKeyLastUsedCoalesceSeconds` | `60` | Coalescing window, in seconds, for the `last_used_utc` write. The library verifier writes `last_used` on every successful verification; this bounds the write to at most one per key per window, so a hammered key does not churn the WAL. `0` forwards every write. Must be zero or greater. |
|
||
| `MxGateway:Security:LoginRateLimitPermitLimit` | `10` | Maximum `POST /auth/login` attempts permitted per remote IP within `LoginRateLimitWindowSeconds` before requests are rejected with HTTP 429. Throttles LDAP credential stuffing before the bind is relayed to the directory. Must be greater than zero. |
|
||
| `MxGateway:Security:LoginRateLimitWindowSeconds` | `60` | Fixed-window length, in seconds, for the per-IP login rate limit. Must be greater than zero. |
|
||
| `MxGateway:Security:ApiKeyFailureLimit` | `10` | Failed API-key verifications, per `(transport peer, key id)` partition, within `ApiKeyFailureWindowSeconds` that trip the in-process short-circuit. Once tripped, the gRPC auth path rejects further attempts from that partition with `ResourceExhausted` **before** the store read — except for the probe admitted every `ApiKeyFailureProbeIntervalSeconds` — and a successful verification resets the partition. The partition always includes the sender's transport address: key ids are public (they ride in every token and are listed on the dashboard), so keying on the key id alone let any peer deny a key to its legitimate holder. Must be greater than zero. |
|
||
| `MxGateway:Security:ApiKeyFailureWindowSeconds` | `60` | Sliding-window length, in seconds, over which API-key verification failures are counted, for both the per-partition and the per-key-id aggregate layer. Must be greater than zero. |
|
||
| `MxGateway:Security:ApiKeyFailureAggregateLimit` | `30` | Failed verifications for one key id counted across **all** transport peers within `ApiKeyFailureWindowSeconds` before that key id enters probe mode. This second layer bounds a distributed or source-rotating sprayer that never trips any single `(peer, key id)` partition. `0` disables the aggregate layer, leaving only per-partition counting. Must be zero or greater. |
|
||
| `MxGateway:Security:ApiKeyFailureProbeIntervalSeconds` | `5` | Minimum interval, in seconds, between probe admissions for an over-limit partition or key-id aggregate. An over-limit state is a valve rather than a wall: one request per interval reaches the real verifier — exactly one, even when a burst arrives together at the interval boundary — so the holder of the correct secret always gets through and clears the state, while everything else is still refused before the store read. `0` blocks absolutely instead — **not recommended**, because an unauthenticated peer can then deny the key to its holder for the whole window. Must be zero or greater. |
|
||
| `MxGateway:Security:ApiKeyFailureTrackedPeers` | `4096` | Maximum distinct partitions tracked by the failure counter (a bounded LRU) so a spray of unique tokens cannot grow memory without limit. It cannot be used to flush an active block either: only a validly shaped `mxgw_<keyId>_<secret>` token mints a key-id partition (everything else lands on the sender's transport-peer partition), each address may mint at most 32 key-id partitions before the overflow collapses onto that address's fallback partition, and eviction prefers fully expired windows, never removing an over-limit partition until the map exceeds twice this cap. Must be greater than zero. |
|
||
|
||
## 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` | derived from `CommonApplicationData` (`C:\ProgramData\MxGateway\galaxy-snapshot.json` on Windows, `/usr/share/MxGateway/galaxy-snapshot.json` or the container equivalent elsewhere) | File path for the persisted Galaxy browse snapshot. Ignored when `PersistSnapshot` is `false`. The snapshot is written atomically (temp file plus rename). `appsettings.json` no longer ships an explicit value (SEC-33): the option is bound by the shared `ZB.MOM.WW.GalaxyRepository` package, so the gateway supplies the `CommonApplicationData`-derived default when the bound value is blank and registers `GalaxyRepositoryOptionsValidator` to enforce that — when `PersistSnapshot` is `true` — the path is non-blank, valid, and **rooted on the host running the gateway** (`Path.IsPathRooted`, current OS). A bare filename or a foreign-platform literal fails startup instead of resolving against the launch working directory (SEC-01, SEC-33). |
|
||
|
||
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 `\\<machine>\Galaxy!<area>` form. The literal `Galaxy` provider is correct regardless of the Galaxy database name. When empty and `Enabled` is `true`, the gateway falls back to `\\<MachineName>\Galaxy!<DefaultArea>` 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.
|
||
|
||
### Alarm fallback options
|
||
|
||
The `Fallback` sub-section controls how the alarm feed selects between the
|
||
native wnwrap alarm-manager provider and the subtag-monitoring fallback.
|
||
|
||
| Option | Default | Description |
|
||
|--------|---------|-------------|
|
||
| `MxGateway:Alarms:Fallback:Mode` | `Auto` | Provider selection mode. `Auto` uses the alarm manager as primary and fails over to subtag monitoring after consecutive COM failures, then fails back automatically. `ForceAlarmManager` disables failover. `ForceSubtag` forces subtag monitoring on from startup. Values are case-insensitive. |
|
||
| `MxGateway:Alarms:Fallback:ConsecutiveFailureThreshold` | `3` | Number of consecutive wnwrap COM failures (`COMException` or failure HRESULT from `Subscribe` / `GetXmlCurrentAlarms2`) before the monitor switches to subtag mode. Floored at 1. |
|
||
| `MxGateway:Alarms:Fallback:FailbackProbeIntervalSeconds` | `30` | While in subtag mode, how often (in seconds) the monitor probes the wnwrap provider to detect recovery. Floored at 1. |
|
||
| `MxGateway:Alarms:Fallback:FailbackStableProbes` | `3` | Number of consecutive clean wnwrap probes required before the monitor switches back to the alarm manager. Floored at 1. |
|
||
| `MxGateway:Alarms:Fallback:Discovery:UseGalaxyRepository` | `true` | When `true`, the monitor queries the Galaxy Repository SQL database to build the subtag watch-list for the configured area. |
|
||
| `MxGateway:Alarms:Fallback:Discovery:Area` | _(empty)_ | Galaxy area to scope the Repository query to. Falls back to `MxGateway:Alarms:DefaultArea` when empty. Ignored when `UseGalaxyRepository` is `false`. This area is **not** used to compose a Repository-derived alarm's canonical `Galaxy!{area}.{reference}`: each discovered alarm uses its object's real Galaxy area (discovered via `gobject.area_gobject_id`), so the reference's group matches what the native alarmmgr emits. `Discovery:Area` / `DefaultArea` is used as the composition area only for explicit `IncludeAttributes` entries, which carry no discovered area. |
|
||
| `MxGateway:Alarms:Fallback:Discovery:IncludeAttributes` | _(empty)_ | Explicit MXAccess attribute paths to add to the subtag watch-list, supplementing (or replacing, when `UseGalaxyRepository` is `false`) the Repository-derived list. |
|
||
| `MxGateway:Alarms:Fallback:Discovery:ExcludeAttributes` | _(empty)_ | Attribute paths to remove from the merged watch-list (case-insensitive). The exclude is applied after the Repository-derived rows and the explicit `IncludeAttributes` entries are combined, so an exclude that matches an explicit include suppresses it too — excludes win. Ignored when `UseGalaxyRepository` is `false`. |
|
||
| `MxGateway:Alarms:Fallback:Subtags:Active` | `InAlarm` | Subtag name for the in-alarm boolean. Confirmed AVEVA `AlarmExtension` field name. |
|
||
| `MxGateway:Alarms:Fallback:Subtags:Acked` | `Acked` | Subtag name for the acknowledged boolean. Confirmed AVEVA `AlarmExtension` field name. |
|
||
| `MxGateway:Alarms:Fallback:Subtags:AckComment` | `AckMsg` | Subtag name for the acknowledgement comment write target. Writing this subtag performs the acknowledge in AVEVA. Confirmed AVEVA `AlarmExtension` field name. When empty, the ack-comment write path is disabled. |
|
||
| `MxGateway:Alarms:Fallback:Subtags:Priority` | `Priority` | Subtag name for the alarm priority / severity value. Confirmed AVEVA `AlarmExtension` field name. |
|
||
|
||
Validation rules:
|
||
|
||
- `Mode` must be `Auto`, `ForceAlarmManager`, or `ForceSubtag` (case-insensitive).
|
||
- `Mode = ForceSubtag` with both `UseGalaxyRepository = false` and an empty
|
||
`IncludeAttributes` list produces a startup validation warning: the subtag
|
||
provider has no attributes to advise.
|
||
- `ConsecutiveFailureThreshold`, `FailbackProbeIntervalSeconds`, and
|
||
`FailbackStableProbes` are floored at 1 by `GatewayOptionsValidator`.
|
||
|
||
Full example with non-default fallback settings:
|
||
|
||
```json
|
||
{
|
||
"MxGateway": {
|
||
"Alarms": {
|
||
"Enabled": true,
|
||
"SubscriptionExpression": "\\\\SCADA01\\Galaxy!PlantArea",
|
||
"DefaultArea": "PlantArea",
|
||
"ReconcileIntervalSeconds": 30,
|
||
"Fallback": {
|
||
"Mode": "Auto",
|
||
"ConsecutiveFailureThreshold": 3,
|
||
"FailbackProbeIntervalSeconds": 30,
|
||
"FailbackStableProbes": 3,
|
||
"Discovery": {
|
||
"UseGalaxyRepository": true,
|
||
"Area": "",
|
||
"IncludeAttributes": [],
|
||
"ExcludeAttributes": []
|
||
},
|
||
"Subtags": {
|
||
"Active": "InAlarm",
|
||
"Acked": "Acked",
|
||
"AckComment": "AckMsg",
|
||
"Priority": "Priority"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
The defaults (`InAlarm`/`Acked`/`AckMsg`/`Priority`) are the confirmed AVEVA
|
||
`AlarmExtension` primitive field names, verified by querying the live ZB Galaxy
|
||
`attribute_definition` rows. The `Subtags` block exists so names can be
|
||
overridden without a code change if a site's alarm template uses different
|
||
attribute names. See `docs/AlarmClientDiscovery.md` for the synthesis rules that
|
||
depend on these names.
|
||
|
||
## 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": "<pfx-password>"
|
||
}
|
||
},
|
||
"Dashboard": {
|
||
"Url": "https://0.0.0.0:5130",
|
||
"Certificate": {
|
||
"Path": "C:\\ProgramData\\MxGateway\\certs\\gateway.pfx",
|
||
"Password": "<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=<pfx-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=<pfx-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` | derived from `CommonApplicationData` (`C:\ProgramData\MxGateway\certs\gateway-selfsigned.pfx` on Windows, `/usr/share/MxGateway/certs/gateway-selfsigned.pfx` or the container equivalent elsewhere) | Where the generated certificate is persisted. The code default is built from `Environment.GetFolderPath(SpecialFolder.CommonApplicationData)` so a generated private key never lands in the launch working directory on a non-Windows host (SEC-01). Must be **rooted** (absolute); the validator rejects a non-rooted override. |
|
||
| `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:<name>: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)
|