3. Installation¶
Pepsi can be installed two ways. Building from source out of the Cargo
workspace gives you the binaries and leaves it to you to create the service
accounts, set the privileged bits and lay out the runtime directories. The
prebuilt Debian package does all of that for you — its postinst creates
the accounts, groups, directories, privileged-binary overrides and the database
— so only the site-specific bootstrap (editing the config and running
pepsi-setup) is left to do.
Pick the tab for the method you are using. Everything after it — provisioning the schema/keys/DNS, running the services, the privilege model and the schema lifecycle — is the same for both.
3.1. Prerequisites¶
PostgreSQL — one database, into which Pepsi installs the single
pepsischema. All components connect to the same database. (The Debian packageDependsonpostgresqland pulls it in for you.)PostgreSQL’s
postgresql-contrib— optional, and only for mailing lists. It carries thepg_trgmextension, which the mailing-list archive uses for substring and fuzzy search: finding a hostname, a config key, a line from a traceback, or a name the searcher has misspelled. A stemming dictionary throws exactly that material away, so full-text search cannot answer those queries at all.A site without it installs and runs normally. The extension is created in a guarded block, so its absence is a warning rather than a failed install; the archive then has full-text search only, and
pepsi-list checksays which mode the site is in. Installing it later and re-runningpepsi-setup runenables it, after which the existing archive needspepsi-archive reindexto populate the column the index reads.The Debian package
Recommendsit, so a defaultapt installpulls it in and a site that hosts no lists can remove it.For real mail flow: public DNS you control for each served domain (to publish DKIM, SPF and MTA-STS records), and the ability to bind the privileged SMTP port 25 (and 465/587 for submission). A development build can run entirely on unprivileged ports and a throwaway database.
3.2. Installing Pepsi¶
Build the workspace, place the binaries and SQL with make install, and
create the system accounts and privileges yourself (the same ones the
package would create — see Users, groups and privileges below).
Additional prerequisites
A Rust toolchain with
rustc1.93 or later (the floor is set by the vendoredtaler-common;./configurewarns about an older one), plus the native build dependencies of the crypto and Kerberos crates:pkg-config,cmake, libclang, nettle, GMP and the Kerberos development headers (on Debian:libclang-dev,nettle-dev,libgmp-dev,libkrb5-dev).The vendored GNU Taler Rust submodule, fetched before the first build:
git submodule update --init --recursive
Building
Pepsi is a Cargo workspace whose root package (pepsi) owns every
binary under src/bin/; the pepsi-* member crates are libraries the
thin binaries call into. A development build produces one executable per
program:
cargo build --workspace
A release build instead links most programs into a single multi-call
binary, pepsi, which selects the program to run from the name it is
invoked under (argv[0]). This is selected by the unibin Cargo
feature (which the make build target enables for you); the default
multibin feature keeps the per-program binaries used for development and
the test suite:
make build # == cargo build --release --locked \
# --no-default-features --features unibin,crypto-nettle
Either way the per-program names are what you run; the release layout
installs them as symlinks to the one binary (see Installing). Some
programs are not folded in and stay standalone; the authoritative list
is the Makefile’s STANDALONE_BINARIES. Two reasons put a program there:
it carries its own setuid or setgid bit, which one shared binary cannot do per program — the local-delivery,
~/.forwardand wallet helpers and their calling stages, the smarthost relay,pepsi-quota,pepsi-whitelist, andpepsi-stage-encryptandpepsi-stage-decrypt(both setuidpepsi-crypto, so they can reach the private key material) — or a site may give it one (pepsi-keys);it is deployed or invoked separately —
pepsi-setup,pepsi-config,pepsi-stage-detect-language(its language models are large; that binary is itself multi-call, and a second$PATHsymlink,pepsi-detect-language, runs the offline diagnostic tool out of it),pepsi-telemetry(which ships in its own package and runs on a separate host) andpepsi-helper-mailbox-scan(whichpepsi-whitelistexecutes by name).
The programs this chapter refers to are listed below. This is a
selection, not an inventory: the authoritative lists are the Makefile’s
FOLDED_BINARIES and STANDALONE_BINARIES (BINARIES is derived
from those two and is not itself editable).
pepsi-ingress— inbound SMTP serverpepsi-dispatch— stage dispatcherpepsi-stage-arc,pepsi-stage-srs,pepsi-stage-encrypt,pepsi-stage-decrypt,pepsi-stage-dkim-sign,pepsi-stage-bounce,pepsi-stage-relay-to-internet,pepsi-stage-relay-to-smarthost,pepsi-stage-discard— the stagespepsi-setup— provisioning/bootstrappepsi-queue— queue inspection/repairpepsi-status— pipeline health summarypepsi-sendmail— local submission (installed as/usr/sbin/sendmailby the Debian package)
The quality gates the project keeps green are:
RUSTFLAGS="-D warnings" cargo build --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all -- --check
cargo test --workspace
cargo deny check
make docs
cargo test --workspace needs a reachable PostgreSQL, because the
dispatcher-lifecycle crate spawns the real dispatcher against a database.
make check is the entry point that gates it – skipping that suite
rather than failing when no database is usable – and it also runs
cargo deny check; see Other make targets below.
Installing
A bare build only produces binaries; Pepsi also ships SQL migrations and a
sample configuration that must be placed on disk. The Makefile wraps
both — run it as root so it can also apply the privileged-binary bits
(it prints the exact chown/chmod to run by hand otherwise):
make install PREFIX=/usr/local SYSCONFDIR=/etc DESTDIR=
Better still, run ./configure first and let it resolve the directory
variables into config.mk (which the Makefile reads before its own
defaults, so what configure decided wins). Every directory derives from
--prefix, so naming a prefix and nothing else relocates the whole
installation — binaries, SQL, config.d defaults and message templates —
below it:
./configure --prefix=/opt/pepsi # templates at /opt/pepsi/share/pepsi/templates
make && make install
--sysconfdir is the one exception, because a configuration file has to
be where the system looks for it: it is /etc when no --prefix was
given (and when --prefix=/usr was, since /usr/etc exists on no
system), and $(PREFIX)/etc otherwise. Pass --sysconfdir explicitly
to override either way.
./configure --help lists everything it accepts. Besides the GNU
directory variables (--prefix, --exec-prefix, --bindir,
--sbindir, --libexecdir, --sysconfdir, --datarootdir,
--datadir, --mandir, --infodir, --docdir) it takes:
--with-templatedir=DIRWhere the message templates go; must match
[pepsi] TEMPLATE_DIR. Defaults to$(DATADIR)/templates.--with-systemdunitdir=DIR/--with-tmpfilesdir=DIR/--with-sendmail-libdir=DIRThe unit files, the
tmpfiles.dsnippet, and where the legacy/usr/lib/sendmaillink goes.--with-maildir-group=NAME,--with-forward-group=NAME,--with-wallet-group=NAME,--with-token-group=NAME,--with-dispatch-group=NAME,--with-crypto-user=NAME,--with-whitelist-user=NAMEThe site names of the accounts and groups that gate the privileged binaries (see Privileged binaries below). They default to
pepsi-maildir,pepsi-forward,pepsi-wallets,pepsi-token,pepsi,pepsi-cryptoandpepsi-whitelist.--enable-docs/--disable-docsWhether the man pages and the info manual are rendered from source or taken pre-built (what a release tarball ships). The default is
auto: on whensphinx-build,makeinfoand Python withdocutilsare all present.--enable-mta-links/--disable-mta-linksWhether
make installclaims/usr/sbin/sendmailand the rest of the Debian mail-transport-agent interface. Off by default, so a source install cannot silently displace the system’s existing MTA; the Debian package passesINSTALL_MTA_LINKS=yesbecause itsConflicts: mail-transport-agentalready guarantees exclusivity.--with-crypto-backend=FEATUREThe sequoia crypto backend Cargo feature (default
crypto-nettle).
CARGO=, INSTALL=, SPHINXBUILD=, MAKEINFO=,
INSTALL_INFO=, RST2MAN= and PYTHON= are accepted as
VAR=VALUE arguments or from the environment. Options this build has
no use for (--libdir, --localstatedir, --build, --host,
…) are accepted and ignored, so a generic packaging driver can call the
script unchanged; only a missing cargo is a hard error.
make install performs the following steps:
install-binBuilds the release (unified) binary and installs it to
$(DESTDIR)$(PREFIX)/libexec/pepsi/pepsi, then creates one$(DESTDIR)$(PREFIX)/binsymlink per folded program name pointing at it (e.g.pepsi-ingress -> ../libexec/pepsi/pepsi). The seventeen standalone programs named in the Makefile’sSTANDALONE_BINARIESare installed as their own$(DESTDIR)$(PREFIX)/binexecutables. Because each symlink still lives in$(PREFIX)/bin(on$PATH), the runtimeDATADIRderivation below is unaffected.install-dataCopies the SQL files from
pepsi-setup/db/to$(DESTDIR)$(DATADIR)/sql(whereDATADIR = $(PREFIX)/share/pepsi).pepsi-setupreads these when installing the schema; the defaultSQL_DIRis${DATADIR}/sql. Also installscontrib/hosters.txtas$(DATADIR)/hosters.txt, the public-hoster listpepsi-whitelist import --auto-wildcardnever proposes a wildcard for.install-templatesCopies the message templates (
<name>.<lang>.body— the anti-spam payment request, the bounce bodies, the mailing-list notices and the other automatic replies) to$(DESTDIR)$(TEMPLATEDIR), which defaults to$(DATADIR)/templates. The binaries’[pepsi] TEMPLATE_DIRdefault is${DATADIR}/templatesand resolves through the same runtime derivation, so the two ends agree under any prefix without the configuration naming a path.install-configInstalls the reference
pepsi.conf.sampleunder$(DESTDIR)$(SYSCONFDIR)/pepsi/. It never createspepsi.conf: the sample wires up optional stages that need further setup and is not a working configuration, so the live file comes frompepsi-setup --wizard(an existing one is never touched).install-config-dCopies
contrib/config.d/*.confto$(DESTDIR)$(DATADIR)/config.d. These are packaged defaults, not conffiles, and they are parsed beforepepsi.conf— see Configuration. Two ship with Pepsi: the per-language vacation notice texts and the S/MIME container setting (CRYPTO_ALLOW_DOWNGRADE).install-man/install-infoInstalls the rendered roff man pages under
$(DESTDIR)$(MANDIR)/manNandpepsi.infounder$(DESTDIR)$(INFODIR)(registered withinstall-info). WithBUILD_DOCS=no— what./configure --disable-docssets, and what a release tarball uses — pre-built pages are installed instead of being rendered, and each step is skipped with a message if none is present.install-docInstalls
README.md,NEWS,SECURITY.md,AUTHORS,COPYINGand the licence texts fromLICENSES/under$(DESTDIR)$(DOCDIR), which defaults to$(DATAROOTDIR)/doc/pepsi.install-systemdInstalls the systemd units from
debian/systemd/into$(DESTDIR)$(SYSTEMDUNITDIR)(rewriting/usr/binand/etc/pepsito the configuredBINDIR/SYSCONFDIR) plus thetmpfiles.dsnippet that gives/run/pepsiits owner and ACL – which is where that directory’s permissions come from entirely, since no unit may declare it as aRuntimeDirectory(see No unit may claim it as a RuntimeDirectory). Everything ships disabled.make install INSTALL_ADMIN_UNITS=noomits the twopepsi-setup-applyunits, which is the source-install equivalent of not installing thepepsi-httpd-adminpackage.
In addition, install depends on the privileged-bit targets
(install-helper, install-maildir-stage, install-forward-*,
install-auto-pay-*, install-smarthost-stage,
install-whitelist-suid, install-crypto-suid, install-quota-tool)
described under Privileged binaries below; each prints the chown/
chmod to run by hand when it is not run as root, or when the group it
needs does not exist.
Override PREFIX, SYSCONFDIR and DESTDIR to suit the target
system. make uninstall removes everything make install placed
(always including both pepsi-setup-apply units, whatever
INSTALL_ADMIN_UNITS says) but leaves the live pepsi.conf in
place.
Other make targets
make checkThe test entry point that needs no remote accounts: the file-inspection cross-checks of the systemd units and the telemetry socket path, the workspace tests (minus the dispatcher-lifecycle crate, which needs PostgreSQL), then – best effort, skipped rather than failed when the prerequisite is absent – the lifecycle suite against a local
pepsicheckdatabase, the setuid-helper contract tests, the authentication interoperability script,cargo deny checkand the manual’s cross-reference check.make integrationtests/make benchmarksThe live-pipeline scripts under
tests/, which need passwordless SSH to the hosts named inACCOUNTS(defaulttests/test-accounts.ini, created fromtests/test-accounts.ini.sample);integrationtestsfirst runs the Thunderbird interoperability gate. The benchmarks are a separate target because they deliberately load the MTA.make docsThe manual for the source language – an alias for
make docs-en, which builds its HTML and its PDF, plus the man pages and the info manual (both source-language only).make docs-<lang>builds one other language andmake docs-allbuilds every language inDOC_LANGUAGES(en de fr);make htmlbuilds every language’s HTML alone.make dist/make distcheckBuild the release tarball
pepsi-$(VERSION).tar.gz(every tracked file including the vendored submodule, plusconfigureand pre-built documentation), and verify it by unpacking it into a clean tree and runningconfigure,make,make check,make installandmake uninstallagainst a throwaway prefix. The tarball is copied from the working tree, somake distrefuses to run while a tracked file differs fromHEADor the submodule is not at the commitHEADrecords (untracked files do not count);make dist DIST_ALLOW_DIRTY=yesbuilds a development tarball anyway.make clean/make distcleancleanrunscargo cleanand removes the rendered documentation;distcleanadditionally removesconfig.mk,config.statusand anythingmake distgenerated.
Note
DATADIR must match what the binaries compute at runtime:
taler-common derives ${DATADIR} as <install-prefix>/share/pepsi
from the binary’s own location. When running from a source checkout (not
installed), set [pepsi-postgres] SQL_DIR explicitly so pepsi-setup
finds the migrations.
Preparing the system
The Debian package’s postinst does the steps below automatically; on a
source install you do them yourself, using the same account, group,
mode and directory layout documented under Users, groups and privileges
and Directories and secrets below. Create the accounts and groups
before make install (so it can apply the privileged bits), and the
database roles before pepsi-setup (which installs the schema):
Create the system accounts, each
--systemwith/usr/sbin/nologinand home/var/pepsiand a private group of the same name, plus the gating groups.debian/pepsi.postinstis the authoritative list; this loop is it, translated:for u in pepsi pepsi-ingress pepsi-httpd pepsi-owner \ pepsi-helper-token-refresh pepsi-whitelist pepsi-crypto \ pepsi-keydisc pepsi-config; do addgroup --system "$u" adduser --system --home /var/pepsi --no-create-home \ --ingroup "$u" --shell /usr/sbin/nologin \ --disabled-password "$u" done for g in pepsi-maildir pepsi-forward pepsi-token pepsi-admin \ pepsi-telemetry; do addgroup --system "$g" done # The shared-wallet account owns its wallet databases, so unlike the # service accounts it gets a real home. addgroup --system pepsi-wallets adduser --system --home /var/lib/pepsi-wallets --ingroup pepsi-wallets \ --shell /usr/sbin/nologin --disabled-password pepsi-wallets install -d -o pepsi-wallets -g pepsi-wallets -m 0700 /var/lib/pepsi-wallets # Memberships: telemetry producers, and the SRS secret fragment. adduser pepsi pepsi-telemetry adduser pepsi-ingress pepsi-telemetry adduser pepsi pepsi-ingress
Each of the last four accounts exists because something refuses to work without it:
pepsi-whitelistis the account the whitelist CLI is setuid to,pepsi-cryptois the only database role grantedcrypto_identity.private_wrappedand the owner of the key-encryption fragment,pepsi-keydiscis the narrowest role in the deployment (it parses key material fetched from the open internet), andpepsi-configis the only role that may write the configuration overlay. Skippepsi-cryptoandinstall-crypto-suidmerely warns — leaving the two crypto stages unprivileged and unable to open a single private key.Create the nine PostgreSQL login roles and the database (peer auth, as the
postgressuperuser) — the same setdebian/pepsi.postinstcreates, every account that authenticates as itself over the peer-auth socket.pepsi-telemetryis among them even on a node that serves no telemetry:pepsi-setup rungrants it on every host, and it does its database work aspepsi-owner, which cannot create a missing role:for r in pepsi-owner pepsi pepsi-ingress pepsi-httpd \ pepsi-whitelist pepsi-config pepsi-crypto pepsi-keydisc \ pepsi-telemetry; do createuser "$r" # createuser takes one role name at a time done createdb -O pepsi-owner pepsi
Run
make installasroot(above) so the setuid/setgid bits are applied; otherwise apply them by hand per Privileged binaries below. Eachinstall-*target only warns when the group it needs is missing, so an account or group skipped above leaves the corresponding program silently unprivileged.Create the runtime directories with the ownership and modes listed under Directories and secrets.
pepsi-setupcreates/var/pepsi/keysfor you; create the smarthost-secret directories only if you configure a smarthost using them.
The package is the simpler path: it ships the binaries (installed to
/usr/bin), the SQL schema, the message templates, the man pages, the GNU
info manual and socket-activated systemd units, and its postinst
provisions the whole system for you.
Installing the package
Install the .deb with apt so its dependencies (including
postgresql) are pulled in:
apt install ./pepsi_*.deb
On configure the postinst automatically:
creates the ten service accounts and the six gating groups —
pepsi-maildir,pepsi-forward,pepsi-token,pepsi-admin,pepsi-telemetryandpepsi-wallets— with the memberships between them (the same identities the source install makes by hand);creates the
/var/pepsiruntime directories —keys,tokens,token-refresh,tls,krb5— and/etc/pepsi/secretsand/etc/pepsi/secrets.dwith the ownership and modes documented under Directories and secrets below;applies the privileged-binary bits via
dpkg-statoverride(the three setuid-root helpers, the setgid programs that may exec them — the two delivery stages, the wallet stage andpepsi-quota— the setgid smarthost relay, the setuidpepsi-whitelistand the two setuidpepsi-cryptostages — see Privileged binaries below); andbest-effort creates the nine PostgreSQL login roles (
pepsi-owner,pepsi,pepsi-ingress,pepsi-httpd,pepsi-whitelist,pepsi-config,pepsi-crypto,pepsi-keydisc,pepsi-telemetry) and thepepsidatabase (owned bypepsi-owner) when a local cluster is reachable — printing the roles to create by hand if it is not.
The service is shipped disabled: the package installs but does not start Pepsi, leaving the site-specific bootstrap to you.
Note
pepsi is one of four binary packages built from this source:
the language-detection stage and the telemetry collector are split out
for size and deployment reasons, and pepsi-httpd-admin — which
pepsi Recommends, so apt installs it by default — carries
the only mechanism by which the browser console can cause a privileged
change on this host. Removing that one package is a supported
hardening step for a deployment administered from a terminal.
Debian packages describes each package and that lever in full.
After the package is installed
On first installation the package copies a placeholder configuration to
/etc/pepsi/pepsi.conf; later upgrades never touch that file (it is not a
dpkg conffile, so an upgrade never stops to ask about it). Edit it, then
run the bootstrap exactly as in the shared steps below — for example:
# 1. edit /etc/pepsi/pepsi.conf (HOSTNAME, ACCEPTED_DOMAINS, domains, TLS)
# 2. install the schema + DKIM keys and print the DNS records:
pepsi-setup --no-certbot run # or: pepsi-setup --wizard
# 3. publish the printed DNS records, then start the pipeline:
systemctl enable --now pepsi.target
Because the units are socket-activated, pepsi.target brings up
pepsi-ingress, pepsi-httpd and pepsi-dispatch together; you do
not invoke the serve subcommands by hand.
3.3. Migrating from an existing mail server¶
If this host already runs Postfix, Exim, Sendmail, qmail or Stalwart, you do not
have to re-derive its configuration by hand. With no pepsi.conf present,
pepsi-setup --wizard detects what is installed and offers to import it: the
hostname, accepted domains, smarthost (with the relay credentials read from that
server’s own password file), local-delivery method, submission SASL socket,
trusted networks, message size limit, queue lifetime and TLS certificate paths
all arrive as the interview’s pre-filled answers, which you then confirm or
correct one by one.
To see what a migration would produce before running the wizard — nothing is written:
pepsi-setup import
Routing tables are converted too. /etc/aliases, /etc/postfix/virtual,
Sendmail’s virtusertable, qmail’s .qmail-* files and their equivalents
become a Pepsi alias map beside the configuration (see
pepsi-stage-aliases), with bare alias names qualified in the
style you choose. Since Pepsi alias keys must carry a domain, postmaster:
root becomes postmaster@example.org root@example.org for each accepted
domain by default.
Everything that does not carry over is written to import-report.txt
beside the configuration, split into what failed to parse, what Pepsi has no
equivalent for (header_checks, an alias delivering to a command),
what was approximated (mbox delivery becoming Maildir) and what the importer did
not recognise. An import never aborts setup — an unreadable file or an unknown
directive produces a report entry, not a failure.
Two things are deliberately not migrated: queued mail (let the old server’s
queue drain before switching the MX over) and DKIM keys (pepsi-setup run
generates fresh ones and prints the records to publish). If the old MTA still
holds port 25 when the wizard runs, the report says so — stop and disable it
before starting pepsi-ingress.
See pepsi-setup for the per-server detail, the
--alias-style choices and the --expert options that expose the settings
the interview does not otherwise ask about.
3.3.1. Mail filters already on the host¶
Independently of any migration, the wizard scans the host for milter daemons
— clamav-milter, rspamd, spamass-milter, milter-greylist,
milter-regex, mimedefang, amavisd-milter — and offers each one it
finds as a stage. Finding one means three things in turn: the package is
installed, a socket can be read from its own configuration (or its unit, or the
paths its distribution ships), and a real milter option negotiation completes
against that socket. Nothing is offered on the strength of an installed
package alone, and the negotiation is also how the generated ALLOW_ACTIONS
comes to grant exactly what that filter asks for rather than a guess.
Accepted filters are placed by what they do — policy and greylisting, then
virus, then spam, then general-purpose frameworks — after decryption and before
the whitelist and paywall gates. Because Pepsi filters after accepting a
message, a filter’s REJECT is routed to a generated discard stage rather
than bounced, so forged senders get no backscatter; see
pepsi-stage-milter. Filters whose job Pepsi already does
(opendkim, openarc, opendmarc, SPF daemons, postsrsd) are named
and skipped rather than offered.
3.4. Provisioning the database, keys and DNS¶
With a configuration file in place (see Configuration), run the bootstrap tool once:
pepsi-setup -c /etc/pepsi/pepsi.conf run > pepsi-dns.zone
This validates the whole configuration, installs the pepsi schema,
generates per-domain RSA-2048 and Ed25519 DKIM keys under KEY_DIR (only
if absent), and prints the DNS records to publish (DKIM public keys, an SPF
policy from the configured PUBLIC_IP addresses, and an MTA-STS record). The
operation is idempotent and may be re-run at any time; upgrading the schema to a
new release is pepsi-setup schema (see Upgrading). run --reset
drops and recreates the schema (destroying all queued mail; keys on disk are
kept).
Publish the printed DNS records, then verify what is live against what Pepsi expects:
pepsi-setup -c /etc/pepsi/pepsi.conf check
See pepsi-setup for the full provisioning workflow.
3.5. Setting up in a browser¶
Everything above can also be done from a browser, against the same code. The terminal path is the shortest route on a machine you have a shell on; the browser path is for the ones you do not, and for operators who would rather see the DNS records as a red/green checklist than as a zone file.
3.5.1. The split, and why¶
Setup is privileged: it writes /etc/pepsi/pepsi.conf, hands each
secrets.d fragment to the single account that reads it, runs certbot, creates
database roles and generates key material — all as root. pepsi-httpd, which
serves the interface, drops privileges before it accepts a connection and must
never regain them.
So it does not act. It writes an intent row saying what should be true, and a
separate root program — pepsi-setup apply, started on demand and gone again
when it has nothing to do — decides how, does it, and records what happened. Root
is never on the network. Read “The applier’s trust model” in
:doc:`programs/pepsi-setup` before enabling this: it states exactly what the
applier will and will not do, including the things it deliberately cannot (there
is no restart or shutdown task, and install-schema refuses reset).
3.5.2. Enabling it¶
The Debian packages ship a minimal but complete configuration, an administrative
listener on a UNIX socket, and — in pepsi-httpd-admin, which pepsi
Recommends and apt therefore installs by default — the applier’s socket
unit (see Debian packages). Three steps:
# systemctl enable --now pepsi-httpd.socket pepsi-setup-apply.socket
# pepsi-setup -c /etc/pepsi/pepsi.conf bootstrap
The second prints a one-time token and the curl command that turns it into
the first administrator account. On the machine itself you can skip it entirely:
a member of the pepsi-admin group (and root) is identified by
SO_PEERCRED over /run/pepsi/admin.sock and needs no credential at all.
One more setting is needed before anything privileged can be asked for:
[pepsi-admin] CONFIG_DB must name a connection that authenticates as the
pepsi-config database role. That is the boundary: only that role may enqueue
setup work, and pepsi-httpd’s own account is not it, so a compromise of the
web tier is not a compromise of the pipeline definition. Until it is set the
setup endpoints answer 503 and say so.
3.5.3. Driving it¶
The interview is data. GET /api/v1/setup/questions returns the ordered steps
and, for each question, its shape, default, help text and the condition under
which it is asked — the same model pepsi-setup questions prints and the same
one the terminal wizard is described by, asserted equal by the test suite.
Answers are staged with PUT /api/v1/setup/answers into draft rows that no
running process can see, so an interrupted session cannot half-configure
anything.
POST /api/v1/setup/tasks then asks for the privileged half — writing the
configuration, obtaining certificates, installing the schema, generating keys —
and GET /api/v1/setup/tasks/{id}?since=<seq> streams what the applier is
doing, line by line.
3.5.4. What will always need a text editor¶
Some settings are read from the configuration file only, and no interface will
change that: [pepsi], [pepsi-postgres], [paths], and the HTTP(S) and
ingress listener sections. They have to work before a database connection
exists, or they are a security boundary — a database that could move a listening
socket or relax the crypto policy would make a database compromise a compromise
of the MTA’s own TLS and ports.
So adding a submission port, or moving a listener, is a
text-editor-and-restart operation. So is any change the console marks
restart rather than hot: there is no restart button, because a button
that restarts a service on request would hand the web tier’s privileges to
whoever reaches it.
3.5.5. Scripting it¶
The same answer model drives an unattended install:
pepsi-setup --wizard --answers answers.json -c /etc/pepsi/pepsi.conf
pepsi-setup questions prints the schema that file is written against. This is
not a third implementation of the interview: it runs the interactive one with
every prompt answered from the file, so the branches and the per-field validation
are the same ones a human sees.
3.6. Running the services¶
Two long-lived processes need to run continuously:
pepsi-ingress serve— accepts mail (typically under a service manager, optionally with systemd socket activation for the privileged ports).pepsi-dispatch serve— runs exactly one instance per system; it is the only process that starts stage programs.
The stage programs themselves are spawned by the dispatcher as persistent
PROGRAM worker processes that read message ids on standard input; you do not
run them by hand in production (for debugging you can pipe an id to worker,
e.g. echo 42 | pepsi-stage-srs -c /etc/pepsi/pepsi.conf worker).
3.7. Upgrading¶
From the first release, 0.0.0, on, every release upgrades the database of any earlier release in place: the queue, the key store, the settings and everything else in it are kept, and mail waiting in the queue is delivered by the new release. Downgrades are not supported (see below).
The procedure:
Read
NEWSfor the release. It lists configuration options that changed, and anything else that needs doing by hand.Take a backup (Backup and restore), or at least enable the automatic dump described below.
Install the new release. The Debian packages do the rest: they upgrade the schema and restart the services that were running. From source:
make install, thenpepsi-setup -c /etc/pepsi/pepsi.conf schemaandsystemctl try-restart pepsi.target.Check with
pepsi-statusthat the queue drains, and look at the journal for warnings (Logs).
The rest of this section explains what happens during step 3.
Every Pepsi program checks, as it connects to the database, that the schema was
built from exactly the SQL files the program itself was built with. The
installer records the SHA-256 and release of each file it applies in
pepsi.schema_file, and a program that finds anything else – an older
schema, a newer one, or one built from different files – stops at once with
exit status 78 and a message naming the fix, rather than failing at its first
missing column in the middle of a message. The dispatcher treats a stage worker
that stops this way as “cannot run yet”, not as a crash: the message it was
handed is put back in the queue, never failed.
Upgrading the schema is its own step:
pepsi-setup -c /etc/pepsi/pepsi.conf schema
It installs the patches this release adds, replaces the stored functions,
re-applies the role grants and does nothing else – no certificates, keys or DNS
– so it needs only the [pepsi-postgres] section. On a telemetry collector,
give it the collector’s file, /etc/pepsi-telemetry/pepsi-telemetry.conf.
Debian packages run it automatically when they are upgraded, before the
services are restarted; a new installation is left to pepsi-setup run. If
it fails (the database is down, say), the package still installs, a notice says
so, and the services refuse to start and are retried by systemd – backing off
to once a minute, without a start limit – until the schema matches. Once the
cause is fixed, running the command above is all it takes; the services come
back by themselves.
From source, after make install:
pepsi-setup -c /etc/pepsi/pepsi.conf schema
systemctl try-restart pepsi.target
What the refusals mean:
The message says |
What to do |
|---|---|
no |
A new installation: |
older than this program |
|
newer than this program |
A downgrade, which is not supported. Install the newer release again, or restore the database backup taken before the upgrade (below). |
built from a different copy of a patch |
A released patch never changes, so this is a bug; please report it. The
one exception is a database built from a development snapshot (a Git
checkout between releases), where the newest patch is still being
edited: drain it and re-create it with |
stored functions from a different build |
Development builds only: |
3.7.1. Downgrades¶
Not supported. pepsi-setup refuses to install an older release’s SQL over a
newer schema (exit status 78, nothing changed), and the older release’s
programs refuse to run on it. Going back to an older release means restoring a
database backup taken before the upgrade, then installing the older packages.
3.7.2. Backing up before a schema upgrade¶
No backup is taken unless you enable it. When enabled, the backup is taken only
when an upgrade actually changes the schema: pg_dump saves the pepsi
schema (with the extensions it uses) as
pepsi-<previous version>-<UTC time>.dump, readable by root only. If the dump
fails, the schema is not upgraded. Backups are never deleted automatically.
With the Debian packages it is a debconf setting, asked only at priority low:
dpkg-reconfigure -plow pepsi
Answer yes, and confirm or change the directory (default
/var/backups/pepsi). Unattended, or before the first upgrade:
echo "pepsi pepsi/schema-backup boolean true" | debconf-set-selections
echo "pepsi pepsi/schema-backup-dir string /var/backups/pepsi" | debconf-set-selections
On a telemetry collector host the questions belong to pepsi-telemetry
(pepsi-telemetry/schema-backup, pepsi-telemetry/schema-backup-dir).
When both packages are installed and use the same database, whichever upgrades
it first takes the backup.
From source, pass the directory to the upgrade step:
pepsi-setup -c /etc/pepsi/pepsi.conf schema --backup-dir /var/backups/pepsi
To restore one, stop the services, re-create the database empty and owned by the schema owner, and restore into it as that owner:
systemctl stop pepsi.target
sudo -u postgres dropdb pepsi
sudo -u postgres createdb -O pepsi-owner pepsi
sudo -u pepsi-owner pg_restore -d pepsi /var/backups/pepsi/pepsi-0.0.0-20261001T000000Z.dump
Then install the release the backup came from and start the services again.
Everything the database learnt after the backup – mail accepted since, settings
changed since – is lost. The stored private keys in the dump are encrypted
under the key-encryption key in secrets.d, which is not in the database: keep
that file, and back it up separately (see Backup and restore).
3.8. Users, groups and privileges¶
Pepsi never runs as root. The long-lived daemons may be started as
root so they can bind privileged ports and read root-only TLS keys, but each one
drops to its own unprivileged service account before serving a single
connection; if the drop cannot be completed the process refuses to start rather
than risk running as root (see pepsi-common::privdrop). Local delivery —
which must write into arbitrary users’ mailboxes — is the one operation that
needs more than the service user can give, and it is confined to a single
narrowly-scoped setuid helper rather than handed to the pipeline at large.
Under the shipped systemd units the daemons are never root at all: systemd
binds the privileged sockets (socket activation) and starts each process as its
service account directly. That leaves the TLS keys, which certbot keeps readable
by root only — so pepsi-setup has systemd read those too, writing a
LoadCredential= drop-in per unit: systemd opens each certificate and key as
root when it starts the unit and hands the service a private copy under
$CREDENTIALS_DIRECTORY, which is where the server looks first. Re-run
pepsi-setup run whenever you add, move or remove a certificate, so the
drop-in is regenerated. See pepsi-httpd.
The browser setup described above is the one path by which something other
than an operator at a terminal can cause a root process to run: pepsi-httpd
rings a doorbell socket and systemd starts a short-lived root pepsi-setup
apply. That path is the easiest privilege in Pepsi to take away.
Its two systemd units are a separate Debian package, pepsi-httpd-admin,
whose source-install equivalent is make install INSTALL_ADMIN_UNITS=no;
without them nothing drains pepsi.setup_task and no HTTP request can change
anything under /etc/pepsi, while pepsi-setup on a terminal keeps working
unchanged. Putting the package back does not retroactively act on what was asked
for while it was away: installing it empties that queue before its units are
armed. See The split as a hardening lever.
The accounts and groups below are created automatically by the Debian package’s
postinst; on a make install you create them yourself (the same names).
PostgreSQL access is by peer authentication over the local socket: each
service account logs in as a same-named database role, so no passwords are
stored anywhere. A remote database keeps the same model: one
login role per account, each authenticated by a client certificate or password
file of its own that only that account can read, named per role with the
{role} placeholder in [pepsi-postgres] CONFIG (see CONFIG in
pepsi.conf(5)). The configuration file never holds a password.
3.8.1. Service accounts¶
These are system accounts (--system, /usr/sbin/nologin, home
/var/pepsi), each with a private primary group of the same name. The first
three are long-lived daemons that drop to their account after binding; the rest
are not daemons in the pipeline.
Account |
Runs |
Purpose / access |
|---|---|---|
|
|
Inbound SMTP server. Socket-activated under systemd (never root, TLS
material arrives as a service credential); started by hand it binds
25/465/587 and reads the TLS keys as root, then drops. DB role
|
|
|
HTTP/HTTPS server (MTA-STS policy + |
|
|
The pipeline’s working identity. Reads the DKIM keys (via group
|
|
|
Not a runtime service. Owns the |
|
|
Only present when a smarthost uses |
|
|
Custodian of private end-to-end key material. Owns
|
|
|
Account the setuid whitelist CLI assumes so ordinary users can maintain
their own sender whitelists. Its database role is granted
|
|
|
Key discovery (WKD, DANE, VKS, LDAP). Parses key material fetched from the open internet, so its database role is the narrowest in the deployment. See Key management. |
|
|
The only database role that may write the |
The nine PostgreSQL login roles — pepsi-owner, pepsi,
pepsi-ingress, pepsi-httpd, pepsi-telemetry, plus the narrow
pepsi-whitelist, pepsi-config, pepsi-crypto and pepsi-keydisc
roles — are created by
the package postinst (or, on a manual install, by you) before pepsi-setup
installs the schema; pepsi-setup then grants each what it needs.
3.8.3. Privileged binaries¶
These bits are set by make install when run as root (it prints the exact
chown/chmod to run by hand otherwise), and by the Debian package via
dpkg-statoverride, which is the authoritative list
(debian/pepsi.postinst):
Binary |
Mode / owner |
Why |
|---|---|---|
|
|
Setuid root so it can write into any local user’s mailbox; group-locked
so only |
|
|
Setgid so the |
|
|
Same group and same reason: |
|
|
Setuid root so it can run a user’s |
|
|
Setgid so the |
|
|
Setuid root so it can drop to the wallet-owning account (or the
recipient’s own uid) to run |
|
|
Setgid so the |
|
|
Setgid so the |
|
|
Setuid, and world-executable on purpose: any local user may manage their
own |
|
|
Setuid, not setgid: PostgreSQL peer authentication keys off the
effective uid, and |
Every other binary is installed unprivileged, including pepsi-keys, which is
standalone for the same reasons but ships 0755: one binary able to open every
private key in a deployment is a much larger prize than pepsi-whitelist’s
single table, so making it setuid pepsi-crypto is a per-site decision rather
than the default.
3.8.4. Directories and secrets¶
pepsi-setup and the package create the following under /var/pepsi (the
service home) and /etc/pepsi. The setgid (2750) directories cause new
files to inherit the directory’s group, so token/TLS/Kerberos material is
group-readable by the relay without any per-file chgrp.
Path |
Owner / mode |
Contents |
|---|---|---|
|
|
Service home. |
|
|
Per-domain DKIM private keys: written by |
|
|
OAuth access-token files: written by the refresher, read by the smarthost
relay via group |
|
|
Private rotated refresh tokens; never group-readable. |
|
|
The |
|
|
The fragments |
|
|
Mutual-TLS client cert + key for the smarthost relay
( |
|
|
Kerberos credential cache for |
|
|
The local submission, administrative, HTTP and setup-applier sockets.
Created by the |
|
|
Home of the |
The /var/pepsi/tokens, /var/pepsi/tls and /var/pepsi/krb5 directories
(and the pepsi-helper-token-refresh account) only matter when you configure a
smarthost with the corresponding AUTH mechanism; a default direct-to-MX or
local-delivery deployment uses none of them.
3.9. Migrating from GNU Mailman¶
An existing GNU Mailman installation — 2.1 or 3 — moves onto Pepsi with
pepsi-list import and pepsi-archive import. Read the table below before
you start, not afterwards.
The behavioural differences are a different table, and it is not duplicated
here: Declared differences from GNU Mailman 3 in Mailing lists collects every place
Pepsi deliberately does something other than what upstream does — one-click
unsubscribe, the always-per-recipient fan-out, the archive’s obscured sender
addresses, the purge tombstone, the renamed X-Mailman-* headers, and what is
simply absent (no NNTP gateway, no pluggable archivers, no plugin API). Two
copies of that table would diverge, and the copy a migrating operator happened to
read would be the stale one. This section is only about what the importer
leaves behind.
3.9.1. What does not come across, and why¶
Why |
|
|---|---|
Passwords |
Neither Mailman core’s |
Bounce scores |
Upstream does not import them either: |
In-flight tokens |
A pending confirmation or moderation token is a promise about a workflow that will not exist after the cutover. Anybody mid-subscription asks again. |
Topics (2.1) |
No Mailman 3 equivalent. |
Template URIs we refuse to fetch |
A |
Anything else the importer did not understand is in its report. It exits
non-zero when something was lost, which is the point: a migration that half
worked and exited zero is a migration nobody checks. Nothing is rolled back, and
re-running an import is safe — every write is keyed on the list id, the
address or the Message-ID, so a second run is a no-op.
3.9.2. The recommended order¶
Do it in this order. The reason is in step 4.
Import the configuration and the roster, against the running old server:
pepsi-list import mailman3 --rest http://localhost:8001 \ --user restadmin --password-file /etc/mailman3/rest.pw \ --report /root/mailman-import.txt
Or, for a Mailman 2.1 site whose server is already off:
pepsi-list import mailman21 --listdir /var/lib/mailman/lists \ --report /root/mailman-import.txt
Add
--dry-runfirst: it reads everything, reports everything and writes nothing.Import the archive, per list:
pepsi-archive import --list announce@lists.example.org \ --rejects /root/rejected.txt /var/lib/mailman/archives/private/announce.mbox/*.mbox
Threads are reattached and renumbered automatically at the end — an out-of-order import otherwise leaves them split, which is the mistake this command exists not to repeat.
Verify.
pepsi-list check --strict, then spot-check a few lists’ settings against the old server. Over REST both ends answer/3.1/lists/<id>/config, so the comparison is per-attribute rather than by eye.Warm the IP with the invitation run, while the old server is still delivering mail.
pepsi-list invite send --dry-run # the per-domain histogram pepsi-list invite send --rate 200 --limit 500 pepsi-list invite status
This is the step whose timing matters. Telling every member to choose a password is the largest single mailing this server will ever send, to an address list it has never validated, from an IP with no reputation — and that is how a new deployment’s reputation is destroyed on day one. Doing it before the MX switch means a throttling or blocking problem becomes visible while it is still not load-bearing.
The dry run’s histogram is a deliverability tool, not a progress bar: one provider holding forty per cent of a migrated site is the fact that decides the rate, and it is invisible until something counts it. Invitations last four weeks, the run resumes where it stopped, and nobody is unsubscribed for not answering — a member who never acts keeps their subscription and simply cannot sign in until they use the link or ask for another.
Switch the MX, and only then stop the old server.
3.9.3. When our pickle reader is stricter than theirs¶
pepsi-list import mailman21 reads config.pck with its own parser, which
constructs nothing: no module is looked up and no callable is called, because
a config.pck comes from somebody else’s server and Python’s pickle.load
on untrusted input is arbitrary code execution.
The price of that is strictness. If the reader refuses a file — an opcode it does not implement, a protocol newer than Python 2 could write, an integer too wide for 64 bits — it says so and names the escape hatch:
python2 contrib/mm21-export.py /var/lib/mailman/lists/announce/config.pck \
> /root/announce.json
pepsi-list import mailman21 --json /root/announce.json --list announce
That script runs on the old server, with the Python that server already trusted, and dumps the same dictionary as JSON. Both routes then go through the same mapping, so they cannot disagree about what the file means. A migration must never be blocked by our parser being stricter than the one that wrote the file.
3.9.4. Header renames a user will notice¶
Pepsi emits X-Pepsi-List-* where Mailman emitted X-Mailman-*; the mapping
table is in Mailing lists. This is a migrating user’s real cost, because
procmail and Sieve rules are written against those names, and it is worth telling
them before the switch rather than after.