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

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

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:

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.

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