# Postfix unprivileged master(8) operation

## Summary

This document describes a proof of concept that may be used for experiments to run the Postfix master(8) daemon without "root" privileges (and consequently, all Postfix daemon processes). As discussed below, this will make Postfix less secure.

This is where "unprivileged" Postfix can or must not be used:

- Feasible for production use, with some weakened security:

  - Unprivileged master(8) deployments that forward all messages with SMTP or LMTP.

  - This does not make Postfix less secure than other mail systems. It just makes Postfix less secure than it could be.

- MUST NOT BE USED FOR PRODUCTION:

  - Unprivileged master(8) deployments that can execute non-Postfix programs (with local(8), pipe(8) or spawn(8)) or that deliver to files (with local(8) or virtual(8)).

The remainder of this document is organized as follows:

- Postfix privileged security model, and what changes with unprivileged operation
- Unprivileged Postfix management: start, stop, reload, queue, and configuration
- Credits
- Appendix

## Postfix privileged security model, and what changes with unprivileged operation

This section summarizes the Postfix security model for privileged master(8) operation, and how it is weakened with an unprivileged master(8).

- Over-all Postfix architecture:

  Postfix consists of a) service daemons that receive and deliver email messages in the background, b) client commands that are invoked by users, and c) commands for system administrators. The privileged master(8) daemon runs all service daemons in a controlled environment. Postfix has no set-uid program files, eliminating a large class of attacks.

  > What changes with unprivileged master(8) operation:

  > The master(8) daemon runs with fixed "postfix" privilege instead of "root". This affects all Postfix service daemon processes as discussed next.

- Postfix service daemon processes:

  When launched by a privileged master(8) daemon, a Postfix service daemon can revoke "root" privileges after initialization, before it handles any requests. This allows each service process to open and read sensitive files (e.g. TLS private keys or database passwords) and to open and modify sensitive databases or logfiles. When a service process becomes compromised after it drops root privileges, it can no longer open sensitive files, databases or logfiles.

  All but four Postfix service daemons irrevocably release root privileges, and therefore do not in any sense run as "root" once they're handling service requests. Here, "root" privilege is used to separate privileges within Postfix (internal privilege separation).

  The only exceptions are local(8), pipe(8), spawn(8) and virtual(8) which run as "postfix" most of the time, but retain a saved effective uid of "root" so that they can temporarily run as the target user (the user that owns the target mailbox or maildir file, or that is configured to execute the given command). Here, root privilege is used to select different privileges for Postfix and non-Postfix resources ("postfix" privilege confinement).

  If the Postfix service daemons in question do not access non-Postfix files and if they do not execute non-Postfix commands, then all the services launched by the master(8) daemon will irrevocably run unprivileged after initialization.

  > What changes with unprivileged master(8) operation:

  > Postfix cannot maintain internal privilege separation (service daemons must use the same Postfix privileges while initializing and while handling requests), and it cannot maintain "postfix" privilege confinement (service daemons must use the same "postfix" privileges to execute non-Postfix programs, to deliver to mailbox or maildir files, or to access non-Postfix files). Specifically:

  > - These MUST NOT be used in production:

  >   - The local(8) deliver agent will execute "`|command`" etc. instructions in a user's ".forward" file with "postfix" privileges instead of the target user's privileges, allowing the user to take control of the Postfix mail system.

  >   - All local(8) and virtual(8) mailbox and maildir files will be owned by the "postfix" user instead of the receiving user. If a user can read one mailbox and maildir file, they can read, write, and delete all such files; additionally they can read, write, and delete all messages in the Postfix queue.

  >   - The local(8), pipe(8) or spawn(8) daemons will execute non-Postfix commands with "postfix" privileges instead of the target user's privileges; if the command has a vulnerability, then an attacker can take over the Postfix mail system.

  > - The weaker protection of secret-key files and the optional Postfix "maillog_file" logging are discussed under the next item.

