Who Can Connect? Access Control on UNIX Sockets
Because UNIX domain sockets appear as files in the file system, their access is controlled by standard file permissions. This gives you a simple but powerful security mechanism: only processes with the right permissions on the socket file can connect or send data.
Beyond file permissions, UNIX domain sockets support credential passing โ a peer can send its PID, UID, and GID and the receiver can verify these credentials with kernel-guaranteed accuracy. The kernel fills in or verifies the credentials, so the receiver can trust them completely.
Key Terms
When you call bind() on a UNIX domain socket, the kernel creates a socket file. This file has permissions determined by your current umask. The default permissions are typically 0777 & ~umask, often resulting in 0755 or 0777.
Controls: Can this process send to the socket (connect() for stream, sendto() for datagram)?
On the directory containing the socket file โ needed to access the socket path.
Not typically needed for sockets themselves (reads happen after connection).
/* Create a socket file accessible only by root */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#define SOCK_PATH "/tmp/secure_socket"
int create_restricted_socket(void)
{
int sfd;
struct sockaddr_un addr;
mode_t old_umask;
sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1) {
perror("socket");
return -1;
}
remove(SOCK_PATH);
/* Method 1: Set umask to control initial permissions */
old_umask = umask(0177); /* Only owner can write (0600 result) */
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);
if (bind(sfd, (struct sockaddr *)&addr, SUN_LEN(&addr)) == -1) {
perror("bind");
umask(old_umask);
return -1;
}
umask(old_umask); /* Restore original umask */
/* Method 2: Use chmod() after bind() for explicit control */
/* Allow only the file owner to connect: */
if (chmod(SOCK_PATH, S_IRUSR | S_IWUSR) == -1) {
perror("chmod");
return -1;
}
/* Allow owner and group to connect: */
/* chmod(SOCK_PATH, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); */
printf("Socket created at %s with restricted permissions\n", SOCK_PATH);
return sfd;
}
/tmp, any user can create a file with the same name before your server starts (a symlink attack). For security, place sockets in a directory that only the server’s user can write to.After accept() on a stream socket (or on a connected datagram socket), you can call getsockopt() with SO_PEERCRED to get the PID, UID, and GID of the connected peer. The kernel fills these in โ they cannot be forged.
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
/* struct ucred โ the credentials structure */
/* Defined in <sys/socket.h> on Linux */
struct ucred {
pid_t pid; /* PID of the sending process */
uid_t uid; /* Effective UID of the sending process */
gid_t gid; /* Effective GID of the sending process */
};
/* Usage: after accept() returns cfd */
int get_peer_credentials(int cfd)
{
struct ucred cred;
socklen_t len = sizeof(cred);
/* getsockopt with SO_PEERCRED fills in cred */
if (getsockopt(cfd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) {
perror("getsockopt(SO_PEERCRED)");
return -1;
}
printf("Peer PID: %ld\n", (long)cred.pid);
printf("Peer UID: %ld\n", (long)cred.uid);
printf("Peer GID: %ld\n", (long)cred.gid);
/* Now you can decide: should this peer be allowed? */
if (cred.uid != 0 && cred.uid != getuid()) {
printf("Unauthorized peer! Closing connection.\n");
return -1; /* Reject this connection */
}
return 0; /* Authorized */
}
Real-world use: The dbus-daemon uses SO_PEERCRED to verify which process is connecting, enabling per-process access control policies. systemd also uses it to identify which service is communicating.
For datagram sockets (which have no connection), SO_PEERCRED is not available. Instead, the sender can explicitly attach credential information to a message using ancillary data (also called control messages) with sendmsg(). The receiver uses recvmsg() to retrieve it.
The Linux kernel verifies the credentials: a non-root process can only send its own PID/UID/GID, not someone else’s. Root can send any credentials.
(message header)
(data buffers)
(ancillary data)
/* Sender: attach credentials to a datagram using sendmsg() */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SV_PATH "/tmp/cred_sv"
#define DATA "Hello with credentials"
int send_with_credentials(int sfd)
{
struct sockaddr_un sv_addr;
struct msghdr msgh;
struct iovec iov;
struct ucred cred;
char cmsg_buf[CMSG_SPACE(sizeof(struct ucred))];
struct cmsghdr *cmsgp;
/* The data payload */
iov.iov_base = (void *)DATA;
iov.iov_len = sizeof(DATA);
/* Build our credentials */
cred.pid = getpid();
cred.uid = getuid();
cred.gid = getgid();
/* Build the control message (ancillary data) */
memset(cmsg_buf, 0, sizeof(cmsg_buf));
cmsgp = (struct cmsghdr *)cmsg_buf;
cmsgp->cmsg_len = CMSG_LEN(sizeof(struct ucred));
cmsgp->cmsg_level = SOL_SOCKET;
cmsgp->cmsg_type = SCM_CREDENTIALS;
memcpy(CMSG_DATA(cmsgp), &cred, sizeof(struct ucred));
/* Build the server address */
memset(&sv_addr, 0, sizeof(sv_addr));
sv_addr.sun_family = AF_UNIX;
strncpy(sv_addr.sun_path, SV_PATH, sizeof(sv_addr.sun_path) - 1);
/* Build the message header */
memset(&msgh, 0, sizeof(msgh));
msgh.msg_name = &sv_addr;
msgh.msg_namelen = SUN_LEN(&sv_addr);
msgh.msg_iov = &iov;
msgh.msg_iovlen = 1;
msgh.msg_control = cmsg_buf;
msgh.msg_controllen = cmsgp->cmsg_len;
/* Send the message with credentials */
if (sendmsg(sfd, &msgh, 0) == -1) {
perror("sendmsg");
return -1;
}
printf("Sent message with PID=%ld UID=%ld GID=%ld\n",
(long)cred.pid, (long)cred.uid, (long)cred.gid);
return 0;
}
/* Receiver: extract credentials from received message using recvmsg() */
int recv_with_credentials(int sfd)
{
struct msghdr msgh;
struct iovec iov;
struct ucred *credp;
struct cmsghdr *cmsgp;
char data_buf[256];
char cmsg_buf[CMSG_SPACE(sizeof(struct ucred))];
ssize_t n;
iov.iov_base = data_buf;
iov.iov_len = sizeof(data_buf) - 1;
memset(&msgh, 0, sizeof(msgh));
msgh.msg_iov = &iov;
msgh.msg_iovlen = 1;
msgh.msg_control = cmsg_buf;
msgh.msg_controllen = sizeof(cmsg_buf);
n = recvmsg(sfd, &msgh, 0);
if (n == -1) {
perror("recvmsg");
return -1;
}
data_buf[n] = '\0';
printf("Received data: %s\n", data_buf);
/* Walk the control message chain looking for SCM_CREDENTIALS */
for (cmsgp = CMSG_FIRSTHDR(&msgh);
cmsgp != NULL;
cmsgp = CMSG_NXTHDR(&msgh, cmsgp)) {
if (cmsgp->cmsg_level == SOL_SOCKET &&
cmsgp->cmsg_type == SCM_CREDENTIALS) {
credp = (struct ucred *)CMSG_DATA(cmsgp);
printf("Sender PID: %ld\n", (long)credp->pid);
printf("Sender UID: %ld\n", (long)credp->uid);
printf("Sender GID: %ld\n", (long)credp->gid);
}
}
return 0;
}
Important: To receive SCM_CREDENTIALS, the receiving socket must have the SO_PASSCRED option enabled:
int enable = 1;
setsockopt(sfd, SOL_SOCKET, SO_PASSCRED, &enable, sizeof(enable));
Working with ancillary data requires using the CMSG_* macros to properly navigate the control message buffer. These macros handle alignment issues for you.
| Macro | Purpose |
|---|---|
CMSG_SPACE(len) |
Total bytes needed for control buffer (includes alignment padding) |
CMSG_LEN(len) |
Value for cmsg_len field (header + data, no trailing pad) |
CMSG_DATA(cmsgp) |
Pointer to the data portion of a control message |
CMSG_FIRSTHDR(msgh) |
Pointer to first control message in the buffer |
CMSG_NXTHDR(msgh, cmsgp) |
Pointer to next control message (for iteration) |
/* Always allocate control buffer using CMSG_SPACE, not sizeof: */
char cmsg_buf[CMSG_SPACE(sizeof(struct ucred))]; /* CORRECT */
/* char cmsg_buf[sizeof(struct cmsghdr) + sizeof(struct ucred)]; WRONG - alignment issues */
/* Always set cmsg_len using CMSG_LEN: */
cmsgp->cmsg_len = CMSG_LEN(sizeof(struct ucred)); /* CORRECT */
Using file system permissions on the socket file. The server can call chmod() on the socket path to restrict access: for example, chmod(path, 0600) means only the socket owner can connect. The server can also place the socket in a directory that only authorized users can access (execute permission on the directory). This is simpler and more robust than trying to authenticate inside the application.
SO_PEERCRED is a Linux socket option that retrieves the PID, UID, and GID of the process on the other end of a connected UNIX domain socket. You use getsockopt(fd, after
SOL_SOCKET, SO_PEERCRED, &cred, &len)accept(). The kernel fills in the credentials โ they cannot be spoofed. This is useful for servers that need to authorize clients based on who they are (e.g., “only root or the web server user can connect”). D-Bus and systemd use this extensively.
SO_PEERCRED is used on stream sockets after a connection is established โ the receiver calls getsockopt() and the kernel reports the credentials of the connected peer. SCM_CREDENTIALS is used with ancillary data on both stream and datagram sockets โ the sender explicitly attaches credentials to a message using sendmsg(), and the receiver extracts them from recvmsg(). Both are kernel-verified and cannot be forged by unprivileged processes.
Only a process with CAP_SETUID / CAP_SETGID capability (typically root) can send credentials that differ from its own PID/UID/GID. A regular (non-root) process that tries to send someone else’s credentials will get EPERM. The kernel validates the credentials before allowing the sendmsg() to succeed.
Ancillary data (also called control messages or out-of-band data for sockets) is metadata attached to a message sent via sendmsg(). It travels alongside the normal data but in a separate buffer (msg_control). Each control message has a struct cmsghdr header with cmsg_level, cmsg_type, and cmsg_len fields identifying what the data contains. For UNIX domain sockets, the two important types are SCM_CREDENTIALS (identity) and SCM_RIGHTS (file descriptor passing, covered in Chapter 61).
