.. This file is part of PEPSI. Copyright (C) 2026 Pepsi contributors PEPSI is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. ============== pepsi-sendmail ============== *Let programs on this host send mail, through the traditional Sendmail interface.* Role ==== ``pepsi-sendmail`` is the program Debian installs as ``/usr/sbin/sendmail``. It reads a message on standard input and hands it to ``pepsi-ingress`` over a local UNIX-domain socket, so that software which expects an MTA to provide that path — ``cron``, ``mail(1)``, ``git send-email``, ``logwatch``, PHP's ``mail()`` — can send through Pepsi unchanged. Reference: :manpage:`pepsi-sendmail(1)`. Without it, a host running Pepsi has no local submission path at all: those programs can only reach port 25, where an unauthenticated client is not a local origin and is refused with ``550 5.7.1 Relaying denied`` for any recipient outside a served domain. How Debian arbitrates the sendmail path ======================================= Debian does **not** use ``update-alternatives`` for this. Policy §11.6 has every mail transport agent declare:: Provides: mail-transport-agent Conflicts: mail-transport-agent Replaces: mail-transport-agent so exactly one MTA can be installed at a time and it simply owns the paths. The ``pepsi`` package declares that triple and ships the interface as symlinks to the unified ``pepsi`` binary, which dispatches on ``argv[0]`` — the same technique Postfix and exim use: =============================== ========================================== Path Behaves as =============================== ========================================== ``/usr/sbin/sendmail`` the submission client ``/usr/lib/sendmail`` the same (historical path) ``/usr/bin/mailq`` ``sendmail -bp`` ``/usr/bin/newaliases`` ``sendmail -bi`` (a no-op for Pepsi) ``/usr/sbin/rmail`` the submission client (UUCP delivery) =============================== ========================================== A source ``make install`` deliberately does not claim these paths — they belong to whatever MTA the system already has, and taking them over silently would stop its mail delivery with no warning. Pass ``INSTALL_MTA_LINKS=yes`` to do it on purpose. Why it needs no privilege ========================= This is the part worth understanding, because it differs from every other sendmail implementation. Postfix's ``sendmail`` is setgid ``postdrop`` so that it may write into the ``maildrop`` spool directory. Pepsi's queue is a PostgreSQL table reached by peer-authenticated roles, so the equivalent privilege would have to be *setuid* rather than setgid — a much larger thing to hand to a program that parses attacker-influenced message headers. Pepsi avoids the question entirely by moving the identity decision to the server. ``pepsi-ingress`` reads the connecting process's user id from the kernel (``SO_PEERCRED``, enabled per listener with ``AUTH_PEERCRED``), resolves it to a login, and looks that login up in ``USERNAME_MAP`` — exactly as it maps a SASL username arriving on the submission port. The RFC 6409 §6/§8.1 submission-identity rules then apply unchanged. So ``pepsi-sendmail`` is an ordinary unprivileged program (and is folded into the shared ``pepsi`` binary like any other), while the guarantees are *stronger* than a privileged implementation could offer: * A caller can only ever send **as itself**. The kernel supplies the identity; nothing the client says is trusted. * The socket can be world-writable, because opening it grants nothing. Deny an account by mapping it to no address in ``USERNAME_MAP``, not with permissions. * Nothing on the network gains anything. Compared with trusting loopback =============================== The alternative — putting ``127.0.0.0/8 ::1/128`` in the MX listener's ``MYNETWORKS`` — authenticates a *network position* instead of a user: .. list-table:: :header-rows: 1 :widths: 30 35 35 * - - ``loopback`` - ``sendmail`` * - Who is authenticated - anyone who can open a TCP connection - the specific account, per the kernel * - Sender enforcement - none — any process may send as anyone - ``USERNAME_MAP``, as for SASL * - Reachable from the network - no (loopback only), but the listener is a network listener - no — a filesystem socket * - Self-relay loop risk - yes: a message relayed to this host re-enters as a submission - no Both remain available, but they are no longer symmetrical choices. The socket is always served, so ``pepsi-setup --wizard`` does not ask about it; the one question left is whether to *additionally* trust loopback, which is worth answering yes to only for software that speaks SMTP to ``127.0.0.1`` and cannot be pointed at the socket. Presenting the two as alternatives invited operators to pick the weaker one. Configuration ============= The client reads ``[pepsi-sendmail] SOCKET`` (default ``/run/pepsi/submission.sock``) — and so does the server. ``pepsi-ingress`` serves that socket unconditionally, synthesising the listener from that one option, so there is no listener section to write and nothing that can disagree with anything else: .. code-block:: ini [pepsi-sendmail] SOCKET = /run/pepsi/submission.sock # ... and that is all. No [pepsi-ingress-listener-*] section. Earlier versions made this an optional listener section the wizard emitted only when asked, so the facility depended on three things agreeing: the section existing, its ``UNIXPATH`` matching ``SOCKET``, and the runtime directory being writable by the server. Each was easy to get wrong and the symptom was always the same — cron mail silently stops. A host running an MTA is expected to provide ``/usr/sbin/sendmail``, so it is no longer a question. ``SOCKET = none`` switches it off for the cases that genuinely want that (a container, an appliance), and failing to bind it is fatal rather than degraded: a server serving only *some* of what it was asked to serve is the failure being removed. Under systemd the descriptor is inherited from ``pepsi-ingress.socket``, matched **by the address it is bound to** rather than by an ``FD_INDEX``. That matters because an index is a position among the unit's ``ListenStream=`` entries, so it shifts whenever a port is added or removed; asking the kernel what each passed descriptor is bound to cannot drift out of step with anything. Off systemd, or for a ``SOCKET`` no unit binds, ``pepsi-ingress`` binds the path itself. Socket activation here is not about privilege — this is not a privileged port — but about the directory. ``/run/pepsi`` is shared with ``pepsi-httpd`` and the setup-apply doorbell, and systemd chowns a ``RuntimeDirectory=`` to the declaring unit's own ``User=`` on every start, so an unprivileged ``pepsi-ingress`` creating its own socket there breaks as soon as another of them restarts. Bound by the ``.socket`` unit the node is created by systemd as root, so the server needs no write access to the directory at all — and the socket stays available across a service restart, so a concurrent ``/usr/sbin/sendmail`` waits rather than failing. The synthesised listener is ``MODE = plain``, ``SUBMISSION = yes`` and ``AUTH_PEERCRED = yes``, on a socket with mode ``0666``; none of that is configurable, because none of it is a choice. ``SUBMISSION = yes`` brings the RFC 6409 header fixups (a missing ``Date:`` or ``Message-ID:`` is supplied). A UNIX-domain submission listener is exempt from the usual "submission requires TLS" rule: a filesystem socket has no transport an attacker could observe, and its authentication comes from the kernel rather than the wire, so a certificate would protect nothing. For a listener that *is* configured with ``SERVE = systemd``, that exemption is checked once the descriptor is resolved rather than at parse time — the socket's family lives in the ``.socket`` unit — and ``pepsi-ingress`` refuses to start if it turns out to be a network socket. To give the socket different options — a group, a stricter mode, no peer credentials — write a listener section for the same path; it replaces the built-in one rather than colliding with it. Because submissions are authenticated they carry ``state.local_origin``, so the pipeline's ``pepsi-stage-if`` entry stage routes them down the outbound branch and they are DKIM-signed as the author's domain. See also ======== :doc:`pepsi-ingress`, :doc:`pepsi-queue`, :doc:`pepsi-stage-aliases`, :doc:`../getting-started`, :manpage:`pepsi-sendmail(1)`, :manpage:`pepsi.conf(5)`.