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.
This commit is contained in:
@@ -743,8 +743,8 @@ gateway↔worker link is a per-session **named pipe**
|
|||||||
(`mxgw-{gatewayPid}-{sessionUid}`), not a network socket. It is not
|
(`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
|
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
|
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
|
protected DACL whose only entry grants its own account (the account the worker
|
||||||
launched as) and denies network logons; on the non-Windows test matrix it uses
|
is launched as); on the non-Windows test matrix it uses
|
||||||
`PipeOptions.CurrentUserOnly`. See `WorkerPipeSecurity` and [Worker Frame Protocol](./WorkerFrameProtocol.md).
|
`PipeOptions.CurrentUserOnly`. See `WorkerPipeSecurity` and [Worker Frame Protocol](./WorkerFrameProtocol.md).
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|||||||
@@ -10,8 +10,13 @@ namespace ZB.MOM.WW.MxGateway.Server.Sessions;
|
|||||||
/// credential-bearing commands (secured writes, <c>AuthenticateUser</c>, alarm-ack passwords), so
|
/// credential-bearing commands (secured writes, <c>AuthenticateUser</c>, alarm-ack passwords), so
|
||||||
/// its unguessable name is not enough: only the account the gateway runs as — which is also the
|
/// 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
|
/// account the worker is launched as, because the launcher sets no alternate credentials — may
|
||||||
/// open it, and never over the network.
|
/// open it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// There is deliberately no deny entry for <c>NT AUTHORITY\NETWORK</c>. That SID marks a network
|
||||||
|
/// <em>logon</em>, 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).
|
||||||
|
/// </remarks>
|
||||||
internal static class WorkerPipeSecurity
|
internal static class WorkerPipeSecurity
|
||||||
{
|
{
|
||||||
/// <summary>Gets the options every worker pipe is created with on non-Windows hosts.</summary>
|
/// <summary>Gets the options every worker pipe is created with on non-Windows hosts.</summary>
|
||||||
@@ -34,8 +39,8 @@ internal static class WorkerPipeSecurity
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a pipe ACL that grants full control to <paramref name="user"/> only, denies network
|
/// Creates a pipe ACL that grants full control to <paramref name="user"/> only and does not
|
||||||
/// logons, and does not inherit any other entries.
|
/// inherit any other entries.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user">The account the gateway and its workers run as.</param>
|
/// <param name="user">The account the gateway and its workers run as.</param>
|
||||||
/// <returns>The pipe security descriptor.</returns>
|
/// <returns>The pipe security descriptor.</returns>
|
||||||
@@ -51,12 +56,6 @@ internal static class WorkerPipeSecurity
|
|||||||
security.SetOwner(user);
|
security.SetOwner(user);
|
||||||
security.AddAccessRule(new PipeAccessRule(user, PipeAccessRights.FullControl, AccessControlType.Allow));
|
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;
|
return security;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Sessions;
|
|||||||
public sealed class WorkerPipeSecurityTests
|
public sealed class WorkerPipeSecurityTests
|
||||||
{
|
{
|
||||||
|
|
||||||
/// <summary>Verifies the factory grants only the given user and denies network logons.</summary>
|
/// <summary>Verifies the factory grants only the given user.</summary>
|
||||||
[WindowsFact]
|
[WindowsFact]
|
||||||
[SupportedOSPlatform("windows")]
|
[SupportedOSPlatform("windows")]
|
||||||
public void Create_GrantsOnlyTheUserAndDeniesNetwork()
|
public void Create_GrantsOnlyTheUser()
|
||||||
{
|
{
|
||||||
SecurityIdentifier user = new("S-1-5-21-1000-2000-3000-1001");
|
SecurityIdentifier user = new("S-1-5-21-1000-2000-3000-1001");
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ public sealed class WorkerPipeSecurityTests
|
|||||||
|
|
||||||
Assert.True(security.AreAccessRulesProtected);
|
Assert.True(security.AreAccessRulesProtected);
|
||||||
Assert.Equal(user, security.GetOwner(typeof(SecurityIdentifier)));
|
Assert.Equal(user, security.GetOwner(typeof(SecurityIdentifier)));
|
||||||
AssertOnlyUserAllowedAndNetworkDenied(security, user);
|
AssertOnlyUserAllowed(security, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -41,12 +41,14 @@ public sealed class WorkerPipeSecurityTests
|
|||||||
using NamedPipeServerStream pipe = SessionWorkerClientFactory.CreatePipe(NewPipeName());
|
using NamedPipeServerStream pipe = SessionWorkerClientFactory.CreatePipe(NewPipeName());
|
||||||
PipeSecurity applied = pipe.GetAccessControl();
|
PipeSecurity applied = pipe.GetAccessControl();
|
||||||
|
|
||||||
AssertOnlyUserAllowedAndNetworkDenied(applied, user);
|
AssertOnlyUserAllowed(applied, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Verifies the pipe still accepts a client running as the same user — the worker is launched
|
/// 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.
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CreatePipe_AcceptsClientRunningAsTheSameUser()
|
public async Task CreatePipe_AcceptsClientRunningAsTheSameUser()
|
||||||
@@ -77,24 +79,22 @@ public sealed class WorkerPipeSecurityTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
[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 everyoneSid = new(WellKnownSidType.WorldSid, null);
|
||||||
SecurityIdentifier anonymousSid = new(WellKnownSidType.AnonymousSid, null);
|
SecurityIdentifier anonymousSid = new(WellKnownSidType.AnonymousSid, null);
|
||||||
SecurityIdentifier networkSid = new(WellKnownSidType.NetworkSid, null);
|
|
||||||
List<PipeAccessRule> rules = security
|
List<PipeAccessRule> rules = security
|
||||||
.GetAccessRules(includeExplicit: true, includeInherited: true, typeof(SecurityIdentifier))
|
.GetAccessRules(includeExplicit: true, includeInherited: true, typeof(SecurityIdentifier))
|
||||||
.Cast<PipeAccessRule>()
|
.Cast<PipeAccessRule>()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
PipeAccessRule[] allows = rules.Where(r => r.AccessControlType == AccessControlType.Allow).ToArray();
|
// Exactly one entry: an allow for the gateway's user. In particular no deny entry — a deny
|
||||||
PipeAccessRule allow = Assert.Single(allows);
|
// 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(user, allow.IdentityReference);
|
||||||
Assert.Equal(PipeAccessRights.FullControl, allow.PipeAccessRights & PipeAccessRights.FullControl);
|
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 == everyoneSid && r.AccessControlType == AccessControlType.Allow);
|
||||||
Assert.DoesNotContain(rules, r => r.IdentityReference == anonymousSid && r.AccessControlType == AccessControlType.Allow);
|
Assert.DoesNotContain(rules, r => r.IdentityReference == anonymousSid && r.AccessControlType == AccessControlType.Allow);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user