- Postfix configuration, secrets, and logs:

  Postfix configuration files and "maillog_file" logfiles are owned by the "root" user. Additionally, secret key files are readable only by "root". This protects secret keys, configuration files, and Postfix logging, after someone compromises an unprivileged Postfix service daemon process.

  > What changes with unprivileged master(8) operation:

  > - Configuration files including secret keys will still be owned by "root", but keys will need to be readable by the "postfix" user. After someone compromises an unprivileged Postfix service daemon process, they will be able to read all Postfix secret key files.

  > - The optional Postfix "maillog_file" logfiles will need to be owned by "postfix" instead of "root". After someone compromises an unprivileged Postfix service daemon process, they will be able to read, write, or delete Postfix "maillog_file" records.

  > - This does not make Postfix less secure than other mail systems. It just makes Postfix less secure than it could be.

- Handling requests from local users:

  Postfix uses the set-gid postdrop(1) client command to submit messages into the mail queue; uses the set-gid postqueue(1) client command to list or mail queue, or to request delivery; and uses the set-gid postlog(1) client command to report events to the optional Postfix "maillog_file". Postfix provides sendmail-compatible helper programs sendmail(1) and mailq(1) that invoke postdrop(1) or postqueue(1).

  > What changes with unprivileged master(8) operation:

  > No change. The set-gid client programs are still needed.

- Postfix queue files and directories:

  The "postfix" user owns all Postfix queue directories and queue files, except for local submissions created with the postdrop(1) command. Those queue files are owned by the originating user, and are stored in the "maildrop" queue directory, from where they are ingested into Postfix by the pickup(8) service daemon. The Postfix queue is not directly accessible by users.

- Commands for system administrators:

  The postfix(1), postmulti(1), and postsuper(1) commands are reserved for the "root" user; these commands require "root" privileges to make configuration changes.

  > What changes with unprivileged master(8) operation:

  > As detailed in the next section, many commands may also be invoked by the "postfix" user, but "root" is still required for commands that make configuration changes.

## Unprivileged Postfix management: start, stop, reload, queue , and configuration

The following instructions assume that the steps in the " Appendix have already been implemented.

- To start, reload or stop Postfix, execute as the "root" or "postfix" user:

  >     postfix start

  >     postfix reload

  >     postfix stop

- Commands that do not change configuration may be executed as the "root" or "postfix" user:

  >     postfix status

  >     postfix non-bdb status

  >     postfix tls output-server-csr (or output-server-tlsa)

- The postsuper(1) queue management command may be executed as the "root" or "postfix" user:

  >     postsuper options... arguments...

- Commands that change Postfix permissions or configuration must be executed as the "root" user:

  >     postfix set-permissions

  >     postfix tls subcommand

  >     postfix non-bdb subcommand

  >     postmulti -e command

  >     postconf "key = value"

  >     postmap "maptype:mapname..."

  >     postalias "maptype:mapname..."

Postfix configurations MUST NOT be owned by the `postfix` user: that would be too convenient for someone who compromises a Postfix daemon process.

## Credits

- Wietse Venema originally implemented unprivileged master(8) operation for unreleased integration tests in 2025. After a customer inquiry in 2026, Wietse made the implementation suitable for limited production deployment, and added documentation.

- Viktor Dukhovni contributed background (adopted in the documentation) and MacOS support (code).

## Appendix

### Build requirements

Unprivileged master(8) operation requires that Postfix is built with dynamically-linked libraries, that is, with "`shared=yes`" on the "`make makefiles`" command line.

### One-time changes required after initial Postfix installation

After installing Postfix from a standard pre-compiled package or from source code, a few tweaks are needed to make unprivileged operation possible. These changes should persist after subsequent Postfix updates.

> Note: with multi-instance configuration, all Postfix instances must use privileged master(8) operation, or all instances must use unprivileged master(8) operation. You cannot have a mix of privileged and unprivileged.

To revert to the privileged Postfix operation, stop Postfix and undo the changes described in the subsections that follow.

### Update maillog_file permissions

