docs: TLS auto-cert and lenient client trust
This commit is contained in:
@@ -107,6 +107,7 @@ public sealed class MxGatewayClientOptions
|
|||||||
public required string ApiKey { get; init; }
|
public required string ApiKey { get; init; }
|
||||||
public bool UseTls { get; init; }
|
public bool UseTls { get; init; }
|
||||||
public string? CaCertificatePath { get; init; }
|
public string? CaCertificatePath { get; init; }
|
||||||
|
public bool RequireCertificateValidation { get; init; }
|
||||||
public string? ServerNameOverride { get; init; }
|
public string? ServerNameOverride { get; init; }
|
||||||
public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(10);
|
public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(10);
|
||||||
public TimeSpan DefaultCallTimeout { get; init; } = TimeSpan.FromSeconds(30);
|
public TimeSpan DefaultCallTimeout { get; init; } = TimeSpan.FromSeconds(30);
|
||||||
@@ -124,6 +125,24 @@ or subscription changes because those calls can partially succeed in MXAccess.
|
|||||||
API key may be loaded from `MXGATEWAY_API_KEY` by the CLI, not implicitly by the
|
API key may be loaded from `MXGATEWAY_API_KEY` by the CLI, not implicitly by the
|
||||||
library constructor unless a helper explicitly says it does that.
|
library constructor unless a helper explicitly says it does that.
|
||||||
|
|
||||||
|
### TLS trust posture
|
||||||
|
|
||||||
|
The gateway can serve a self-signed certificate it generates itself (it has no
|
||||||
|
PKI). To make that usable, TLS is **lenient by default**: when `UseTls` is set
|
||||||
|
and `CaCertificatePath` is empty, `CreateHttpHandler` installs a
|
||||||
|
`RemoteCertificateValidationCallback` that returns `true`, so the gateway's
|
||||||
|
self-signed certificate is accepted without verification.
|
||||||
|
|
||||||
|
To verify the gateway instead:
|
||||||
|
|
||||||
|
- set `CaCertificatePath` to pin a CA — validated via a `CustomRootTrust`
|
||||||
|
`X509Chain` against that root, and the callback additionally rejects a
|
||||||
|
hostname/SAN mismatch (`RemoteCertificateNameMismatch`); or
|
||||||
|
- set `RequireCertificateValidation` to `true` to keep the default OS/system-trust
|
||||||
|
verification on a connection with no pinned CA.
|
||||||
|
|
||||||
|
Pinning a CA always wins over the lenient default.
|
||||||
|
|
||||||
## Auth Interceptor
|
## Auth Interceptor
|
||||||
|
|
||||||
Use a gRPC call credentials/interceptor layer to attach:
|
Use a gRPC call credentials/interceptor layer to attach:
|
||||||
|
|||||||
@@ -287,6 +287,17 @@ Use TLS options for a secured gateway:
|
|||||||
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- smoke --endpoint https://ZB.MOM.WW.MxGateway.example.local:5001 --tls --ca-file C:\certs\mxgateway-ca.pem --server-name ZB.MOM.WW.MxGateway.example.local --api-key-env MXGATEWAY_API_KEY --item Area001.Pump001.Speed --json
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- smoke --endpoint https://ZB.MOM.WW.MxGateway.example.local:5001 --tls --ca-file C:\certs\mxgateway-ca.pem --server-name ZB.MOM.WW.MxGateway.example.local --api-key-env MXGATEWAY_API_KEY --item Area001.Pump001.Speed --json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### TLS trust
|
||||||
|
|
||||||
|
The gateway can auto-generate its own self-signed certificate (it has no PKI), so
|
||||||
|
the client is **lenient by default**: a TLS connection (`UseTls` / `--tls`) with
|
||||||
|
no pinned CA accepts whatever certificate the gateway presents. To verify
|
||||||
|
instead, pin a CA with `CaCertificatePath` / `--ca-file` (this path also enforces
|
||||||
|
the certificate hostname/SAN match), or set `RequireCertificateValidation` to
|
||||||
|
force OS/system-trust verification without pinning. Use `ServerNameOverride` /
|
||||||
|
`--server-name` when the dialed host differs from the certificate SAN. See
|
||||||
|
[Gateway Configuration](../../docs/GatewayConfiguration.md#automatic-self-signed-certificate).
|
||||||
|
|
||||||
## Integration Checks
|
## Integration Checks
|
||||||
|
|
||||||
Run live checks only when a gateway and MXAccess-backed worker are available:
|
Run live checks only when a gateway and MXAccess-backed worker are available:
|
||||||
|
|||||||
@@ -104,6 +104,23 @@ Support:
|
|||||||
- `credentials.NewClientTLSFromFile`,
|
- `credentials.NewClientTLSFromFile`,
|
||||||
- custom `tls.Config` for advanced callers.
|
- custom `tls.Config` for advanced callers.
|
||||||
|
|
||||||
|
### Trust posture
|
||||||
|
|
||||||
|
The gateway can serve a self-signed certificate it generates itself (it has no
|
||||||
|
PKI). To make that usable, TLS is **lenient by default**: when `Plaintext` is
|
||||||
|
`false` and no `CACertFile`/`TLSConfig`/`TransportCredentials` is supplied,
|
||||||
|
`buildCredentials` dials with `tls.Config{InsecureSkipVerify: true}` (carrying
|
||||||
|
`ServerNameOverride` as the SNI when set), so the gateway's self-signed
|
||||||
|
certificate is accepted without verification.
|
||||||
|
|
||||||
|
To verify the gateway instead:
|
||||||
|
|
||||||
|
- set `CACertFile` to pin a CA (full verification against that root), or
|
||||||
|
- set `RequireCertificateValidation: true` to verify against the OS/system trust
|
||||||
|
roots without pinning.
|
||||||
|
|
||||||
|
Pinning a CA always wins over the lenient default.
|
||||||
|
|
||||||
## Streaming
|
## Streaming
|
||||||
|
|
||||||
`Events(ctx)` should return a receive channel of:
|
`Events(ctx)` should return a receive channel of:
|
||||||
|
|||||||
@@ -75,6 +75,14 @@ client, err := mxgateway.Dial(ctx, mxgateway.Options{
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The gateway can auto-generate its own self-signed certificate (it has no PKI), so
|
||||||
|
the client is **lenient by default**: a TLS connection (`Plaintext: false`) with
|
||||||
|
no `CACertFile`/`TLSConfig` accepts whatever certificate the gateway presents
|
||||||
|
(`InsecureSkipVerify`, with `ServerNameOverride` as the SNI when set). To verify
|
||||||
|
instead, set `CACertFile` to pin a CA, or set `RequireCertificateValidation:
|
||||||
|
true` to verify against the OS/system trust roots without pinning. See
|
||||||
|
[Gateway Configuration](../../docs/GatewayConfiguration.md#automatic-self-signed-certificate).
|
||||||
|
|
||||||
`Client.OpenSession` returns a `Session` with helpers for `Register`,
|
`Client.OpenSession` returns a `Session` with helpers for `Register`,
|
||||||
`AddItem`, `AddItem2`, `Advise`, `Write`, `Events`, and `Close`. Prefer
|
`AddItem`, `AddItem2`, `Advise`, `Write`, `Events`, and `Close`. Prefer
|
||||||
`SubscribeEvents` or `SubscribeEventsAfter` for long-running streams because the
|
`SubscribeEvents` or `SubscribeEventsAfter` for long-running streams because the
|
||||||
|
|||||||
@@ -112,6 +112,23 @@ Support:
|
|||||||
- custom CA certificate file,
|
- custom CA certificate file,
|
||||||
- server name override for test environments.
|
- server name override for test environments.
|
||||||
|
|
||||||
|
### Trust posture
|
||||||
|
|
||||||
|
The gateway can serve a self-signed certificate it generates itself (it has no
|
||||||
|
PKI). To make that usable, TLS is **lenient by default**: when the channel is not
|
||||||
|
plaintext and no `caCertificatePath` is set, the client builds
|
||||||
|
`GrpcSslContexts.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE)`
|
||||||
|
(grpc-netty-shaded), so the gateway's self-signed certificate is accepted without
|
||||||
|
verification.
|
||||||
|
|
||||||
|
To verify the gateway instead:
|
||||||
|
|
||||||
|
- set `caCertificatePath` to pin a CA (full verification against that root), or
|
||||||
|
- set `requireCertificateValidation` to `true` to verify against the JVM trust
|
||||||
|
store without pinning.
|
||||||
|
|
||||||
|
Pinning a CA always wins over the lenient default.
|
||||||
|
|
||||||
## Streaming
|
## Streaming
|
||||||
|
|
||||||
Support both:
|
Support both:
|
||||||
|
|||||||
@@ -57,6 +57,16 @@ try (MxGatewayClient client = MxGatewayClient.connect(options);
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The gateway can auto-generate its own self-signed certificate (it has no PKI), so
|
||||||
|
the client is **lenient by default**: a TLS connection (`plaintext(false)`) with
|
||||||
|
no `caCertificatePath` accepts whatever certificate the gateway presents (via
|
||||||
|
grpc-netty-shaded's `InsecureTrustManagerFactory`). To verify instead, set
|
||||||
|
`caCertificatePath` to pin a CA, or set `requireCertificateValidation(true)` to
|
||||||
|
verify against the JVM trust store without pinning. Use `serverNameOverride` /
|
||||||
|
`--server-name-override` when the dialed host differs from the certificate SAN.
|
||||||
|
See
|
||||||
|
[Gateway Configuration](../../docs/GatewayConfiguration.md#automatic-self-signed-certificate).
|
||||||
|
|
||||||
Use `rawBlockingStub`, `rawFutureStub`, `rawAsyncStub`, `openSessionRaw`,
|
Use `rawBlockingStub`, `rawFutureStub`, `rawAsyncStub`, `openSessionRaw`,
|
||||||
`closeSessionRaw`, `invoke`, and raw session helper methods when tests need the
|
`closeSessionRaw`, `invoke`, and raw session helper methods when tests need the
|
||||||
underlying protobuf messages. `MxGatewayCommandException` and
|
underlying protobuf messages. `MxGatewayCommandException` and
|
||||||
|
|||||||
@@ -112,6 +112,28 @@ Support:
|
|||||||
- TLS channel with default roots,
|
- TLS channel with default roots,
|
||||||
- custom root certificate file.
|
- custom root certificate file.
|
||||||
|
|
||||||
|
### Trust posture (trust-on-first-use)
|
||||||
|
|
||||||
|
The gateway can serve a self-signed certificate it generates itself (it has no
|
||||||
|
PKI). grpc-python exposes no per-channel skip-verify hook, so the client cannot
|
||||||
|
"accept any certificate" the way the other clients do. Instead, when the channel
|
||||||
|
is not plaintext and neither `ca_file` nor `require_certificate_validation` is
|
||||||
|
set, the TLS default is **trust-on-first-use**: the client fetches the server's
|
||||||
|
presented certificate once via `ssl.get_server_certificate` (an unverified
|
||||||
|
probe), pins it as the channel's only trust root, and — because the generated
|
||||||
|
certificate always carries a `localhost` SAN — defaults
|
||||||
|
`grpc.ssl_target_name_override` to `localhost` when no `server_name_override` was
|
||||||
|
supplied (tolerating dial-by-IP or a hostname mismatch). A failed probe is
|
||||||
|
surfaced as a transport error naming the endpoint.
|
||||||
|
|
||||||
|
To verify the gateway instead:
|
||||||
|
|
||||||
|
- set `ca_file` to verify against a specific CA, or
|
||||||
|
- set `require_certificate_validation=True` to verify against the system trust
|
||||||
|
roots.
|
||||||
|
|
||||||
|
Both bypass the TOFU path.
|
||||||
|
|
||||||
## Streaming
|
## Streaming
|
||||||
|
|
||||||
Expose `stream_events` as an async iterator. Canceling the task should cancel
|
Expose `stream_events` as an async iterator. Canceling the task should cancel
|
||||||
|
|||||||
@@ -230,6 +230,17 @@ The client supports plaintext channels for local development, TLS with system
|
|||||||
roots, TLS with a custom `ca_file`, and an optional test server name override.
|
roots, TLS with a custom `ca_file`, and an optional test server name override.
|
||||||
API keys are redacted from option repr output and CLI error output.
|
API keys are redacted from option repr output and CLI error output.
|
||||||
|
|
||||||
|
The gateway can auto-generate its own self-signed certificate (it has no PKI).
|
||||||
|
grpc-python has no per-channel skip-verify, so the lenient TLS default is
|
||||||
|
**trust-on-first-use**: with no `ca_file` and `require_certificate_validation`
|
||||||
|
left `False`, the client fetches the gateway's presented certificate once
|
||||||
|
(unverified) and pins it for the channel, defaulting the SNI/target-name override
|
||||||
|
to `localhost` (the generated certificate always carries a `localhost` SAN) when
|
||||||
|
none was supplied. To verify instead, pass `ca_file` to verify against a specific
|
||||||
|
CA, or set `require_certificate_validation=True` to verify against the system
|
||||||
|
trust roots. See
|
||||||
|
[Gateway Configuration](../../docs/GatewayConfiguration.md#automatic-self-signed-certificate).
|
||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
The CLI emits deterministic JSON for automation:
|
The CLI emits deterministic JSON for automation:
|
||||||
|
|||||||
@@ -76,6 +76,19 @@ types.
|
|||||||
cargo run -p mxgw-cli -- smoke --endpoint https://mxgateway.example.local:5001 --tls --ca-file C:\certs\mxgateway-ca.pem --server-name-override mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item TestChildObject.TestInt --json
|
cargo run -p mxgw-cli -- smoke --endpoint https://mxgateway.example.local:5001 --tls --ca-file C:\certs\mxgateway-ca.pem --server-name-override mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item TestChildObject.TestInt --json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### TLS trust (pin-only)
|
||||||
|
|
||||||
|
The gateway can auto-generate its own self-signed certificate (it has no PKI).
|
||||||
|
Unlike the other clients, the Rust client is **not** lenient: tonic 0.13.1
|
||||||
|
exposes no public hook to inject a custom certificate verifier, so TLS over Rust
|
||||||
|
is pin-only. A TLS connection requires either `--ca-file` /
|
||||||
|
`ClientOptions::with_ca_file(...)` to pin a CA (export the gateway's self-signed
|
||||||
|
certificate and pin it), or `--require-certificate-validation` /
|
||||||
|
`with_require_certificate_validation(true)` to verify against the system trust
|
||||||
|
roots. TLS with neither set fails `connect` with a clear, actionable error rather
|
||||||
|
than accepting the certificate. See
|
||||||
|
[Gateway Configuration](../../docs/GatewayConfiguration.md#automatic-self-signed-certificate).
|
||||||
|
|
||||||
## Library Surface
|
## Library Surface
|
||||||
|
|
||||||
`ClientOptions` configures endpoint, API key, plaintext or TLS transport,
|
`ClientOptions` configures endpoint, API key, plaintext or TLS transport,
|
||||||
|
|||||||
@@ -189,6 +189,25 @@ Support:
|
|||||||
- custom CA file,
|
- custom CA file,
|
||||||
- domain override.
|
- domain override.
|
||||||
|
|
||||||
|
### Trust posture (pin-only)
|
||||||
|
|
||||||
|
The gateway can serve a self-signed certificate it generates itself (it has no
|
||||||
|
PKI). Rust is the **exception** to the lenient-by-default posture the other
|
||||||
|
clients use: tonic 0.13.1 exposes no public hook to inject a custom certificate
|
||||||
|
verifier, so the Rust client cannot accept an arbitrary certificate. TLS over the
|
||||||
|
Rust client is therefore **pin-only** — it requires either:
|
||||||
|
|
||||||
|
- `ClientOptions::with_ca_file(...)` to pin a CA (the supported path for the
|
||||||
|
gateway's self-signed certificate; export the certificate and pin it), or
|
||||||
|
- `ClientOptions::with_require_certificate_validation(true)` to verify against the
|
||||||
|
system trust roots.
|
||||||
|
|
||||||
|
With TLS enabled (`with_plaintext(false)`), no pinned CA, and certificate
|
||||||
|
validation not required, `GatewayClient::connect` rejects the connection with a
|
||||||
|
clear, actionable error pointing at `with_ca_file` /
|
||||||
|
`require_certificate_validation` rather than silently accepting the certificate.
|
||||||
|
The CLI exposes `--ca-file` and `--require-certificate-validation`.
|
||||||
|
|
||||||
## Streaming
|
## Streaming
|
||||||
|
|
||||||
Expose event streams as a `Stream<Item = Result<MxEvent, Error>>`. Dropping the
|
Expose event streams as a `Stream<Item = Result<MxEvent, Error>>`. Dropping the
|
||||||
|
|||||||
@@ -51,6 +51,19 @@ The shared inputs are:
|
|||||||
The commands in the matrix use `MXGATEWAY_API_KEY` through each CLI's
|
The commands in the matrix use `MXGATEWAY_API_KEY` through each CLI's
|
||||||
`api-key-env` flag. They must not embed bearer tokens or raw API keys.
|
`api-key-env` flag. They must not embed bearer tokens or raw API keys.
|
||||||
|
|
||||||
|
### TLS variant
|
||||||
|
|
||||||
|
The matrix runs over plaintext (`h2c`) by default. A TLS variant exists but stays
|
||||||
|
a manual/opt-in run, consistent with the gate above, because it needs the gateway
|
||||||
|
started with an HTTPS endpoint (an `https://` `MXGATEWAY_ENDPOINT`) and each CLI
|
||||||
|
switched to its TLS flag (`--tls` / `-tls` / `--plaintext=false` /
|
||||||
|
`plaintext=False`). The clients are lenient by default and accept the gateway's
|
||||||
|
auto-generated self-signed certificate without extra trust setup, except the Rust
|
||||||
|
CLI, which is pin-only and needs `--ca-file` or `--require-certificate-validation`
|
||||||
|
(and Python uses trust-on-first-use). See
|
||||||
|
[Gateway Configuration — Automatic self-signed certificate](./GatewayConfiguration.md#automatic-self-signed-certificate)
|
||||||
|
and each client README for the per-client TLS flags.
|
||||||
|
|
||||||
## JSON Comparison
|
## JSON Comparison
|
||||||
|
|
||||||
Every command in the matrix requests JSON output. A runner can compare the
|
Every command in the matrix requests JSON output. A runner can compare the
|
||||||
|
|||||||
@@ -375,6 +375,42 @@ deployment-heavy box, multiply per-session SQL connections, and complicate the
|
|||||||
cold-start path. Wire-side laziness solves the actual pain (oversized gRPC
|
cold-start path. Wire-side laziness solves the actual pain (oversized gRPC
|
||||||
replies and a heavy DOM) without disturbing the materialization model.
|
replies and a heavy DOM) without disturbing the materialization model.
|
||||||
|
|
||||||
|
## TLS Auto-Certificate and Lenient Client Trust
|
||||||
|
|
||||||
|
Decision: when a Kestrel `https://` endpoint is configured without a certificate
|
||||||
|
of its own (and no `Kestrel:Certificates:Default` is set), the gateway generates
|
||||||
|
and persists a self-signed certificate rather than failing to start. Clients
|
||||||
|
connecting over TLS without a pinned CA accept whatever certificate the server
|
||||||
|
presents by default; pinning a CA restores full verification.
|
||||||
|
|
||||||
|
Rationale: `mxaccessgw` is an internal tool with no PKI to issue or distribute
|
||||||
|
certificates. The prior behavior — an `https` endpoint with no certificate
|
||||||
|
fails at startup with Kestrel's opaque "no server certificate was specified"
|
||||||
|
error — pushed operators toward plaintext (`h2c`), exposing the API key and
|
||||||
|
request payloads on the wire. Auto-generating a long-lived, persisted, reused
|
||||||
|
certificate lets TLS "just work" with zero certificate management, while the
|
||||||
|
lenient client default means clients connect to that self-signed certificate
|
||||||
|
without a manual trust step. Both choices are deliberate, not oversights:
|
||||||
|
strict-by-default would force PKI work this tool does not warrant. Plaintext-only
|
||||||
|
deployments are untouched — no certificate or key material is written for them —
|
||||||
|
and an operator who supplies a real certificate transparently overrides the
|
||||||
|
generated one.
|
||||||
|
|
||||||
|
Two clients diverge from "accept any certificate" because their gRPC stacks lack
|
||||||
|
a per-channel skip-verify hook:
|
||||||
|
|
||||||
|
- Python uses trust-on-first-use: it fetches the server's presented certificate
|
||||||
|
over a separate unverified probe and pins it for the channel, and defaults the
|
||||||
|
SNI/target-name override to `localhost` (the generated certificate always
|
||||||
|
carries a `localhost` SAN).
|
||||||
|
- Rust is pin-only: tonic exposes no public hook to inject a custom certificate
|
||||||
|
verifier, so TLS over Rust requires either a pinned CA or an explicit opt-in to
|
||||||
|
system-trust verification; otherwise connecting returns a clear, actionable
|
||||||
|
error.
|
||||||
|
|
||||||
|
See [Gateway Configuration — Automatic self-signed certificate](./GatewayConfiguration.md#automatic-self-signed-certificate)
|
||||||
|
and the per-client READMEs for the as-built behavior.
|
||||||
|
|
||||||
## Later Revisit Items
|
## Later Revisit Items
|
||||||
|
|
||||||
These are explicit post-v1 revisit items, not open blockers:
|
These are explicit post-v1 revisit items, not open blockers:
|
||||||
|
|||||||
@@ -229,6 +229,185 @@ behavior.
|
|||||||
The alarm monitor is independent of client sessions: `AcknowledgeAlarm` and
|
The alarm monitor is independent of client sessions: `AcknowledgeAlarm` and
|
||||||
`StreamAlarms` are session-less RPCs served by the monitor.
|
`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": "<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` | `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:<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
|
## Related Documentation
|
||||||
|
|
||||||
- [Gateway Process Detailed Design](./GatewayProcessDesign.md)
|
- [Gateway Process Detailed Design](./GatewayProcessDesign.md)
|
||||||
|
|||||||
@@ -243,9 +243,27 @@ services.AddGrpc(options => options.Interceptors.Add<GatewayGrpcAuthorizationInt
|
|||||||
|
|
||||||
Because the interceptor runs before any handler, `MxAccessGatewayService` can safely assume the call has been authorized and that `IGatewayRequestIdentityAccessor.Current` is populated. The handler's only responsibility is to read the identity for `OpenSession` so the session is owned by the authenticated principal; it does not perform any authorization checks of its own. See [Authorization](./Authorization.md) for the policy and identity model.
|
Because the interceptor runs before any handler, `MxAccessGatewayService` can safely assume the call has been authorized and that `IGatewayRequestIdentityAccessor.Current` is populated. The handler's only responsibility is to read the identity for `OpenSession` so the session is owned by the authenticated principal; it does not perform any authorization checks of its own. See [Authorization](./Authorization.md) for the policy and identity model.
|
||||||
|
|
||||||
|
## Transport Security
|
||||||
|
|
||||||
|
The gRPC endpoint runs over HTTP/2, in cleartext (`h2c`) or TLS depending on the
|
||||||
|
Kestrel endpoint configuration. The current deployments serve it in cleartext, so
|
||||||
|
the API key and request payloads cross the network unencrypted. The endpoint,
|
||||||
|
protocol pinning, and TLS certificate configuration — plus the corresponding
|
||||||
|
client `UseTls` / `CaCertificatePath` options — are documented in
|
||||||
|
[Host Endpoints and Transport Security](./GatewayConfiguration.md#host-endpoints-and-transport-security-kestrel).
|
||||||
|
|
||||||
|
To make TLS usable without PKI, the gateway can auto-generate and persist a
|
||||||
|
self-signed certificate when an HTTPS endpoint is configured without one, and the
|
||||||
|
language clients are lenient by default — a TLS connection with no pinned CA
|
||||||
|
accepts the presented certificate (with per-stack nuances: Python is
|
||||||
|
trust-on-first-use, Rust is pin-only). See
|
||||||
|
[Automatic self-signed certificate](./GatewayConfiguration.md#automatic-self-signed-certificate)
|
||||||
|
and each client README for the as-built behavior.
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Contracts](./Contracts.md)
|
- [Contracts](./Contracts.md)
|
||||||
- [Sessions](./Sessions.md)
|
- [Sessions](./Sessions.md)
|
||||||
- [Authorization](./Authorization.md)
|
- [Authorization](./Authorization.md)
|
||||||
|
- [Gateway Configuration](./GatewayConfiguration.md)
|
||||||
- [Gateway Process Design](./GatewayProcessDesign.md)
|
- [Gateway Process Design](./GatewayProcessDesign.md)
|
||||||
|
|||||||
Reference in New Issue
Block a user