FreeBSD Unix-Domain Peer Credentials: Authorize Local Socket Clients
Use FreeBSD getpeereid() to authorize local stream-socket clients, separate kernel peer credentials from request claims, and test the boundary safely.
A pathname Unix-domain socket keeps a service on the local machine, but locality is not an authorization policy. A daemon that accepts a connection and then trusts a username in the first request has let the requester choose its own identity. On FreeBSD, a server can inspect kernel-provided credentials for a connected stream socket with getpeereid(3) and apply an explicit account policy before it reads application data.
That check answers a narrow question: which effective UID and GID were associated with the peer when the connection was established? It does not authenticate every byte, identify a particular executable, or decide whether that account may perform every operation. Keep those boundaries visible in both the protocol and the code.
Separate connection identity from request identity
The FreeBSD libc function is declared in <unistd.h>:
int getpeereid(int socket, uid_t *euid, gid_t *egid);
It succeeds for a connected SOCK_STREAM Unix-domain socket and writes the peer’s effective user and group IDs to the output arguments. On the server side, the credentials are those of the client when it called connect(2). On the client side, they are those of the server when it called listen(2). FreeBSD implements the helper through the LOCAL_PEERCRED option documented by unix(4).
This is connection-time state, not a live query of the other process. If a client changes its credentials after connecting, the server does not receive a refreshed identity through getpeereid(). Likewise, a client that connects while a service is listening observes the service credentials cached for the listener, not necessarily the account under which the daemon runs later. A service that starts as root, calls listen(), and then drops privileges should account for that ordering when clients verify the server.
Use the returned numeric IDs as inputs to a policy the service owns. Do not accept a requested UID, account name, group name, or PID from the request and substitute it for the kernel result. If configuration is expressed with account names, resolve and validate that configuration through the service’s trusted setup path, then compare the intended numeric identity. The socket API itself supplies effective UID and GID; it is not a lookup service for names or a complete authorization engine.
Apply the check before parsing a request
For a server that accepts local clients under one approved effective UID and GID, a small helper can fail closed:
#include <sys/types.h>
#include <errno.h>
#include <unistd.h>
static int
authorize_client(int connected_fd, uid_t allowed_uid, gid_t allowed_gid)
{
uid_t peer_uid;
gid_t peer_gid;
if (getpeereid(connected_fd, &peer_uid, &peer_gid) == -1)
return (-1); /* Preserve the syscall error in errno. */
if (peer_uid != allowed_uid || peer_gid != allowed_gid) {
errno = EACCES;
return (-1);
}
return (0);
}
Call it on the connected descriptor returned by accept(2), before reading the request. Supply allowed_uid and allowed_gid from trusted service configuration, not from socket data. On any failure, save errno if needed for logging, close the client descriptor, and do not continue into the request parser. getpeereid() can fail for an invalid descriptor, a non-socket, a socket that is not connected, or a socket type other than SOCK_STREAM; treating those failures as permission to continue would invert the security boundary.
This sample implements an exact effective-UID/effective-GID rule. A policy that authorizes any member of a group needs more care: getpeereid() returns one effective GID, not the peer’s supplementary-group set. Do not infer supplementary membership by looking up a PID after the fact. FreeBSD’s unix(4) page warns that a credential structure’s PID may already have been reused when an application queries process state, so a PID lookup must not establish authorization.
Keep pathname permissions as a separate gate
FreeBSD Unix-domain addresses are filesystem pathnames. Binding a socket creates a socket node; normal filesystem access checks apply when a client references the path. For connect(2), denied search permission on a path component or denied write access to the socket can reject the attempt before the server handles an accepted connection. Those checks control who can reach the endpoint. The later peer-credential comparison lets the application make its own explicit decision for the connection.
Protect the parent directory as well as the socket node. Use a service-owned runtime directory and the narrow group or account access required by the deployment. Avoid world-writable socket directories and broad modes used as a shortcut for a local client problem. unix(4) also notes that binding leaves a filesystem socket node behind when the socket closes; an application must manage its own path cleanup. Do not remove a path speculatively while a service may still own or recreate it. If a service needs to set the node’s permissions with fchmod(2), the manual describes doing so before bind(2) to avoid a creation-to-chmod(2) race.
Path access is useful defense in depth, but it is not a substitute for checking the accepted peer. Conversely, a successful credential comparison does not make a writable parent directory safe from endpoint replacement or lifecycle mistakes. Establish who owns the directory, how the listener is created, and which process is allowed to connect before enabling a privileged operation.
Choose message credentials only when the protocol needs them
getpeereid() is intentionally narrower than the unix(4) credential options. The FreeBSD manual documents LOCAL_PEERCRED for SOCK_STREAM and SOCK_SEQPACKET; direct getsockopt(2) use returns an xucred structure whose cr_version must be checked against XUCRED_VERSION. That broader option does not change the contract of the getpeereid() wrapper, which is documented for SOCK_STREAM.
Datagrams and protocols that need to attribute individual sends require the message-control path instead. LOCAL_CREDS asks the receiver to obtain credentials through recvmsg(2) ancillary data, including on datagram sockets. FreeBSD also documents LOCAL_CREDS_PERSISTENT for repeated credentials on stream or sequenced-packet reads. Validate the control-message level, type, length, and presence alongside the payload, and design around the precise delivery semantics of the chosen socket type. Do not enable a message option and then silently accept requests whose required credential control message was absent or malformed.
Even per-message credentials identify the sending process credentials, not authorship of every payload byte. The unix(4) manual specifically warns that a setuid program may write data partly controlled by its invoker under the program’s effective identity. A privileged helper can therefore be the credentialed sender while forwarding attacker-influenced text. Credential metadata does not replace a bounded frame format, strict parsing, or authorization for each requested action.
Test the policy with isolated local accounts
Use a disposable FreeBSD VM or test jail configured for the exact service and socket path. Exercise at least these cases with a harmless, read-only test request:
- A client with the approved effective UID and GID connects and receives only the operation the test policy permits.
- A different UID or GID connects but is rejected before the server reads its payload.
- An invalid descriptor, an unconnected stream, and a connected datagram do not pass the helper.
- In an isolated fixture, connect under one credential, change the client’s credential where permitted, and confirm that the server still sees the connection-time identity rather than treating it as a refresh.
- Verify that path traversal permission and socket-node write permission deny an unapproved connection independently of the application check.
Run positive and negative cases after changes to account ownership, directory mode, listener startup order, or the authorization list. Keep logs limited to the numeric identity and decision needed for operations; do not dump request contents or unrelated credentials into diagnostics. Then test the request parser separately with oversized, truncated, and malformed frames. Authentication at the socket boundary should reduce the set of clients, not the amount of input validation required.
Know what the result does not prove
A matching UID and GID means the connection was made under those effective credentials according to the kernel’s connection-time contract. It does not distinguish two programs running as the same account, prove the peer’s binary is uncompromised, or guarantee that the peer remains alive. It does not bind a process name or PID to the connection. If the service has multiple request classes, authenticate the connection once and still authorize each command and object against application policy.
Use a different mechanism when the needed identity is per-message, includes supplementary groups, crosses a machine boundary, or must distinguish mutually untrusted processes that share one UID. Keep the service account least-privileged, retain filesystem controls on the socket path, and reject any connection whose credential lookup or policy evaluation fails. The useful guarantee is narrow but strong: a local request cannot claim a different connect-time effective UID/GID merely by writing a different value into its payload.
Related:
- FreeBSD sockstat Operations: Attribute Listeners, Connections, and Unix Sockets
- Capsicum Beyond the Basics: Building Capability-Mode Services From Scratch
Sources: