Configuration
Where configuration lives
install.sh writes one environment file per instance to
/etc/eon-keyserver/ (mode 0600):
| Instance | Env file | systemd unit | Data dir |
|---|---|---|---|
| default (single-instance) | keyserver.env | eon-keyserver.service | /var/lib/eon-keys |
INSTANCE=auth | auth.env | eon-keyserver-auth.service | /var/lib/eon-keys/auth |
INSTANCE=directory | directory.env | eon-keyserver-directory.service | /var/lib/eon-keys/directory |
Edit the env file and systemctl restart the matching unit to apply a
change. On first start, the server generates keyserver.db (SQLCipher) and
keyserver_signer.json (the ML-DSA signing identity) in its data
directory.
Command-line flags / env
These are the flags eon-keyserver accepts (env vars in parentheses where
one exists); install.sh writes most of them into the instance’s env file
as EXTRA_ARGS:
| Flag | Env | Meaning |
|---|---|---|
--data-dir | — | Directory for the database and signing identity (default .) |
--bind | — | Bind address (default 127.0.0.1) |
--port | — | Listen port (default 8443) |
--jwt-secret | JWT_SECRET | Signs the mailauth/mailverify JWTs |
--db-key | DB_ENCRYPTION_KEY | SQLCipher database encryption key |
--dovecot-auth-socket | — | Dovecot auth-client socket path; enables mailbox-bound publish auth (authoritative mode) |
--smtp-addr | — | SMTP relay host:port; lets the instance send mail. Needed to confirm a key replacement, and — with --open-enrollment — for the email verification of directory mode. Without it, /api/mailverify/* return 404 |
--open-enrollment | — | Allow enrolling an address by proving you can read its mail (/api/mailverify/*), default true. Right for the directory instance; set false on a mailbox-bound one, where publishing requires the mailbox password and “can read the mail” must not become the bar |
--allow-unconfirmed-key-change | — | Let a replacement bundle go live without the mailbox confirming it (default false, the pre-confirmation behaviour). Only for an operator who runs no mail at all: a valid publish token is then again enough to swap someone’s published key |
--admin-listen | — | host:port for the administrative API, e.g. 127.0.0.1:8444. Empty (default) means the admin surface is not mounted at all, not merely protected |
--four-eyes | — | Comma-separated administrative actions that need a second administrator’s approval: user.delete, user.disable, user.enable. Empty (default) gates nothing |
--allow-unencrypted-db | — | Start with a plaintext database when no --db-key/DB_ENCRYPTION_KEY is given (dev/test only; without it the daemon refuses to start rather than storing key bundles unencrypted) |
--smtp-from | — | From: address for verification emails; required when --smtp-addr is set (the process refuses to start otherwise) |
--trusted-proxy | — | Comma-separated CIDRs of reverse proxies allowed to set X-Forwarded-For. Empty (default) trusts no proxy and uses the raw TCP peer IP — set this to your proxy’s CIDR when deployed behind nginx/Caddy, or per-IP rate/harvest limits collapse onto the proxy’s IP |
--print-fingerprint | — | Print the signing-key fingerprint (and DNS anchor line) and exit — offline, no server start, no JWT/DB needed |
--version | — | Print the build version and exit |
Each action in --four-eyes is gated on its own, including the two
directions of disabling: gating user.disable without user.enable means
two administrators must agree to switch an account off, while one alone can
switch it back on — which on a keyserver restores that account’s ability to
publish a key. Gate user.enable too unless you mean that. The list is a
floor the web console can raise but never lower.
Do not pass --dovecot-auth-socket on a directory instance — that flag
is for domain-owned (mailbox-password) auth; the directory instance uses
the email-challenge instead. On an authoritative instance --smtp-addr is
still wanted, because key-replacement confirmation is mailed; pair it with
--open-enrollment=false so that being able to read the mail doesn’t become
a second, weaker way to publish.
Authoritative vs. directory mode, configured
Authoritative (bound to your own domain, Dovecot-gated):
JWT_SECRET="<secret>" DB_ENCRYPTION_KEY="<key>" \
bin/eon-keyserver \
--data-dir /var/lib/eon-keys/auth \
--bind 127.0.0.1 --port 8443 \
--dovecot-auth-socket /run/dovecot/auth-client \
--open-enrollment=false \
--trusted-proxy 127.0.0.1/32Directory / “Codex” (open enrollment, needs an SMTP relay):
JWT_SECRET="<secret>" DB_ENCRYPTION_KEY="<key>" \
bin/eon-keyserver \
--data-dir /var/lib/eon-directory \
--bind 127.0.0.1 --port 8444 \
--smtp-addr mx.example.com:25 \
--smtp-from verify@example.com \
--trusted-proxy 127.0.0.1/32Use a warmed domain in
--smtp-from, not the directory’s own subdomain. Send asverify@example.com(an established domain with SPF/DKIM/DMARC and sending reputation), notverify@codex.example.com— a brand-new subdomain has zero sending reputation and gets spam-foldered by Gmail even with perfect authentication. The--smtp-frommailbox should actually exist (a real mailbox or alias) so it passesreject_unlisted_senderchecks and captures bounces.
Dovecot mailbox-bound publish auth
Without this, anyone who can register on the keyserver can publish keys
for any address. With it, publishing keys for user@domain requires
proving you can log into that mailbox — Codex checks the supplied password
against Dovecot. The Eon Phoenix desktop app does this automatically on
first login, using the mailbox password it just authenticated with.
Stock mail servers (iRedMail included) don’t expose a Dovecot
auth-client socket to third-party services by default.
dovecot-setup.sh adds one, scoped to the eonkeys user, and wires the
keyserver to it:
sudo ./dovecot-setup.shIt writes /etc/dovecot/conf.d/99-eon-keyserver.conf (an auth-client
listener, mode 0660 user eonkeys), validates and restarts Dovecot,
resolves the socket path from doveconf base_dir, and restarts the
keyserver pointed at it. Idempotent — safe to run again. Verify:
curl -sk -o /dev/null -w '%{http_code}\n' -X POST \
-u 'user@domain:PASSWORD' https://keys.<domain>/api/mailauth/token
# 200 = OK · 401 = wrong creds · 502 = socket unreachableA 502 means the keyserver can’t reach the socket — check that
doveconf -h base_dir is traversable (o+x) and check
journalctl -u dovecot.
DNS
Under DNSSEC, for a domain running its own authoritative instance:
_eon-keysign.<domain>. IN TXT "v=eon-keysign1; fpr=<fingerprint from /api/server-key>"
_eon-keys._tcp.<domain>. IN SRV 0 5 443 keys.<domain>. ; optional discovery
keys.<domain>. IN A <server IP>DKIM/SPF/DMARC on the surrounding mail infrastructure authenticate domain routing only, not end-to-end sender identity (that’s the per-user ML-DSA+Ed25519 signature inside the message).
The directory instance has no domain of its own to anchor a
_eon-keysign record in — a client trusts it via a pinned signing-key
fingerprint instead of DNSSEC. That fingerprint is either compiled into
the client (internal/eonkeys/config.go,
DefaultDirectoryURL/DefaultDirectoryFpr) or supplied per-install via the
client’s keyserver.json (DirectoryURL/DirectoryFpr fields), which
overrides the compiled-in default without a rebuild. Until a directory’s
fingerprint is pinned somewhere, clients fail closed — directory-served
bundles are simply not trusted, never silently downgraded to plaintext.
Deployment topology reference
Two real deployment shapes are documented step-by-step, useful as a template for your own domain:
- Split (two boxes) — authoritative instance on the mail box, directory
instance (“Codex” proper) on its own EU-hosted box, isolating
open-enrollment abuse from the authoritative directory and giving each a
distinct reputation anchor. See
packaging/DEPLOY-EONCORE-SPLIT-RUNBOOK.mdin the source repository. - One-box — mail server and both keyserver instances on a single host,
bound to
127.0.0.1behind one nginx front, as the pragmatic first bring-up before splitting Codex onto its own box. Seepackaging/DEPLOY-EONCORE-ONEBOX-RUNBOOK.md.
Both require the directory instance to be EU-hosted / EU-jurisdiction in the reference deployment, since it is meant to serve as a sovereign trust root outside the reach of non-EU legal process — a constraint to carry into your own deployment plan if it applies to you, not something the software enforces.