I've neeb thinking about making the infrastructure more reusable
and came up with the sketch below. It is missing some fine details
like who waits on what, but the over-all picture looks promising.

======

Background 

Three Postfix client programs (postdrop, postqueue, postlog) communicate
in a controlled manner with Postfix core daemon processes. The client
programs enforce an admission policy (for example, who can list or
manipulate the queue), a low-level data format policy, and limit how
many concurrent requests an attacker can make; they do all that in code
that intentionally runs outside the Postfix core (i.e. with non-postfix
privilege).

The daemon-side communication endpoints are either a UNIX-domain socket
(content in messages) or a directory (content in files). Access to these
endpoints is restricted with directory permissions. The three client
programs are set-gid postdrop and can access the communication endpoints;
an attacker cannot directly access the endpoints, and therefore cannot
bypass an admission policy, data format policy, or per-user concurrency
limit.

End of background

Problem statement

In a "No new privs" world, the set-gid feature becomes unavailable,
and must be replaced with authenticated IPC (inter-process
communication). This may be facilitated with systemd-managed sockets
that launch client programs with suitable privileges.

A naive implementation would require three systemd sockets: each socket
would have its own configuration for what program to run (postdrop,
postlog or postqueue), what privileges to use, etc. But that is not all:
a naive implementation would require three such sockets for every Postfix
instance in a multi-instance configuration. I think that we should not
do that.

Proposed solution

Instead of three systemd-managed sockets per Postfix instance, I propose
a single systemd-managed socket for all Postfix client programs in all
Postfix instances as illustrated below.

    postdrop --> \                       / --> postdrop

    postlog --> systemd socket --> postmux --> postlog

    postqueue --> /                      \ --> postqueue

    Yes, that's the same client program file on the left and on the
    right side. There is some pseudocode below for how this is done.

In this architecture, systemd manages one socket and launches a 
postmux process that runs with postdrop UID and GID privilege, and that
receives the left-side client's stdin, stdout, stderr file descriptors
and the (postdrop, postqueue, or postlog) command-line and environment
array (argv and envp, respectively).

The command-line array contains all the information that is needed to
identify the right-side program file.

The command-line and environment arrays contain all the information that
is needed to determine the per-Postfix-instance config_directory.

Implementation details

Depending on the value of the zeroth command-line argument ('postdrop',
'postqueue', or 'postlog') postmux launches the corresponding client
program with the received command-line and environment arrays. The client
process inherits from postmux the postdrop user and group ID privilege
and the received stdin, stdout, and stderr.

Each (postdrop, postlog, postqueue) program has logic like this (where
argv = command-line array, envp = environment array):

    if running with postdrop group ID {
        RUN EXISTING CODE (with minor tweaks to determine the UID for
        admission control)
        in postdrop, create a new queue file record to store the peer
        UID; the pickup(8) daemon will use that UID instead of the
        maildrop queue file owner UID.
    } else {
#ifdef NONEWPRIVS
        // new code, running as systemd socket client 
        connect to systemd-managed socket 
        receive and validate postmux protocol name from systemd socket
        send (stderr, stdin, stdout) file descriptors over systemd socket 
        send size plus serialized argv and envp over systemd socket 
        wait until the systemd-managed socket is closed
#else
        fatal need setgid permission
#endif
    }

The postmux implementation goes like this (where argv = command-line
array, envp = environment array):

    send postmux protocol name over stdout
    receive (stderr, stdin, stdout) file descriptors over stdin
        (but do not yet replace postmux's stdin and stdout)
    receive size plus serialized argv and envp over stdin (hard limit on size)
    if argv[0] in ['postdrop', 'postqueue', 'postlog'] {
        build envp (environment array) with only these:
            MAIL_CONFIG (value of config_directory) 
            POSTDROP_UID (peer credential from systemd socket)
        replace (stdin, stdout) with the received (stdin, stdout)
        execve(command_directory/argv[0], argv + 1, envp) 
        fatal could not execute command_directory/argv[0]
    } else {
        fatal invalid argument
    }

That's a rough sketch. It adds a small amount of client code that is 100%
reusable between postdrop, postqueue, or postlog, and it adds a small
postmux program.

        Wietse
_______________________________________________
Postfix-users mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to