85.1.56. pepsi-helper-dot-forward

run a single user’s ~/.forward as that user

Manual section:

1

85.1.56.1.1. Name

pepsi-helper-dot-forward - privileged helper that processes one local user’s ~/.forward after dropping to that user.

85.1.56.1.2. Synopsis

pepsi-helper-dot-forward TARGET-UID [–pipe] [–file] < message

85.1.56.1.3. Description

pepsi-helper-dot-forward is a minimal, security-hardened helper that runs exactly one local user’s ~/.forward file. It exists so that a trusted but unprivileged caller (a member of the pepsi-forward group, in practice pepsi-stage-dot-forward(1)) can have any local user’s ~/.forward processed as that user — and do nothing else.

The helper is installed setuid-root, owned root:pepsi-forward with mode 4750. Only members of the pepsi-forward group may execute it; when they do, the kernel runs it with an effective uid of root. Given the numeric uid of the target user as its sole positional argument, and the raw message on standard input, the helper:

  1. resolves the target user’s passwd entry, refusing uid 0 / the login root and any uid below /etc/login.defs UID_MIN (default 1000);

  2. fully and irreversibly drops to the target user — installing the user’s supplementary groups, then the gid, then the uid (real, effective and saved) — and verifies that root cannot be regained, exactly as pepsi-helper-maildir-writer(1) and pepsi-helper-auto-pay(1) do;

  3. refuses to act — exit 3 — when the home directory, or the ~/.forward itself, is not owned by the target uid or is group- or world-writable (mode & 0o022). This is the sendmail/postfix-local rule: the file names |command and /file directives the helper runs as that user, so anyone else who can write the directory could plant one. Note this is checked on the home directory before the ~/.forward is looked for, so on a host with group-writable homes the account is refused even when it has no ~/.forward at all;

  4. if the user has no ~/.forward file, exits 1 (the stage then proceeds to its next stage);

  5. otherwise reads ~/.forward and acts on each non-empty, non-# line as the user: a bare address is collected as a forwarding address, and one written \name is collected with its marker, which the calling stage reads as sendmail’s “deliver to this name and expand it no further”; |command pipes the message to /bin/sh -c command (only with –pipe); /absolute/path appends the message to that file (only with –file). A directive whose kind was not enabled, or whose execution fails, is a failure. A |command is run with a sanitised environment — HOME and USER/LOGNAME set to the target user, a fixed PATH, and the shell-hijacking variables IFS/BASH_ENV/ENV/CDPATH removed — so the inherited delivery-agent environment cannot influence the command.

A |command runs in its own process group and is given 300 seconds; on expiry the entire group is SIGKILLed (the group, because a pipe directive routinely forks) and the run is reported as a failure. A /path directive opens the target with O_NOFOLLOW|O_NONBLOCK and refuses anything that is not a regular file, so a FIFO or a symlink cannot block the helper indefinitely. Without these bounds any local account could wedge a dispatcher worker for the lifetime of the pipeline simply by writing |sleep infinity in its ~/.forward.

A directive may be written double-quoted — the form procmail’s and maildrop’s documentation tells users to use, "|IFS=' ' && exec /usr/bin/procmail -f- || exit 75 #bob" — and one line may carry several comma-separated entries, as the alias-database right-hand-side grammar that .forward inherits permits. One layer of surrounding double quotes is removed before the directive is classified, and a comma inside double quotes does not split.

The message is read from standard input only when a |pipe//file directive that consumes it may run (i.e. when –pipe or –file is given).

85.1.56.1.4. Arguments

TARGET-UID

The numeric user id whose ~/.forward is processed. It must have a passwd entry; uid 0 / the root account and any uid below /etc/login.defs UID_MIN are refused.

–pipe

Permit |command directives (the stage passes this when its ALLOW_PIPE is on).

–file

Permit /path file-append directives (the stage passes this when its ALLOW_FILE is on).

85.1.56.1.5. Exit Status

0

The ~/.forward was processed successfully. The forwarding addresses it named (possibly none) are written one per line to standard output.

1

The user has no ~/.forward. The caller proceeds to its next stage.

2

Processing failed — a pipe command exited non-zero (other than 75) or was killed by a signal, a file could not be written, or a disabled directive was requested. A human-readable diagnostic is written to standard output for inclusion in the bounce. This is a permanent failure of that recipient.

3

An operational failure of the host, not of the delivery: the helper is not installed setuid-root, the target uid has no passwd entry or the passwd lookup itself failed (an NSS/LDAP outage), the account is root or below UID_MIN, the home directory is not absolute, or the home directory or the ~/.forward itself is not exclusively the target user’s (not owned by them, or group-/world-writable). The diagnostic goes to both standard output and standard error. The caller must treat this as retryable — the message stays queued rather than being bounced for a condition the operator can fix.

The unsafe-home case is a property of the account rather than of the message: with chmod 0775 ~alice every message to alice stops here and stays queued, and none of them reach local delivery. chmod g-w,o-w on the home directory (and on ~/.forward) is the fix.

A |command that asks to be tried again also exits 3: one that exits 75 (EX_TEMPFAIL, what procmail and maildrop report for a locked mailbox or a full disk), one that runs past the 300-second limit, and one that cannot be started at all. The command’s own diagnostic goes to both output streams. Directives earlier in the file have already acted by then and act again on the retry, as they would under any delivery agent that defers part of the way through a .forward.

When invoked by a user who is not a member of the pepsi-forward group, the kernel refuses to execute the binary at all; the helper itself never runs.

85.1.56.1.6. Security

Membership of the pepsi-forward group is the access-control gate. The helper runs arbitrary commands and writes arbitrary files only as the unprivileged target user, never as root: it drops privilege completely before reading the ~/.forward or touching any of the user’s files, and refuses to act for the root account.

85.1.56.1.7. See Also

pepsi-stage-dot-forward(1), pepsi-helper-maildir-writer(1), pepsi-dispatch(1), pepsi.conf(5)

85.1.56.1.8. Bugs

Report bugs to the Pepsi issue tracker.