Skip to content

Configuration

Where configuration lives

install.sh writes one environment file per instance to /etc/eon-keyserver/ (mode 0600):

InstanceEnv filesystemd unitData dir
default (single-instance)keyserver.enveon-keyserver.service/var/lib/eon-keys
INSTANCE=authauth.enveon-keyserver-auth.service/var/lib/eon-keys/auth
INSTANCE=directorydirectory.enveon-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:

FlagEnvMeaning
--data-dirDirectory for the database and signing identity (default .)
--bindBind address (default 127.0.0.1)
--portListen port (default 8443)
--jwt-secretJWT_SECRETSigns the mailauth/mailverify JWTs
--db-keyDB_ENCRYPTION_KEYSQLCipher database encryption key
--dovecot-auth-socketDovecot auth-client socket path; enables mailbox-bound publish auth (authoritative mode)
--smtp-addrSMTP 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-enrollmentAllow 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-changeLet 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-listenhost: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-eyesComma-separated administrative actions that need a second administrator’s approval: user.delete, user.disable, user.enable. Empty (default) gates nothing
--allow-unencrypted-dbStart 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-fromFrom: address for verification emails; required when --smtp-addr is set (the process refuses to start otherwise)
--trusted-proxyComma-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-fingerprintPrint the signing-key fingerprint (and DNS anchor line) and exit — offline, no server start, no JWT/DB needed
--versionPrint 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/32

Directory / “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/32

Use a warmed domain in --smtp-from, not the directory’s own subdomain. Send as verify@example.com (an established domain with SPF/DKIM/DMARC and sending reputation), not verify@codex.example.com — a brand-new subdomain has zero sending reputation and gets spam-foldered by Gmail even with perfect authentication. The --smtp-from mailbox should actually exist (a real mailbox or alias) so it passes reject_unlisted_sender checks 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.sh

It 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 unreachable

A 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.md in the source repository.
  • One-box — mail server and both keyserver instances on a single host, bound to 127.0.0.1 behind one nginx front, as the pragmatic first bring-up before splitting Codex onto its own box. See packaging/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.