C# named pipes and Windows Service permissions
Posted: (EET/GMT+2)
I was today working on a classic Windows Service application with C# that communicates to the outside with named pipes. As with many things this close to the operating system, you have to be careful that you have the correct permissions, especially if you are developing clients that connect to this service application (like I was).
Namely, you need to remember: if a Windows Service creates a named pipe, remember that the pipe also has Windows access control (ACL). This can be easy to miss during local testing. The code may work perfectly when the service runs under your own user account, but fail after installation when the service runs under a separate service account.
A common symptom is that the client can connect only when run as administrator. Otherwise, you will get the classic "Access denied" error message.
Instead of just running as admin and going on, is not to run the client elevated. Configure the named pipe access control list (ACL) when the pipe server is created.
First, add the access control package if the project needs it:
dotnet add package System.IO.Pipes.AccessControl
Then create a PipeSecurity object and allow the client users or group to connect with NamedPipeServerStreamAcl.Create (documentation here).
using System.IO.Pipes;
using System.Security.AccessControl;
using System.Security.Principal;
const string PipeName = "MyAppPipeNameHere";
PipeSecurity pipeSecurity = new();
WindowsIdentity currentIdentity = WindowsIdentity.GetCurrent();
pipeSecurity.AddAccessRule(new PipeAccessRule(
currentIdentity.User!, PipeAccessRights.FullControl,
AccessControlType.Allow));
NTAccount allowedClients = new NTAccount("MY-SERVER", "MyAppPipeUsers");
pipeSecurity.AddAccessRule(new PipeAccessRule(
allowedClients, PipeAccessRights.ReadWrite,
AccessControlType.Allow));
using NamedPipeServerStream pipeServer =
NamedPipeServerStreamAcl.Create(
pipeName: PipeName,
direction: PipeDirection.InOut,
maxNumberOfServerInstances: 1,
transmissionMode: PipeTransmissionMode.Byte,
options: PipeOptions.Asynchronous,
inBufferSize: 0,
outBufferSize: 0,
pipeSecurity: pipeSecurity);
await pipeServer.WaitForConnectionAsync();
In this example, the service account gets full control over the pipe, and members of the local group MyAppPipeUsers can read and write.
Create the local group and add the client users to it:
net localgroup MyAppPipeUsers /add net localgroup MyAppPipeUsers MYDOMAIN\someuser /add
Tip: prefer granting access to a Windows group instead of hardcoding individual user names in the application.
The client code can then connect normally:
using System.IO.Pipes;
using NamedPipeClientStream pipeClient = new(".", "MyAppPipeNameHere",
PipeDirection.InOut, PipeOptions.Asynchronous);
await pipeClient.ConnectAsync(5000);
The pipe name maps to the Windows pipe path:
\\.\pipe\MyAppPipeNameHere
Tip 2: do not use Everyone with broad access unless that is really intended. For service communication, a specific local or domain group is usually cleaner.
This is especially important for Windows Services because the service account and the interactive user account are usually different identities.
Local testing can hide the problem because the server and client may both run as the same user. Once the service runs as MYDOMAIN\MyServiceAccount and the client runs as
MYDOMAIN\SomeUser, the named pipe ACL starts to matter.
Good checks when named pipe clients cannot connect:
- which account runs the Windows Service?
- which account runs the client?
- does the pipe ACL grant the client read/write access?
- is the client only working because it was started as administrator?
- should access be granted through a local or domain group?
For production, keep the rule simple: the service owns the pipe, and only the expected client users or groups can connect.
Hope this helps!