70.1.4. pepsi-stage-arc¶
ARC-verify, sign and seal in the Pepsi pipeline
- Manual section:
1
70.1.4.1.1. Name¶
pepsi-stage-arc - the ARC verification and sealing stage of the Pepsi pipeline.
70.1.4.1.2. Synopsis¶
pepsi-stage-arc [GLOBAL-OPTIONS] worker
70.1.4.1.3. Description¶
pepsi-stage-arc is a stage program run by pepsi-dispatch(1) as a persistent worker reading message ids on standard input. It implements ARC (RFC 8617): it verifies any existing inbound
ARC chain, records the resulting verdict in the row’s state.auth.arc, and
— when an ARC signing identity is configured — seals the message as our ADMD by
prepending a fresh ARC set: ARC-Authentication-Results (AAR),
ARC-Message-Signature (AMS) and ARC-Seal (AS). Sealing only prepends
header lines, so existing signatures lower in the message are undisturbed. The
row then advances to its NEXT_STAGE. The row’s state — including any
RFC 3461 state.dsn parameters — is left untouched for later stages to honour.
This work was previously done inline by pepsi-ingress(1); it is now a stage
so ingress no longer holds the ARC signing keys. Ingress verifies SPF, DKIM and
DMARC, writes the standalone Authentication-Results header, and records the
rendered result text under state.origin.ar_results; this stage adds only the
ARC set. The AAR (which, per RFC 8617, should mirror the receiver’s full
assessment) is built by feeding that stored fragment straight back in — so
SPF/DKIM/DMARC are not re-verified here, and the DNS-heavy work is done
exactly once. Only the inbound ARC chain — which ingress does not assess — is
verified by this stage, to compute the chain-validation (cv=) value and append
its own arc= line to the AAR.
70.1.4.1.4. Who sealed the chain: state.auth.arc_sealers¶
Besides the verdict, the stage records the d= of every ARC-Seal the
message arrived carrying, as a JSON array under
state.auth.arc_sealers — lower-cased, stripped of any trailing root dot,
de-duplicated, and ordered by instance so the ADMD closest to the originator
comes first. Our own ARC_DOMAIN never appears: the list is read from the
message before this stage’s own set is rendered, so a seal we added cannot
satisfy a rule about intermediaries.
This exists because state.auth.arc cannot answer the question a receiver
actually has. RFC 8617 §8.4 is explicit that a valid chain conveys no
trustworthiness — it establishes only that the ADMDs named in it handled the
message, and deliberately leaves to local policy the question of whether those
ADMDs are honest. A receiver that wants to excuse a forwarding hop’s broken SPF
or DKIM therefore has to name the hops it accepts, and it needs somewhere to read
the hops from. That is this key; the sealer_domain rows of
pepsi-stage-check-whitelist(1) are the policy written against it.
The array is written whether or not the chain validated — an unvalidated d=
is a claim, and the record of what was claimed is what an operator diagnoses a
rejected message with. Anything consuming it must therefore first require
state.auth.arc = pass; check-whitelist does, discarding the whole list
otherwise, so a chain anybody minted cannot name a trusted sealer.
70.1.4.1.5. Chain validation and cv=¶
RFC 8617 §5.2 defines three chain-validation outcomes, and the cv= tag of the
ARC-Seal this stage adds reports which one applies to the message as it
arrived:
cv=noneNo ARC chain was present — this ADMD is the first to seal the message.
cv=passA chain was present and every set validated.
cv=failA chain was present and did not validate.
The distinction matters more than it looks. cv=none is an assertion about
provenance, not merely an absence: it tells the next receiver that nothing
upstream had already vouched for this message. Emitting it over a chain that was
present but broken would launder that chain — the downstream validator would see
an apparently pristine i=1 set and no evidence that anything had been
tampered with.
Pepsi therefore seals cv=fail for every inbound chain that fails to
validate, including the cases where the chain cannot be read at all: an
ARC-Seal/ARC-Message-Signature/ARC-Authentication-Results header that
does not parse, a chain longer than the 50 sets RFC 8617 permits, and a chain
with unequal numbers of the three header types. The new set continues the
chain’s instance numbering rather than restarting it at i=1.
The one exception is a chain whose most recent ARC-Seal already says
cv=fail. Per RFC 8617 §5.1.1 a failed chain cannot be repaired and that seal
is the last one, so this stage adds no set at all and the message is forwarded
with the chain as it stands.
The session context the seal needs — the authserv-id, the rendered
Authentication-Results fragment, the connecting client IP and the announced
HELO/EHLO name — is read from the SMTP-origin metadata ingress recorded
under the row’s state.origin; the envelope sender comes from the mail_from
column. This is how information is passed from ingress to this stage.
The ARC signing key is the [pepsi] ARC_DOMAIN domain’s DKIM key under
KEY_DIR (provisioned by pepsi-setup(1)), signed with the single
algorithm named by [pepsi] ARC_ALGORITHM (ARC permits one signature per hop).
ARC applies only to mail we receive and forward: it preserves an upstream
ADMD’s authentication across our hop. It must therefore never touch mail we
originate — our own authenticated submissions (state.local_origin), which
are authenticated as the author domain by pepsi-stage-dkim-sign(1) instead.
Sealing such a message would only publish the submission’s own SPF/DKIM/DMARC
verdicts (typically an SPF fail for a roaming client not listed in our SPF
record) inside the ARC set. The stage therefore skips any
state.local_origin message it is handed, advancing it unsealed; this makes the
stage safe wherever it is placed. On a host that both receives and submits, place
it on the inbound branch only, after the state.local_origin split — so it
sees only received mail, ahead of SRS envelope rewriting and any other
transformation. On a receive-only host it is naturally the initial stage
([stage-init]).
Sealing is fail-open: if the origin context is missing, the [pepsi]
configuration or ARC keys are unavailable, or sealing errors, the message is
advanced unsealed with a warning rather than being held up (the ARC verdict is
still recorded whenever it could be computed).
70.1.4.1.6. Configuration¶
The stage is wired and configured through its own [stage-<name>] section
(PROGRAM = pepsi-stage-arc, a required NEXT_STAGE). Its options — the DNS
settings (DNS_SERVERS, DNS_TIMEOUT) and the ARC-set signature parameters
(HEADER_CANONICALIZATION, BODY_CANONICALIZATION, COVER_BODY,
SIGNATURE_EXPIRATION_DAYS, SIGNED_HEADERS) — together with the shared
[pepsi] signing identity (ARC_DOMAIN, ARC_ALGORITHM, KEY_DIR,
DKIM_SELECTOR) are documented in pepsi.conf(5).
70.1.4.1.7. State¶
Inputs: state.local_origin — when true the message is one of our own
submissions and is advanced untouched (no ARC). Otherwise state.origin — the
SMTP-origin metadata ingress recorded. The
authserv_id is required to reproduce the AAR identity (without it the stage
advances unsealed, fail-open); ar_results carries ingress’s rendered
Authentication-Results fragment, reused verbatim in the AAR so SPF/DKIM/DMARC
need not be re-checked; remote_ip and helo refine the ARC-verification
context.
Outputs: the stage overwrites state.auth.arc with the re-evaluated chain
verdict (replacing the none ingress seeds there), writes
state.auth.arc_sealers (see below) and prepends the ARC set to
the headers column; the rest of state — including the other
state.auth verdicts and state.dsn — is preserved unchanged. The state
layout is described in pepsi.conf(5).
Transitions: always advances to NEXT_STAGE — after prepending the ARC
set, after merely recording the verdict in state.auth.arc when no signing
identity is configured, or immediately (untouched) for a state.local_origin
message, which is never ARC-processed. There is no branch; the stage never
pauses, fails, reroutes or finishes.
70.1.4.1.8. Commands¶
Run as a worker, the program verifies and seals each message and advances it.
70.1.4.1.9. Global Options¶
- -c FILE, –config FILE
Read the configuration from FILE instead of the default search path.
- -L LOGLEVEL, –log LOGLEVEL
Set the logging verbosity (
error,warn,info,debug,trace; defaultinfo).- -h, –help
Print a usage summary and exit.
- -V, –version
Print the version and exit.
70.1.4.1.10. Exit Status¶
- 0
The message was sealed (or advanced unsealed, fail-open) and moved to its next stage, or the subcommand succeeded.
- 1
An error occurred (misconfiguration, the message is not in the expected state, or a database error). The reason is written to the log.
70.1.4.1.11. See Also¶
pepsi-config(1), pepsi-ingress(1), pepsi-stage-srs(1), pepsi-stage-dkim-sign(1), pepsi-dispatch(1), pepsi.conf(5), pepsi.state(7), pepsi-setup(1)
70.1.4.1.12. Bugs¶
Report bugs to the Pepsi issue tracker.