From 777fef9cadd1bb6e94d5639d79f49102ea8d02b2 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 6 Nov 2026 15:52:46 -0500 Subject: [PATCH] fix(sessions): drop the deny-NETWORK pipe ACE (#187) NT AUTHORITY\NETWORK marks a network logon, not a remote caller. On windev a gateway run from an SSH session carries it in its own token, so the deny entry locked the gateway's same-machine workers out (UnauthorizedAccess on connect; 15 fake-worker tests failed). The ACL now holds exactly one entry: FullControl for the gateway's user. --- docs/GatewayConfiguration.md | 4 ++-- .../Sessions/WorkerPipeSecurity.cs | 17 +++++++------- .../Sessions/WorkerPipeSecurityTests.cs | 22 +++++++++---------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/docs/GatewayConfiguration.md b/docs/GatewayConfiguration.md index 618a2f2..378a734 100644 --- a/docs/GatewayConfiguration.md +++ b/docs/GatewayConfiguration.md @@ -743,8 +743,8 @@ gateway↔worker link is a per-session **named pipe** (`mxgw-{gatewayPid}-{sessionUid}`), 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: on Windows the gateway creates each pipe with a -protected DACL that grants only its own account (the account the worker is -launched as) and denies network logons; on the non-Windows test matrix it uses +protected DACL whose only entry grants its own account (the account the worker +is launched as); on the non-Windows test matrix it uses `PipeOptions.CurrentUserOnly`. See `WorkerPipeSecurity` and [Worker Frame Protocol](./WorkerFrameProtocol.md). ## Related Documentation diff --git a/src/ZB.MOM.WW.MxGateway.Server/Sessions/WorkerPipeSecurity.cs b/src/ZB.MOM.WW.MxGateway.Server/Sessions/WorkerPipeSecurity.cs index f095ab0..b99bf29 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Sessions/WorkerPipeSecurity.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Sessions/WorkerPipeSecurity.cs @@ -10,8 +10,13 @@ namespace ZB.MOM.WW.MxGateway.Server.Sessions; /// credential-bearing commands (secured writes, AuthenticateUser, alarm-ack passwords), so /// its unguessable name is not enough: only the account the gateway runs as — which is also the /// account the worker is launched as, because the launcher sets no alternate credentials — may -/// open it, and never over the network. +/// open it. /// +/// +/// There is deliberately no deny entry for NT AUTHORITY\NETWORK. That SID marks a network +/// logon, not a remote caller: a gateway started from an SSH or WinRM session carries it in +/// its own token, so a deny entry locked its own same-machine workers out (found on windev, #187). +/// internal static class WorkerPipeSecurity { /// Gets the options every worker pipe is created with on non-Windows hosts. @@ -34,8 +39,8 @@ internal static class WorkerPipeSecurity } /// - /// Creates a pipe ACL that grants full control to only, denies network - /// logons, and does not inherit any other entries. + /// Creates a pipe ACL that grants full control to only and does not + /// inherit any other entries. /// /// The account the gateway and its workers run as. /// The pipe security descriptor. @@ -51,12 +56,6 @@ internal static class WorkerPipeSecurity security.SetOwner(user); security.AddAccessRule(new PipeAccessRule(user, PipeAccessRights.FullControl, AccessControlType.Allow)); - // A network logon of the same account still carries the user SID; the deny ACE keeps the - // pipe local-only regardless. - security.AddAccessRule(new PipeAccessRule( - new SecurityIdentifier(WellKnownSidType.NetworkSid, domainSid: null), - PipeAccessRights.FullControl, - AccessControlType.Deny)); return security; } diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/WorkerPipeSecurityTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/WorkerPipeSecurityTests.cs index 58b32a2..ba6ace2 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/WorkerPipeSecurityTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/WorkerPipeSecurityTests.cs @@ -13,10 +13,10 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Sessions; public sealed class WorkerPipeSecurityTests { - /// Verifies the factory grants only the given user and denies network logons. + /// Verifies the factory grants only the given user. [WindowsFact] [SupportedOSPlatform("windows")] - public void Create_GrantsOnlyTheUserAndDeniesNetwork() + public void Create_GrantsOnlyTheUser() { SecurityIdentifier user = new("S-1-5-21-1000-2000-3000-1001"); @@ -24,7 +24,7 @@ public sealed class WorkerPipeSecurityTests Assert.True(security.AreAccessRulesProtected); Assert.Equal(user, security.GetOwner(typeof(SecurityIdentifier))); - AssertOnlyUserAllowedAndNetworkDenied(security, user); + AssertOnlyUserAllowed(security, user); } /// @@ -41,12 +41,14 @@ public sealed class WorkerPipeSecurityTests using NamedPipeServerStream pipe = SessionWorkerClientFactory.CreatePipe(NewPipeName()); PipeSecurity applied = pipe.GetAccessControl(); - AssertOnlyUserAllowedAndNetworkDenied(applied, user); + AssertOnlyUserAllowed(applied, user); } /// /// Verifies the pipe still accepts a client running as the same user — the worker is launched /// under the gateway's own account, so the restriction must not break the session handshake. + /// On windev this runs from an SSH (network-logon) session, which is what caught the earlier + /// deny-NETWORK entry. /// [Fact] public async Task CreatePipe_AcceptsClientRunningAsTheSameUser() @@ -77,24 +79,22 @@ public sealed class WorkerPipeSecurityTests } [SupportedOSPlatform("windows")] - private static void AssertOnlyUserAllowedAndNetworkDenied(PipeSecurity security, SecurityIdentifier user) + private static void AssertOnlyUserAllowed(PipeSecurity security, SecurityIdentifier user) { SecurityIdentifier everyoneSid = new(WellKnownSidType.WorldSid, null); SecurityIdentifier anonymousSid = new(WellKnownSidType.AnonymousSid, null); - SecurityIdentifier networkSid = new(WellKnownSidType.NetworkSid, null); List rules = security .GetAccessRules(includeExplicit: true, includeInherited: true, typeof(SecurityIdentifier)) .Cast() .ToList(); - PipeAccessRule[] allows = rules.Where(r => r.AccessControlType == AccessControlType.Allow).ToArray(); - PipeAccessRule allow = Assert.Single(allows); + // Exactly one entry: an allow for the gateway's user. In particular no deny entry — a deny + // for NETWORK locked out same-machine workers when the gateway ran from an SSH logon. + PipeAccessRule allow = Assert.Single(rules); + Assert.Equal(AccessControlType.Allow, allow.AccessControlType); Assert.Equal(user, allow.IdentityReference); Assert.Equal(PipeAccessRights.FullControl, allow.PipeAccessRights & PipeAccessRights.FullControl); - PipeAccessRule deny = Assert.Single(rules, r => r.AccessControlType == AccessControlType.Deny); - Assert.Equal(networkSid, deny.IdentityReference); - Assert.DoesNotContain(rules, r => r.IdentityReference == everyoneSid && r.AccessControlType == AccessControlType.Allow); Assert.DoesNotContain(rules, r => r.IdentityReference == anonymousSid && r.AccessControlType == AccessControlType.Allow); }