.. This file is part of PEPSI. Copyright (C) 2026 GNUnet e.V. PEPSI is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. ============= Configuration ============= All Pepsi components read **one** INI-style configuration file (the format is shared with the GNU Taler tools). The exhaustive option reference lives in the single man page :manpage:`pepsi.conf(5)` and in the per-program chapters under **Programs**. File format =========== A file is a sequence of ``[SECTION]`` headers, each followed by ``OPTION = VALUE`` assignments. Section and option names are case-insensitive (conventionally upper case); ``#`` or ``%`` begins a comment; a value may be double-quoted to preserve surrounding whitespace. Three directives compose multiple files — important for keeping secrets out of the world-readable config: ``@inline@ FILE`` Include another file, relative to the current one. ``@inline-matching@ GLOB`` Include every file matching a shell glob. ``@inline-secret@ SECTION FILE`` Merge one section from a mode-restricted file (typically credentials). A directive ends the section it appears in, so write it *after* that section's last option; an option below it would belong to no section and the file would not load. A fragment that cannot be read is only a warning, which makes the options it carries look unset; ``pepsi-setup run`` gives each fragment to the service account meant to read it. Path-valued options undergo ``$``-expansion from the ``[PATHS]`` section or the environment (``$VAR``, ``${VAR}``, ``${VAR:-default}``). Value types are *boolean* (``YES``/``NO``), *number*, *duration* (``h``/``m``/``s`` units only — see the caveat below), *path* and *unix mode* (octal). .. warning:: ``duration`` values are parsed by ``jiff::SignedDuration``, which accepts only hours/minutes/seconds and below. A calendar unit such as ``5 d`` or ``2 weeks`` does **not** parse. For day-or-longer windows write the equivalent in hours (``120 h``) or use an integer option where one is provided (e.g. ``[pepsi-srs] MAX_AGE_DAYS``). Packaged defaults: ``config.d`` ------------------------------- Below the operator's file there is one more layer. Every ``*.conf`` in ``${DATADIR}/config.d`` (``/usr/share/pepsi/config.d`` on a packaged install) is parsed **before** ``pepsi.conf``, so those files supply defaults that ``pepsi.conf`` overrides option by option. They belong to the package: redefine what you want in ``pepsi.conf`` rather than editing them, or an upgrade will discard the change. Two ship, and they are the two shapes this layer is for. ``vacation.conf`` Configuration that ships as *wording* rather than as a value — the ``[pepsi-vacation-default-message]`` notices in ten languages that :doc:`programs/pepsi-stage-vacation` falls back to. Redefining ``EN`` in ``pepsi.conf`` leaves the other nine in place. ``thunderbird.conf`` A default an operator must be able to *undo in one step*: ``[pepsi] CRYPTO_ALLOW_DOWNGRADE = yes``, so S/MIME is encrypted as AES-256-CBC ``EnvelopedData``. Thunderbird cannot read the AES-256-GCM ``AuthEnvelopedData`` Pepsi's code default produces, and fails silently when it tries (see :doc:`interoperability`). The *code* default remains ``AuthEnvelopedData``, so deleting the file restores authenticated encryption immediately — which is why this lives in ``config.d`` and not in the source. The shared sections =================== These sections are read by more than one component, so they live once in the shared file: ``[pepsi]`` Cross-cutting identity and policy: ``KEY_DIR``, ``DKIM_SELECTOR``, ``DKIM_ALGORITHMS``, ``ARC_DOMAIN``, ``ARC_ALGORITHM``, ``ORIGINATE_SUCCESS_DSN``, ``LOG_JSON`` (structured JSON-lines logging, off by default), ``LOG`` (the pipeline-wide default log level), and the MTA-STS publishing options. See :manpage:`pepsi.conf(5)`. ``[pepsi-postgres]`` Database connection (``CONFIG`` connection string; ``SQL_DIR`` for ``pepsi-setup``). Every component connects to the same database and the one ``pepsi`` schema. ``[pepsi-srs]`` The SRS engine parameters (``SRS_DOMAIN``, ``SECRET``/``SECRET_FILE``, ``MAX_AGE_DAYS``) shared by ``pepsi-stage-srs`` and ``pepsi-ingress``'s reverse decode. See :doc:`programs/pepsi-stage-srs`. ``[pepsi-dispatch]`` Dispatcher options (``MAX_RUNTIME``, ``WORKER_IDLE_TIMEOUT``, ``POLL_INTERVAL``, ``STATS_INTERVAL``, ``CONFIG_FILE``, ``DB_POOL_SIZE``). Per-stage worker-pool sizing (``PARALLELISM``, ``MAX_MESSAGES``, ``QUEUE_LIMIT``) lives in each ``[stage-]`` section. See :doc:`programs/pepsi-dispatch`. ``[pepsi-httpd]`` / ``[pepsi-httpd-listener-]`` / ``[pepsi-httpd-cert-]`` The HTTP/HTTPS server: a ``MAX_CONNECTIONS`` cap, one listener section per socket (``SERVE`` exactly like the ingress listeners; ``MODE = plain`` (the default) or ``tls``; default port 443), and one certificate section per SNI host name (``SNI``, ``TLS_CERT``, ``TLS_KEY``). The served domains and the MTA-STS policy are taken from the ``[pepsi-ingress]`` and ``[pepsi]`` sections. See :doc:`programs/pepsi-httpd`. ``[pepsi-stage-relay-to-smarthost-mta-]`` One upstream smarthost each, shared by every smarthost-relay stage. See :doc:`programs/pepsi-stage-relay-to-smarthost`. Ingress and listeners ===================== ``[pepsi-ingress]`` holds message-acceptance policy (``HOSTNAME``, ``ACCEPTED_DOMAINS``, ``MAX_MESSAGE_SIZE``, ``MAX_CONNECTIONS``, ``DMARC_ENFORCE``, DNS settings). Each ``[pepsi-ingress-listener-]`` section binds one socket: * ``SERVE = tcp`` (with ``BIND_TO``/``PORT``), ``unix`` (with ``UNIXPATH``) or ``systemd`` (socket activation, ``FD_INDEX``). * ``MODE = plain`` | ``starttls`` | ``tls`` selects the transport security; ``starttls``/``tls`` require ``TLS_CERT`` and ``TLS_KEY``. Declare as many listeners as needed — e.g. an MX listener on 25 with opportunistic STARTTLS and a submissions listener on 465 with implicit TLS. .. _config-pipeline: The stage pipeline ================== The pipeline is wired entirely from ``[stage-]`` sections. The stage **name** is an operator-chosen label; the section's ``PROGRAM`` selects the binary, and ``NEXT_STAGE``/``BOUNCE_STAGE`` connect the graph: ``PROGRAM`` *(required)* The stage binary (found on ``PATH`` unless absolute). One binary may serve several stages — it reads its options from whichever section the message is currently at. ``NEXT_STAGE`` *(optional)* Where a message advances on success. A terminal stage with no ``NEXT_STAGE`` removes the delivered message. ``BOUNCE_STAGE`` *(optional)* Where a permanently-failed (or, with ``ORIGINATE_SUCCESS_DSN``, successfully-delivered) message is routed to generate a DSN — usually a ``pepsi-stage-bounce`` stage. ``PARALLELISM`` / ``MAX_MESSAGES`` / ``QUEUE_LIMIT`` *(optional)* The worker-pool sizing the dispatcher applies to this stage: the cap on concurrent worker processes (``PARALLELISM``, default 4, started on demand and reaped when idle), the number of messages a worker handles before it is recycled (``MAX_MESSAGES``, default 1000), and how many messages are pipelined to one worker at once (``QUEUE_LIMIT``, default 4, so the stage's in-flight capacity is ``QUEUE_LIMIT × PARALLELISM`` while the process and connection count stay set by ``PARALLELISM``). See :doc:`programs/pepsi-dispatch`. ``FUSION`` *(optional)* Whether a predecessor may run this stage in its own worker process instead of handing the row back to the dispatcher. Defaults to ``YES`` when ``PROGRAM`` is one of the fast body-free stages (``pepsi-stage-`` + ``if``, ``discard``, ``auto-whitelist``, ``block-language``, ``check-whitelist``, ``srs``, ``list`` or ``edit-settings``) and ``NO`` otherwise; ``[pepsi] ALLOW_FUSION = NO`` disables fusion globally. New messages always enter at ``[stage-init]``, which **must** exist. Each program reads its own options from its section; those are documented per program under **Programs**. Ordering constraints -------------------- The pipeline graph is yours to draw, but five orderings are not free choices, and getting any of them wrong produces mail that *looks* right. ``pepsi-setup`` warns about three of them: a DKIM-signing stage that leads to an encrypt stage, a decrypt stage that leads to an ARC stage, and a decrypt stage with no local delivery downstream. **Encryption comes before DKIM signing.** The recommended outbound chain is:: submission → … → encrypt → srs → dkim-sign → relay :doc:`programs/pepsi-stage-encrypt` rewrites the body, and DKIM must sign the bytes that are actually transmitted. Signing first yields a message whose DKIM signature does not verify at the recipient — which is **worse than no signature**, because a broken signature is a stronger negative signal to a receiving MTA than an absent one. **Anything that reads the body comes before encryption.** The pay-to-send gate (:doc:`programs/pepsi-stage-anti-spam`) and language detection (:doc:`programs/pepsi-stage-detect-language`) both want cleartext, and both get it, because encryption is last on the outbound path — which is the reason for the ordering above. **SRS is for forwarded mail, and harmless on the outbound relay tail.** It rewrites a sender in somebody else's domain — mail this host forwards — so that SPF passes at the next hop. The null sender and a sender already in the SRS domain are left alone, so with ``SRS_DOMAIN`` set to the primary domain (the wizard's default) our own users' mail passes through unchanged, and a relay tail shared by forwarded and submitted mail can run it before signing, in the order ``pepsi-setup`` checks for: ``encrypt → srs → dkim-sign → relay``. A user of a *further* served domain is rewritten too; that costs SPF alignment for their domain, but DMARC still passes on the aligned DKIM signature ``dkim-sign`` adds afterwards. **ARC comes before decryption.** The recommended inbound chain is:: init = arc → decrypt → aliases → (anti-spam / language / …) → local delivery An ARC set seals the message *as it arrived*. :doc:`programs/pepsi-stage-decrypt` rewrites the body, so decrypting first would make the sealed ``AMS`` describe bytes nobody else ever saw — a claim about a message that never existed. **Decryption needs local delivery downstream.** Decrypting invalidates the sender's DKIM signature, which is harmless for a message about to be filed in a mailbox on this host and is not for one being relayed onward. The stage therefore runs only for recipients this host serves and splits a message with both kinds; ``pepsi-setup`` warns when no :doc:`programs/pepsi-stage-relay-to-maildir` or :doc:`programs/pepsi-stage-relay-to-lmtp` is reachable from a decrypt stage, because such a stage is either pointless or harmful. The mirror image of the outbound rule applies too: **anything that reads the body comes after decryption**, which is why the content stages sit where they do. Note that the decrypt stage loads the whole message body for every message that passes through it (its ``Load`` is fixed per stage, and whether a message is protected cannot be told from the envelope columns), so placement is a performance matter as well as a correctness one. Worked examples =============== Each example below is a **complete, minimal** pipeline for one job, with no language classification, ARC/DKIM signing or spam filtering — just the shared sections, one listener, and the stages that do the work. They all share the same shell: * ``[pepsi]`` (only ``KEY_DIR`` here — DKIM/ARC are not used in these examples), * ``[pepsi-postgres]`` for the database, and * ``[pepsi-ingress]`` plus one ``[pepsi-ingress-listener-*]`` socket. The listeners set ``MODE = starttls``/``tls`` but no ``TLS_CERT``/``TLS_KEY``: ``pepsi-setup`` auto-fills the certbot paths (see :doc:`installation`). Set them explicitly, or pass ``--no-certbot``, to manage the certificate yourself. Each is a real, self-contained configuration — to layer ARC, SRS, DKIM signing or spam filtering on top, insert the corresponding stages between ``init`` and the delivery stage as the sample ``pepsi.conf`` shows. Minimal inbound (relay to a smarthost) -------------------------------------- An edge MX that accepts mail for our domains and hands every message to one authenticated upstream smarthost. ``init`` *is* the relay stage; the smarthost authenticates us, so no SRS rewrite is needed here:: [pepsi] KEY_DIR = /var/pepsi/keys [pepsi-postgres] CONFIG = postgres:///pepsi [pepsi-ingress] HOSTNAME = mail.example.org ACCEPTED_DOMAINS = example.org [pepsi-ingress-listener-mx] SERVE = tcp BIND_TO = 0.0.0.0 PORT = 25 MODE = starttls # init: relay every accepted message to the upstream smarthost. A successful # delivery is terminal (no NEXT_STAGE). [stage-init] PROGRAM = pepsi-stage-relay-to-smarthost SERVER_NAME = mail.example.org # The upstream smarthost (one [...-mta-*] section per route; CATCH_ALL takes # everything not matched by a DOMAINS list). [pepsi-stage-relay-to-smarthost-mta-upstream] HOST = smtp.relay.example.net PORT = 587 MODE = starttls AUTH = plain USERNAME = relayuser PASSWORD = changeme CATCH_ALL = yes Minimal outbound (relay direct to the internet) ----------------------------------------------- A submission/relayhost that accepts our own users' mail and delivers it directly to each recipient's MX. The listener trusts a local network (``MYNETWORKS``) so those submissions are marked locally-originated and allowed to relay to any domain; outbound mail keeps its own envelope sender, so there is no SRS stage:: [pepsi] KEY_DIR = /var/pepsi/keys [pepsi-postgres] CONFIG = postgres:///pepsi [pepsi-ingress] HOSTNAME = mail.example.org ACCEPTED_DOMAINS = example.org [pepsi-ingress-listener-submission] SERVE = tcp BIND_TO = 0.0.0.0 PORT = 587 MODE = starttls # Trust submissions from this network without authentication (use SASL_TYPE / # TLS_AUTH_CLIENT instead for roaming users). MYNETWORKS = 10.0.0.0/8 # init: deliver directly to each recipient's mail exchanger. Terminal. [stage-init] PROGRAM = pepsi-stage-relay-to-internet SERVER_NAME = mail.example.org # Read by pepsi-setup to build the SPF record listing our sending hosts, and # to check each address's PTR against SERVER_NAME above. PUBLIC_IP belongs in # the relay *stage's* section: pepsi-setup finds it by walking [stage-*] and # keeping the sections whose PROGRAM is a relay, never by the program's own # section name. PUBLIC_IP = 203.0.113.7 2001:db8::25 Minimal local delivery (relay to Maildir) ----------------------------------------- An MX that delivers mail for our domain into local users' ``Maildir/new/``. Every recipient is in ``ACCEPTED_DOMAINS`` (so ingress accepts it) and resolves to a local account, hence no ``NEXT_STAGE`` is needed — add one (a smarthost or bounce stage) if a recipient in our domain may have no local mailbox:: [pepsi] KEY_DIR = /var/pepsi/keys [pepsi-postgres] CONFIG = postgres:///pepsi [pepsi-ingress] HOSTNAME = mail.example.org ACCEPTED_DOMAINS = example.org [pepsi-ingress-listener-mx] SERVE = tcp BIND_TO = 0.0.0.0 PORT = 25 MODE = starttls # init: write each local recipient's copy into their Maildir via the setuid # helper. Terminal for local recipients. [stage-init] PROGRAM = pepsi-stage-relay-to-maildir SERVER_NAME = mail.example.org A staging deployment that never touches the network swaps the delivery stage for ``pepsi-stage-discard`` (see :doc:`programs/pepsi-stage-discard`). Configuration in the database ============================= The file described above is the base layer. On top of it, Pepsi reads a set of administrator-managed overrides from the ``pepsi.config_override`` table, so a running deployment can be reconfigured without editing files and without a restart. Nothing about this is on by default. **With no rows in that table a deployment behaves exactly as its configuration file says** — there is no implicit configuration hiding in the database, and deleting every override returns the system to the file's behaviour. The scope chain --------------- Five layers, lowest precedence first: .. list-table:: :header-rows: 1 :widths: 22 20 58 * - Layer - Written by - Applies to * - the configuration file - the operator, with a text editor - everything * - ``global`` - ``pepsi-config set`` - everything * - ``domain:`` - ``pepsi-config set --scope`` - messages whose relevant address is at that domain * - ``address:
`` - ``pepsi-config set --scope`` - messages whose relevant address is exactly that one * - ``pepsi.settings`` - **the account owner**, by e-mail - messages to/from that one address Higher layers override lower ones **option by option, never section by section**. An option that no layer mentions keeps its file value, so an override is always a targeted amendment rather than a replacement of a whole section. The "relevant address" is the same one the per-address settings layer uses: the envelope **sender** for a locally-originated message, otherwise each envelope **recipient**. When one message's recipients resolve to different configurations for the stage about to run, the message is split into one row per distinct configuration, exactly as for ``pepsi.settings``. The ``domain:`` and ``address:`` layers hold ``[stage-*]`` sections only: a stage consults them for the section it is running, per message. Every other section is read once per process and sees the file plus the ``global`` layer, so ``pepsi-config`` refuses to store a scoped override of one and ``pepsi-setup run`` reports any such row as an error. Why ``pepsi.settings`` is a separate table ------------------------------------------ The two have different write authorities: * ``pepsi.settings`` is written by **account owners**, from their own mailbox, through :doc:`programs/pepsi-stage-edit-settings`, restricted to the stage sections the operator listed in ``EDITABLE_STAGES``; * ``config_override`` **defines the pipeline** and is writable only through the ``pepsi-config`` database role. Merging them would put user-writable rows in the same table as the definition of the mail server, where one bug in a namespace check stops being an override leak and becomes "a user reconfigured the MTA". PostgreSQL enforces the separation: ``pepsi-setup`` grants ``INSERT``/``UPDATE``/``DELETE`` on ``config_override`` to the ``pepsi-config`` role alone, revokes them from every service account, and **verifies both against the live database** after each install. A stage worker parses hostile mail for a living; it may read the configuration and may not change it. What stays in the configuration file ------------------------------------ Some sections are never read from the database, whatever rows exist. An override naming one of them is ignored at run time and refused at write time: ``[pepsi]``, ``[pepsi-postgres]``, ``[PATHS]`` Needed to find and open the database in the first place, and — for ``[pepsi]`` — the home of the administrator-only cryptographic policy. ``[pepsi-httpd]``, ``[pepsi-httpd-listener-*]``, ``[pepsi-httpd-cert-*]`` The server that serves the configuration interface must always be able to start, whatever the database says. ``[pepsi-ingress-listener-*]`` Listening sockets are bound at start-up, often under socket activation and before privileges are dropped. ``[pepsi-secure-link]`` Its ``PEPPER`` is the server-side secret that makes a stolen database useless (see :ref:`secure-link`). A database able to *replace* it could quietly arrange for the next message stored to be one the attacker can open — the same class of thing as lowering the crypto policy. ``[pepsi-admin]`` Decides who may administer the server and how the administrative surface reaches the database; a database opinion about it would be a way to grant yourself the console. ``[pepsi-crypto]``, ``[pepsi-srs]``, ``[pepsi-origin]`` Each holds a server-side key that exists so that the database alone is not enough: the key-encryption keys (and which of them wraps new private keys), the SRS key that authorises relaying to any address, and the proof-of-origin key a returning bounce or payment demand is checked against. A database able to replace one could arrange for the next wrapped key to be one an attacker can open, or for a forged bounce to be relayed or paid. The rest of each section rides along. ``[pepsi-wizard]`` Here for a third reason: it is not configuration at all, but the setup wizard's record of the answers it was given — which the browser interview stages as *draft* rows in this very table. Drafts are invisible to every reader, but a hand-written non-draft row would otherwise land in the effective configuration of every process. Nothing reads the section at run time, so the safe answer is that the database never supplies it either. The line is "needed before a database connection exists, a security boundary, or the home of a server-side secret". Listeners are on the second side of it deliberately: if the database could move a listening socket or change its TLS material, a database compromise would become a compromise of the mail server's own ports. .. important:: **Adding a submission port is a text-editor-and-restart operation.** So is changing a listener's TLS certificate, the database connection, or the ``[pepsi]`` crypto policy. No administrative interface can change these, and none should imply that it can. Credentials are never stored in the database, whatever section they are in. An option whose name marks it as one — containing ``PASS``, ``SECRET``, ``TOKEN``, ``CREDENTIAL``, ``CLIENT_ID`` or ``PEPPER``, the rule that masks a value wherever a configuration is shown — is refused by ``pepsi-config set`` and by the administrative API, and ignored if a row for one exists anyway. The rule covers the file a credential is read from and the endpoint it is sent to as well (``TOKEN_FILE``, ``TOKEN_ENDPOINT``), since a database able to change those can redirect the credential. Put them in the file or in a ``secrets.d`` fragment. Likewise a ``domain:`` or ``address:`` override is accepted for a ``[stage-*]`` section only (see above: nothing else reads those layers), and refused for any other section rather than stored where nothing will read it. Two further options are file-only for a plainer reason — they are read to open the database itself, before an overlay can exist: ``[pepsi-ingress] DB_POOL_SIZE`` and ``[pepsi-dispatch] DB_POOL_SIZE``. Hot reload, and what needs a restart ------------------------------------ Every write to the table fires a ``config_changed`` notification. What happens next depends on the section: ``[stage-*]`` — **applied without a restart.** ``pepsi-dispatch`` does two things when the notification arrives: it rebuilds its own view of the pipeline from the overlay — which stages exist, and each one's ``PROGRAM``, ``PARALLELISM``, ``MAX_MESSAGES`` and ``QUEUE_LIMIT`` — and it retires its stage workers (no message is interrupted: a worker is given no further work and exits once its current messages are done), so their replacements read the new values. The next message is routed *and* run with the new configuration, which is what makes adding, editing and removing a stage all live. The one hard edge: if the reloaded graph does not **parse**, the dispatcher shuts down and exits ``EX_CONFIG`` rather than route by a pipeline its workers no longer share. The shipped unit restarts it (``Restart=always``, backing off to once a minute), so it resumes on its own once the configuration is fixed. An overlay that cannot be *read* (a database error) is different: the dispatcher keeps its current stage graph and retries the reload, rather than dropping every stage defined only in the database. Everything else — **needs a restart of the component that reads it.** ``[pepsi-ingress]``, ``[pepsi-srs]``, ``[pepsi-tlsrpt]`` and the rest are read once at start-up. The value is stored, and takes effect when that program is next started. ``pepsi-config`` says which applies after every write. A component that misses a notification — because its listener was disconnected — re-reads the overlay when the listener reconnects, so a lost notification costs latency and not correctness. Secrets stay in files --------------------- The database stores only the ``@inline-secret@`` *reference*, never a secret value, so each fragment under ``secrets.d`` stays owned by the single reader that needs it: an unprivileged stage can read its own secret and nothing else. Encrypted values in the database would hand every reader one key that opens everything. Editing the overlay ------------------- :: pepsi-config set stage-relay MAX_LIFETIME '48 h' pepsi-config set --scope domain:example.org stage-relay DELAY_DSN_AFTER '4 h' pepsi-config unset stage-relay MAX_LIFETIME pepsi-config list Every write is validated first, by building the configuration the change would produce and running the owning stage's real parser over it — the same check :doc:`programs/pepsi-stage-edit-settings` applies to an e-mailed override. A value that would stop a program from starting is refused with that program's own error message, and nothing is stored. Should a bad row reach the table anyway, what happens depends on how bad it is. A row that cannot be represented in the configuration (an unknown scope, a file-only section, a malformed name) is logged loudly and skipped; an overlay that cannot be read at all, or whose merged text does not parse, is logged and the program runs on the configuration file alone. A value that is well-formed but that the program itself rejects — an unparseable duration, a ``NEXT_STAGE`` naming no stage — is not skipped: it is part of the effective configuration, and the program fails on it exactly as it would on the same value in the file. ``pepsi-setup run`` checks for both (see `Validating`_). Where a value came from ----------------------- :: pepsi-config dump --origin pepsi-config dump --origin --scope address:user@example.org annotates every effective value with the layer that set it (``file``, or the database scope), and each section with whether a change to it is applied live or needs a restart. Backing it up ------------- The two halves are backed up by different tools, on purpose: * what is **in** the database — the overrides — is covered by ``pg_dump`` along with the rest of the schema; * what **cannot** be, because it must exist before a database connection does — the configuration file and the ``secrets.d`` fragments — is covered by ``pepsi-config export``, which writes a single password-encrypted archive. The archive records each file's mode, owner and group **by name**, and ``pepsi-config import`` restores them; it refuses rather than guesses when a named account does not exist on the target machine. An archive that lost ownership would either break every unprivileged reader or silently make a secret world-readable. Validating ========== Always validate a configuration before relying on it:: pepsi-setup -c /etc/pepsi/pepsi.conf check # against live DNS pepsi-config -c /etc/pepsi/pepsi.conf dump # effective values ``pepsi-setup run`` refuses to change anything if the configuration is invalid: it checks that ``[stage-init]`` exists, that every ``NEXT_STAGE``/``BOUNCE_STAGE`` resolves, that each stage's ``PROGRAM`` configuration parses, and that domains and ``PUBLIC_IP`` values are well-formed. It validates the database overlay too: a stored override must name a real scope, must not name a file-only section (which would be silently ignored), must not put anything but a ``[stage-*]`` section in a ``domain:`` or ``address:`` scope, and must name a stage section that exists — in the file or defined by the overlay itself, since the dispatcher loads a stage added with ``pepsi-config set`` without a restart. The configuration it produces must then still satisfy every affected stage's own parser: the ``global`` layer over the file, and each ``domain:``/``address:`` scope's chain over that, each running the parsers of the stages it touches.