- If you configure a non-default "maillog_file" setting, then Postfix will create that file with user "postfix" privileges (instead of "root"). The parent directory and logfile will need to be writable by the "postfix" user. Example:

  >     postconf maillog_file=/var/log/postfix/maillog
  >     mkdir -p /var/log/postfix
  >     touch /var/log/postfix/maillog
  >     chown postfix /var/log/postfix /var/log/postfix/maillog

- In a multi-instance configuration, each Postfix instance has its own maillog_file setting.

- To revert to privileged Postfix master(8) operation, make the parent directory (in the example: /var/log/postfix) writable by "root" only.

- In a multi-instance configuration, you need to revert this for each Postfix instance.

### Update the import_environment parameter setting

- As user "root", update the file "main.cf" with the following command:

  >     postconf "import_environment = $(postconf -dh import_environment) \
  >         LD_PRELOAD=\$shlib_directory/fake_eugid.so"

  Do not omit the "`\`" in "`\$shlib_directory`".

  For MacOS that should instead be:

  >     postconf "import_environment = $(postconf -dh import_environment) \
  >         DYLD_INSERT_LIBRARIES=\$shlib_directory/fake_eugid.dylib"

  Do not omit the "`\`" in "`\$shlib_directory`".

  The result is an `import_environment` setting that contains both the default import_environment value plus the literal string `LD_PRELOAD=$shlib_directory/fake_eugid.so`. With Postfix 3.12 the result looks like:

  >     import_environment = MAIL_CONFIG MAIL_DEBUG MAIL_LOGTAG TZ XAUTHORITY
  >         DISPLAY LANG=C POSTLOG_SERVICE POSTLOG_HOSTNAME XDG_RUNTIME_DIR
  >         LD_PRELOAD=$shlib_directory/fake_eugid.so

  (it will be all on one long line).

  With MacOS and Postfix 3.12 the result looks like:

  >     import_environment = MAIL_CONFIG MAIL_DEBUG MAIL_LOGTAG TZ XAUTHORITY
  >         DISPLAY LANG=C POSTLOG_SERVICE POSTLOG_HOSTNAME XDG_RUNTIME_DIR
  >         DYLD_INSERT_LIBRARIES=$shlib_directory/fake_eugid.dylib

- In a multi-instance configuration, you need to repeat this step for each Postfix instance.

- To revert to privileged Postfix master(8) operation, execute as the "root" user:

  >     postconf -X import_environment

- In a multi-instance configuration, you need to revert this for each Postfix instance.

### Change the "`pid`" lockfile directory ownership

- As root, execute the following command:

  >     echo '$queue_directory/pid:d:$mail_owner:-:755:ucr' \
  >         > $(postconf -xh meta_directory)/postfix-files.d/unprivileged

  NOTE: use single quotes (`'`) not double quotes (`"`).

  This adds add a file "`unprivileged`" in a subdirectory "`postfix-files.d`" under the Postfix directory configured with "`meta_directory`". The result should look like this:

  >     $queue_directory/pid:d:$mail_owner:-:755:ucr

- Execute as the "root" user:

  >     postfix set-permissions

- In a multi-instance configuration, this setting is used by every Postfix instance.

- To revert to privileged Postfix master(8) operation, delete the file "`unprivileged`" that was created above, and execute as the "root" user:

  >     rm $(postconf -xh meta_directory)/postfix-files.d/unprivileged

  >     postfix set-permissions

- In a multi-instance configuration, this reverts the "pid" directory ownership for every Postfix instance.

### Make TLS secret key files world-readable

Normally, Postfix daemons open secret key files before dropping "root" privileges. With unprivileged Postfix that is no longer possible.

- Execute the following command as "root" for each such file:

  >     chmod 644 /path/to/secret-key-file.pem

  You may need to update Let'sEncrypt etc. key management automation to ensure correct file permissions after a future key update.

- In a multi-instance configuration, you need to repeat this step for each Postfix instance.

- To revert to privileged Postfix master(8) operation, execute the following command as "root" for each of the above secret files:

  >     chmod 600 /path/to/secret-key-file.pem

  and revert the above changes to Let'sEncrypt etc. key management automation.

- In a multi-instance configuration, you need to revert this for each Postfix instance.