fix(sessions): restrict the worker named pipe to the gateway's account (#187)
On Windows each worker pipe is created via NamedPipeServerStreamAcl with a protected DACL granting FullControl only to the gateway process user (the worker is launched under the same account) and denying NETWORK logons. On non-Windows hosts (test matrix) the pipe uses PipeOptions.CurrentUserOnly. Tests assert the factory ACL and the ACL read back from the created pipe (Windows), and that a same-user client still connects.
This commit is contained in:
@@ -171,15 +171,30 @@ public sealed class SessionWorkerClientFactory : ISessionWorkerClientFactory
|
||||
/// The buffer sizes are explicit so the pipe is not created with a zero quota; see
|
||||
/// <see cref="PipeBufferSizeBytes"/>. On Unix hosts (the macOS test matrix, where named pipes
|
||||
/// are Unix domain sockets) the sizes are advisory — the fix targets Windows production.
|
||||
/// Access is restricted to the gateway's own account (#187): on Windows by the explicit ACL
|
||||
/// from <see cref="WorkerPipeSecurity"/>, elsewhere by <see cref="PipeOptions.CurrentUserOnly"/>.
|
||||
/// </remarks>
|
||||
private static NamedPipeServerStream CreatePipe(string pipeName)
|
||||
internal static NamedPipeServerStream CreatePipe(string pipeName)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return NamedPipeServerStreamAcl.Create(
|
||||
pipeName,
|
||||
PipeDirection.InOut,
|
||||
maxNumberOfServerInstances: 1,
|
||||
PipeTransmissionMode.Byte,
|
||||
PipeOptions.Asynchronous,
|
||||
inBufferSize: PipeBufferSizeBytes,
|
||||
outBufferSize: PipeBufferSizeBytes,
|
||||
WorkerPipeSecurity.CreateForCurrentUser());
|
||||
}
|
||||
|
||||
return new NamedPipeServerStream(
|
||||
pipeName,
|
||||
PipeDirection.InOut,
|
||||
maxNumberOfServerInstances: 1,
|
||||
PipeTransmissionMode.Byte,
|
||||
PipeOptions.Asynchronous,
|
||||
WorkerPipeSecurity.UnixPipeOptions,
|
||||
inBufferSize: PipeBufferSizeBytes,
|
||||
outBufferSize: PipeBufferSizeBytes);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.IO.Pipes;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Security.AccessControl;
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Sessions;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the access control applied to each gateway↔worker named pipe. The pipe carries
|
||||
/// 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
|
||||
/// account the worker is launched as, because the launcher sets no alternate credentials — may
|
||||
/// open it, and never over the network.
|
||||
/// </summary>
|
||||
internal static class WorkerPipeSecurity
|
||||
{
|
||||
/// <summary>Gets the options every worker pipe is created with on non-Windows hosts.</summary>
|
||||
/// <remarks>
|
||||
/// On Unix the pipe is a Unix domain socket; <see cref="PipeOptions.CurrentUserOnly"/> makes it
|
||||
/// owner-only and rejects peers running as a different user. Windows uses the explicit ACL from
|
||||
/// <see cref="Create(SecurityIdentifier)"/> instead.
|
||||
/// </remarks>
|
||||
public const PipeOptions UnixPipeOptions = PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly;
|
||||
|
||||
/// <summary>Creates the pipe ACL for the account the gateway process runs as.</summary>
|
||||
/// <returns>The pipe security descriptor.</returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static PipeSecurity CreateForCurrentUser()
|
||||
{
|
||||
using WindowsIdentity identity = WindowsIdentity.GetCurrent();
|
||||
SecurityIdentifier user = identity.User
|
||||
?? throw new InvalidOperationException("The gateway process identity has no user SID.");
|
||||
return Create(user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a pipe ACL that grants full control to <paramref name="user"/> only, denies network
|
||||
/// logons, and does not inherit any other entries.
|
||||
/// </summary>
|
||||
/// <param name="user">The account the gateway and its workers run as.</param>
|
||||
/// <returns>The pipe security descriptor.</returns>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static PipeSecurity Create(SecurityIdentifier user)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
|
||||
PipeSecurity security = new();
|
||||
|
||||
// Drop inherited entries so the default pipe DACL (Everyone/Anonymous read) cannot creep back in.
|
||||
security.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user