diff --git a/docs/requirements/Component-ManagementService.md b/docs/requirements/Component-ManagementService.md
index 92637b25..9483c525 100644
--- a/docs/requirements/Component-ManagementService.md
+++ b/docs/requirements/Component-ManagementService.md
@@ -210,7 +210,7 @@ The two-person authorization workflow for writes through the MxAccess Gateway. B
- **BrowseNodeCommand**: Browse an OPC UA / MxGateway connection's address space one level at a time; supports a `BrowseNext` continuation token (paging) and returns per-node type-info for OPC UA Variables. Actor-dispatched — the handler requires the command's `SiteIdentifier`, enforces site-scope, then relays via `CommunicationService.BrowseNodeAsync` (arch-review C4, CLI/UI parity). (`Design` role.)
- **SearchAddressSpaceCommand**: Bounded recursive address-space search (depth + result caps, case-insensitive substring) against an OPC UA connection. Actor-dispatched, same `SiteIdentifier`-required + site-scope pattern, relayed via `CommunicationService.SearchAddressSpaceAsync` (arch-review C4). (`Design` role.)
- **VerifyEndpointCommand**: Ask a site to probe an OPC UA endpoint (temporary client, short timeout) and report success / typed failure / a captured-but-untrusted server cert. Read-only — never trusts. Actor-dispatched, `SiteIdentifier`-required + site-scope, relayed via `CommunicationService.VerifyEndpointAsync` (arch-review C4). (`Design` role — runs inside the Admin-gated connection editor.)
-- **TrustServerCertCommand** / **RemoveServerCertCommand** / **ListServerCertsCommand**: Manage the site's OPC UA trusted-peer PKI store; Trust/Remove broadcast to **both** site nodes (see Component-SiteRuntime.md). (`Admin` role.)
+- **TrustServerCertCommand** / **RemoveServerCertCommand** / **ListServerCertsCommand**: Manage the site's OPC UA trusted-peer PKI store; Trust/Remove broadcast to **both** site nodes (see Component-SiteRuntime.md). Actor-dispatched and `Admin`-gated at the actor (arch-review C4) — each handler requires the command's `SiteIdentifier`, enforces site-scope, then relays via `CommunicationService.TrustServerCertAsync` / `RemoveServerCertAsync` / `ListServerCertsAsync`; the site-side reconcile across both nodes is owned by the site `CertStoreActor`. (`Admin` role.)
## Authorization
diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs
index 115754f3..8a5a40b5 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs
@@ -432,7 +432,7 @@ public class CommunicationService
/// The trust-server-cert command (connection name + DER + thumbprint).
/// Cancellation token.
/// The cert-trust result (per-node aggregate success + first error).
- public Task TrustServerCertAsync(
+ public virtual Task TrustServerCertAsync(
string siteId,
TrustServerCertCommand command,
CancellationToken cancellationToken = default)
@@ -453,7 +453,7 @@ public class CommunicationService
/// The list-server-certs command.
/// Cancellation token.
/// The cert-trust result carrying the listed certificates on success.
- public Task ListServerCertsAsync(
+ public virtual Task ListServerCertsAsync(
string siteId,
ListServerCertsCommand command,
CancellationToken cancellationToken = default)
@@ -474,7 +474,7 @@ public class CommunicationService
/// The remove-server-cert command (thumbprint).
/// Cancellation token.
/// The cert-trust result (per-node aggregate success + first error).
- public Task RemoveServerCertAsync(
+ public virtual Task RemoveServerCertAsync(
string siteId,
RemoveServerCertCommand command,
CancellationToken cancellationToken = default)
diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
index 79556338..dd1229cf 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
@@ -212,7 +212,12 @@ public class ManagementActor : ReceiveActor
// gate must match — otherwise a Designer blocked in the UI could still
// rotate a production credential via the CLI/Management API.
or UpdateSmtpConfigCommand
- or UpdateSmsConfigCommand => AdminOnly,
+ or UpdateSmsConfigCommand
+ // Server-certificate trust (arch-review C4). These mutate the site's
+ // trusted-peer PKI store — Admin, matching the Admin-gated UI cert
+ // page. Site-side reconcile semantics are owned by the site
+ // CertStoreActor (plan 03); this handler only relays.
+ or TrustServerCertCommand or RemoveServerCertCommand or ListServerCertsCommand => AdminOnly,
// Area management — any-of [Designer, Deployer] (arch-review C6).
// Exposed both in the Designer tooling and inside the Central UI
@@ -443,6 +448,9 @@ public class ManagementActor : ReceiveActor
BrowseNodeCommand cmd => await HandleBrowseNode(sp, cmd, user),
SearchAddressSpaceCommand cmd => await HandleSearchAddressSpace(sp, cmd, user),
VerifyEndpointCommand cmd => await HandleVerifyEndpoint(sp, cmd, user),
+ TrustServerCertCommand cmd => await HandleTrustServerCert(sp, cmd, user),
+ RemoveServerCertCommand cmd => await HandleRemoveServerCert(sp, cmd, user),
+ ListServerCertsCommand cmd => await HandleListServerCerts(sp, cmd, user),
QueryParkedMessagesCommand cmd => await HandleQueryParkedMessages(sp, cmd, user),
RetryParkedMessageCommand cmd => await HandleRetryParkedMessage(sp, cmd, user),
DiscardParkedMessageCommand cmd => await HandleDiscardParkedMessage(sp, cmd, user),
@@ -2955,6 +2963,39 @@ public class ManagementActor : ReceiveActor
return await commService.VerifyEndpointAsync(site, cmd);
}
+ // Server-certificate trust (arch-review C4). Admin-gated at the role check
+ // above. These mutate the site's trusted-peer PKI store — the handler only
+ // relays; the site-side broadcast/reconcile across both nodes is owned by
+ // the site CertStoreActor (plan 03). SiteIdentifier is REQUIRED so the actor
+ // knows which site to route to.
+
+ private static async Task