85.1.38. pepsi-stage-secure-link¶
hold a message that could not be encrypted, and mail the link
- Manual section:
1
85.1.38.1.1. Name¶
pepsi-stage-secure-link - the secure-link fallback stage of the Pepsi pipeline.
85.1.38.1.2. Synopsis¶
pepsi-stage-secure-link [GLOBAL-OPTIONS] worker
85.1.38.1.3. Description¶
pepsi-stage-secure-link is a stage program run by pepsi-dispatch(1) as a persistent worker reading message ids on standard input.
It is the far end of [stage-encrypt] ON_NO_KEY = secure-link. When
pepsi-stage-encrypt(1) must encrypt a message and the recipient publishes
no usable key, it routes the message here instead of sending it in the clear or
bouncing it. This stage then takes the message off the wire entirely:
it generates a PIN, derives the URL token from the queue row (see Retries) and a content key from the PIN and the server-side pepper, and seals the whole message under it;
in one transaction it stores only the AEAD ciphertext in
pepsi.secure_messageand queues a notification mail to the recipient carrying the link, and — under the defaultPIN_DELIVERY = sender— a second mail to the sender carrying the PIN, for the sender to relay by telephone or text message;it deletes the queue row.
The row is deleted rather than advanced because advancing it would transmit the
cleartext to the very recipient who must not receive cleartext. The message’s
continuation is the notification, which is an ordinary outbound message injected
at [pepsi-secure-link] NOTIFY_STAGE and signed and relayed like any other. The
stage is therefore terminal and has no NEXT_STAGE.
A message with several keyless recipients produces one stored message per recipient, each with its own token, PIN and ciphertext, so one recipient’s PIN never opens another’s copy and the recipient list is not disclosed to any of them.
85.1.38.1.4. Configuration¶
The stage’s [stage-<name>] section carries only PROGRAM. Everything else
lives in the global [pepsi-secure-link] section (see pepsi.conf(5)),
deliberately: the expiry window, the PIN length and the lockout are not
behavioural knobs a correspondent may edit by e-mail through
pepsi-stage-edit-settings(1), and a global section is out of reach of the
per-address override layer.
85.1.38.1.5. Example¶
[stage-encrypt]
PROGRAM = pepsi-stage-encrypt
ENCRYPT = required
SECURE_LINK_STAGE = secure-link
NEXT_STAGE = srs
[stage-secure-link]
PROGRAM = pepsi-stage-secure-link
[pepsi-secure-link]
BASE_URL = https://secure.example.org
NOTIFY_STAGE = srs
# PEPPER is written to secrets.d/pepsi-secure-link.secret by pepsi-setup.
85.1.38.1.6. Retries¶
A stage body can run more than once for the same queue row: a worker that dies
after its side effects but before the commit, a pepsi-dispatch(1) restart
re-claiming a running row, an operator requeue, and a failure part-way
through, which is retried (see Failure). The per-recipient loop is not atomic
— a failure at the k-th recipient leaves every recipient before it already
stored and notified — but each recipient is: the stored copy and its
notifications are committed together, by one call of the
secure_message_store SQL function, or not at all.
So the secure-message token is derived from the queue row, as a keyed hash of
the row’s token and the recipient’s position under the deployment’s PEPPER,
rather than drawn at random. A repeated pass recomputes the same token, finds
that recipient’s pepsi.secure_message row already present, logs a warning and
leaves it alone: no second copy, no second link to the recipient and no second
PIN to the sender. The token is keyed rather than merely hashed because it is the
link URL — anything an outsider could recompute would be a link they could guess.
Only the token is derived. The PIN stays random on every pass and is stored nowhere, which is the property that makes a stolen database and even a stolen server useless (see pepsi-secure-link(1)); a PIN reproducible from the pepper and the row’s token would be one the operator could recompute. That is why the copy and its announcement must be one commit: a copy stored without its notifications would carry a PIN that no longer exists anywhere, and no retry could announce it. As it is, a stored copy is always an announced one, and a retry that finds it has nothing left to do for that recipient.
The one thing a retry can repeat is PIN_DELIVERY = command: the command runs
before the commit (so a gateway that is down leaves nothing stored), and if the
commit then fails, the next pass sends a new PIN; the earlier one opens nothing,
since nothing was stored under it.
85.1.38.1.7. Failure¶
Everything that can fail is done before the queue row is deleted, and nothing that fails ever sends the message on: a message that reached this stage is by definition one the operator said must not go out unprotected.
A
[pepsi-secure-link]section that does not parse, has noBASE_URLorPEPPER(or a pepper that cannot be read), or noNOTIFY_STAGEmakes the worker refuse to start (exit 78): the dispatcher holds the queue until the configuration is fixed, instead of each message failing on its own.A fault of this host — a notification template that cannot be read,
PIN_DELIVERY = commandfailing to start, failing or not finishing within 30 seconds, the database — is retried, after one minute and then at doubling intervals up to an hour, until the message has been queued longer than the stage’s MAX_LIFETIME (default120 h); then it goes to the stage’s BOUNCE_STAGE, or is failed without one (see pepsi-dispatch(1)).A message with the null envelope sender is failed at once: there would be nobody to give the PIN to.
85.1.38.1.8. Exit status¶
The worker exits 0 on a clean end of input, and 78 when it refuses to
start because [pepsi-secure-link] is not usable (see Failure).
Per-message failure is reported through the queue, not the exit status.
85.1.38.1.9. See also¶
pepsi-secure-link(1), pepsi-stage-encrypt(1), pepsi-httpd(1), pepsi-dispatch(1), pepsi.conf(5), pepsi.state(7)