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:

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

      o 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:

      o 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:

          o 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.

          o 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:

          o 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.

          o 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.

          o 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.

