56. pepsi-sendmail¶
Let programs on this host send mail, through the traditional Sendmail interface.
56.1. 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: 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.
56.2. 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 |
|---|---|
|
the submission client |
|
the same (historical path) |
|
|
|
|
|
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.
56.3. 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.
56.4. 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:
|
|
|
|---|---|---|
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 |
|
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.
56.5. 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:
[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.
56.6. See also¶
pepsi-ingress, pepsi-queue, pepsi-stage-aliases, Getting started on a cheap VPS, pepsi-sendmail(1), pepsi.conf(5).