Postfix
NoNewPrivileges support This document describes how Postfix uses set-gid privilege elevation to gate access from an unprivileged process to the Postfix queue and to core daemon processes. It then presents an alternative privilege elevation mechanism for systems that disable set-gid privilege elevation.
This guide assumes that Postfix uses the default settings: "mail_owner = postfix" and "setgid_group = postdrop".
Historically, a user request travels through three security zones on its way to a Postfix core daemon process:
Zone 1
user UID
user GIDZone 2
user UID
Set-gid "postdrop"Zone 3
"postfix" UID
"postfix" GIDmail(1)
mailx(1)
sendmail(1)
mailq(1)-> postdrop(1)
postlog(1)
postqueue(1)-> pickup(8)
postlogd(8)
qmgr(8)
flush(8)
showq(8)
Zone 1 contains Non-Postfix utilities such as mail(1) and mailx(1), alongside Postfix helper programs such as sendmail(1) and mailq(1) that operate without Postfix-specific user or group privileges. Because Zone 1 processes cannot directly access Postfix queues or other Postfix-internal resources, they must request service through Zone 2.
Zone 2 contains the maildrop queue and Postfix client processes including postdrop(1), postlog(1) and postqueue(1). While the processes retain the UID of the invoking user, they run with set-gid "postdrop" group privileges. This allows them to access the maildrop queue and specific Postfix core daemon processes.
Zone 3 contains all other Postfix queues and all core daemon processes. Following initialization, these processes run with "postfix" user and group privileges. Some daemon processes (not shown in the figure) may temporarily impersonate a specific user account when delivering mail or running non-Postfix code.
The setgid-based, three-zone, architecture has several key benefits. These will need to be preserved on systems that don't support set-gid privileges:
Zone 2 can accept email submissions from Zone 1 even when the Postfix system is down and no Zone 3 processes are running. This is possible because the set-gid privilege allows the postdrop(1) command to deposit messages into the protected maildrop queue.
Zone 2 enforces a per-client-UID concurrency limit based on the host's per-user process limit, as each service request requires a distinct Zone 2 set-gid process. However, this may not be an effective deterrent against overload, because per-user process limits vary wildly between UNIX-like systems.
Zone 2 enforces low-level input validation before a request reaches Zone 3.
Zone 2 enforcement resists tampering by unprivileged users, because Zone 2 processes run with distinct UIDs and GIDs from Zone 1.
Some systems disable set-gid privilege elevation, for example using "NoNewPrivileges=yes" or the "nosuid" file system mount option. On these systems, Postfix may use an alternative privilege elevation method that is implemented with the postlift(8) service, changing the architecture as shown below. As of Postfix 3.12, this alternative method is supported only on Linux environments using systemd.
Zone 1
user UID
user GID
Zone 2
"postdrop" UID
"postdrop" GIDZone 3
"postfix" UID
"postfix" GIDmail(1)
mailx(1)
sendmail(1)
mailq(1)-> postdrop(1)
postlog(1)
postqueue(1)-> postlift(8) -> postdrop(1)
postlog(1)
postqueue(1)-> pickup(8)
postlogd(8)
qmgr(8)
flush(8)
showq(8)
When set-gid privileges are disabled, a postdrop(1), postlog(1), or postqueue(1) client process with Zone 1 privileges will connect to the Zone 2 postlift(8) service, requesting that postlift(8) runs the same command (postdrop(1) etc.) in a process with Zone 2 privileges. This allows the elevated process to add a message to the protected maildrop queue, or to interact with a Zone 3 core daemon process. postlift(8) propagates the exit status from the elevated process back to the Zone 1 client process.
The super-user ("root") is excluded from postlift(8) privilege elevation. Because root already has sufficient privileges, its Zone 1 postdrop(1), postlog(1), or postqueue(1) processes can directly access the maildrop queue or a Zone 3 process. This eliminates a postlift(8) start-up dependency and avoids some implementation complexity.
The architecture above can provide most of the benefits from the architecture based on set-gid privilege elevation:
(worse) Zone 2 can accept email submissions from Zone 1 while the Postfix system is down, under two conditions:
Root-privileged processes: accepted at any time.
Non-root processes: accepted only after a system start-up target activates the postlift(8) socket. Other submissions will be lost.
Note: some boot-time activity may generate email before a "network" or "multi-user" start-up target activates the postlift(8) socket.
(better) Zone 2 can enforce a per-client-UID concurrency limit before a request reaches Zone 3, relying on systemd or similar infrastructure for enforcement instead of host-based process limits.
(same) Zone 2 enforces low-level input validation before a request reaches Zone 3.
(same) Zone 2 enforcement resists tampering by unprivileged users, because Zone 2 processes run with distinct UIDs and GIDs from Zone 1.
To run postlift(8) under systemd, follow the steeps below:
Verify that your system has a "postdrop" username with the same group ID as the "postdrop" group, and with a different UID than the "postfix" account. The table shows an example.
| Entity | File | UID | GID | Notes |
|---|---|---|---|---|
| "postfix" user | /etc/passwd | 1001 | 1001 | Different UID and GID than the "postdrop"
user. DO NOT CHANGE an existing "postfix" user. |
| "postdrop" user | /etc/passwd | 1002 | 1002 | Different UID and GID than the "postfix"
user. Same GID as the "postdrop" group. |
| "postdrop" group | /etc/group | 1002 | Same GID as the "postdrop" user. DO NOT CHANGE an existing "postdrop" group. |
It is OK when your system is different, as long as the numbers are different between the "postfix" and "postdrop" users, and as long as the "postdrop" user's GID is the same as the "postdrop" group's GID (though they don't have to be 1002).
Caution: DO NOT CHANGE an existing "postdrop" group.
Caution: DO NOT CHANGE an existing "postfix" user.
Create a file /usr/lib/systemd/system/postlift.socket with the following content:
[Unit] Description=Postfix postlift Socket Documentation=man:postlift(8) [Socket] Accept=yes RemoveOnStop=yes ListenStream=/var/run/postfix/postlift.socket # MaxConnectionsPerSource must be less than MaxConnections (default 64) MaxConnectionsPerSource=30 [Install] WantedBy=sockets.target
Create a file /usr/lib/systemd/system/postlift@.service with the following content:
[Unit] Description=Postfix postlift helper Documentation=man:postlift(8) Requires=postlift.socket [Service] # DO NOT run as "postfix" (i.e. core daemon privilege). User=postdrop Group=postdrop ExecStart=/usr/libexec/postfix/postlift Type=simple StandardInput=socket StandardOutput=socket # Only stderr goes to journal for logging StandardError=journal SyslogFacility=mail
Read the new configuration:
# systemctl daemon-reload
Enable the postlift socket:
# systemctl enable --now postlift.socket
Verify the socket status. The expected output looks like:
# systemctl status postlift.socket
● postlift.socket - Postfix postlift Socket
Loaded: loaded (/usr/lib/systemd/system/postlift.socket; enabled; preset: disabled)
Active: active (listening) since Mon 2026-09-28 15:50:21 EDT; 1 week 5 days ago
Invocation: 157533134c214bccaf5fa1fc6031e29c
Docs: man:postlift(8)
Listen: /var/run/postfix/postlift.socket (Stream)
Accepted: 158; Connected: 0;
Sep 28 15:50:21 mail.example.org systemd[1]: Listening on postlift.socket - Postfix postlift Socket.
In case of problems, first look for Postfix logging (in /var/log/maillog or the like). If that does not reveal the problem cause, verify the socket status as shown above and look for failed service instances. These have names like postlift@xx-xx-xx.service. Examine each failed instance with a command like:
# systemctl status postlift@xx-xx-xx.service
and review the error logging at the bottom of the output.
On systems without systemd, partial functionality can be implemented using the Postfix spawn(8) service. This service accepts connections on a private or public (but still protected) socket, and executes a configured program with specific privileges. However, making this work with postlift(8) requires two additional Postfix features:
A new master(5) listen socket type, distinct from the existing "private" or "public" types. This socket must be world-writable, and hosted in directory with world read+execute permission so that it is accessible to unprivileged Zone 1 client commands like postdrop(1).
spawn(8) integration with the existing anvil(8) service. This integration limits the per-userID concurrency for requests originating from Zone 1.
A down-side is that spawn(8) daemons remain unavailable until Postfix starts. Consequently, Zone 1 email submissions from non-root processes will fail to enter the queue before Postfix is running. Because Postfix typically launches with a "network" or "multi-user" start-up target, early boot-time email messages from non-root users may be dropped.
Thorsten Kukuk proposed a systemd integration for the postdrop(1) command which inspired the postlift(8) design.
Wietse generalized the approach in multiple dimensions:
Multiple client support: one postlift(8) configuration can elevate privileges for postdrop(1), postlog(1) and postqueue(1).
Multiple instance support: one postlift(8) configuration serves multiple Postfix instances that are managed with postmulti(1).