Install
Requirements
| OS | Linux with systemd |
| Privileges | root (for the installer) |
| OpenSSL | ≥ 3.5 — ML-DSA (FIPS 204) only exists in OpenSSL 3.5+. Ubuntu 24.04 ships 3.0 and will not work unless you use the static package (below). |
| TLS | A reverse proxy (nginx/Caddy) in front, terminating HTTPS |
| DNS | Ability to publish a TXT record (and optionally SRV/A), ideally DNSSEC-signed, for the authoritative instance |
Check your OpenSSL with openssl version — it must report OpenSSL 3.5.x
or newer, unless you use the static package, which carries its own OpenSSL
and does not depend on the host’s.
Get the package
The keyserver ships as a tarball, either linked against the system OpenSSL or as a static build that carries its own:
eon-keyserver-<version>-linux-amd64.tar.gz
└── eon-keyserver-<version>/
├── bin/eon-keyserver # the server
├── bin/eon-cli # CLI client (optional, for testing)
├── systemd/eon-keyserver.service
├── install.sh
├── upgrade.sh
├── uninstall.sh
├── dovecot-setup.sh
├── check-domain.sh
├── README.md
└── VERSIONInstall a single instance
tar xzf eon-keyserver-<version>-linux-amd64.tar.gz
cd eon-keyserver-<version>/
sudo ./install.shThe installer is interactive (defaults shown, Enter accepts them):
- Verifies systemd and OpenSSL ≥ 3.5 (skip this check by exporting
OPENSSL_PREFIXwhen the package is the static build). - Asks for:
- Domain — e.g.
keys.example.com(used only in the printed DNS hints). - Bind address (default
0.0.0.0) and port (default8443). In production, bind to127.0.0.1behind the reverse proxy. - Data dir (default
/var/lib/eon-keys). - JWT secret and DB encryption key (SQLCipher) — auto-generate or paste your own.
- Whether to enable Dovecot mailbox-bound publish auth, and if so, the Dovecot auth-client socket path.
- Domain — e.g.
- Creates the
eonkeyssystem user/group, the data dir, and/etc/eon-keyserver/keyserver.env(mode0600). - Installs the binaries to
/usr/local/bin, installs and enables the systemd unit, starts the service, and prints status.
Non-interactive (automation): set env vars and ASSUME_YES=1:
sudo DOMAIN=keys.example.com PORT=8443 \
JWT_SECRET=... DB_KEY=... ASSUME_YES=1 ./install.shRe-running install.sh updates the binary and unit but keeps the
existing data directory and any secrets already set.
Two instances on one host (authoritative + directory)
A production deployment usually runs both modes described in
what Codex is: the authoritative instance for
your own mail domain, and the open-enrollment directory instance (“Codex”
proper) for third-party mailboxes. install.sh accepts an optional
INSTANCE variable so both can coexist on one host, each with its own
systemd unit, env file, and data directory:
INSTANCE | unit | env file | data dir |
|---|---|---|---|
| (empty) | eon-keyserver.service | keyserver.env | /var/lib/eon-keys |
auth | eon-keyserver-auth.service | auth.env | /var/lib/eon-keys/auth |
directory | eon-keyserver-directory.service | directory.env | /var/lib/eon-keys/directory |
# authoritative instance — Dovecot-bound, no open enrollment
sudo ASSUME_YES=1 INSTANCE=auth \
DOMAIN=keys.example.com BIND=127.0.0.1 PORT=8443 \
TRUSTED_PROXY=127.0.0.1/32 \
DOVECOT_SOCKET=/run/dovecot/auth-client \
./install.sh
# open directory instance ("Codex") — needs an SMTP relay to send challenge codes
sudo ASSUME_YES=1 INSTANCE=directory \
DOMAIN=codex.example.com BIND=127.0.0.1 PORT=8444 \
SMTP_ADDR=127.0.0.1:25 SMTP_FROM=keys@example.com \
TRUSTED_PROXY=127.0.0.1/32 \
./install.shSee Configuration for what each
of these flags does and why the directory instance needs an SMTP relay and
a warmed SMTP_FROM domain.
Post-install: DNS and TLS
This step is required before clients will trust the server.
Get the signing fingerprint once the service is up:
curl -s http://127.0.0.1:8443/api/server-key | grep -o '"fingerprint":"[0-9a-f]*"'Publish DNS, ideally under DNSSEC, for the authoritative instance:
_eon-keysign.<domain>. IN TXT "v=eon-keysign1; fpr=<fingerprint>" _eon-keys._tcp.<domain>. IN SRV 0 5 443 keys.<domain>. ; optional discoveryThe
_eon-keysignTXT record is the trust anchor clients verify discovery responses against; DNSSEC on this record is what defeats DNS spoofing. The directory instance has no domain of its own to anchor in DNS this way — see Configuration for how its fingerprint gets pinned in clients instead.Put TLS in front. The server speaks plain HTTP on its bind port — terminate HTTPS with a reverse proxy (nginx/Caddy, HSTS, TLS 1.3) so it answers publicly as
https://keys.<domain>(orhttps://codex.<domain>). Never expose the raw HTTP port to the internet.
./check-domain.sh <domain> probes a domain’s keyserver, mail
endpoints, and DNS, and prints exactly which records to publish based on
what the server actually serves.
Verify
systemctl status eon-keyserver # or eon-keyserver-auth / -directory
curl -s https://keys.<domain>/api/health # {"status":"ok",...}
curl -s https://keys.<domain>/api/server-key # mldsa_pub/ed25519_pub/fingerprintEnd-to-end smoke test with the bundled CLI:
eon-cli identity --data ./d --addr you@<domain>
eon-cli discover --data ./d --addr them@<domain> --keyserver https://keys.<domain>Upgrade and uninstall
# in-place upgrade: replaces the binary + unit, restarts, KEEPS data/env/secrets/DNS
sudo ./upgrade.sh # or: sudo INSTANCE=directory ./upgrade.sh
sudo ./uninstall.sh # stop+disable+remove unit & binaries (keeps data)
sudo ./uninstall.sh --purge # also remove the data dir, env file, and eonkeys userupgrade.sh is non-destructive — database schema changes apply
automatically on restart, so
routine upgrades need no manual migration step.
Troubleshooting
ML-DSA key generation failed ... unsupported / OpenSSL too old.
The linked OpenSSL is older than 3.5. Either install OpenSSL ≥ 3.5
system-wide, or use the static package and export OPENSSL_PREFIX when
running install.sh to skip the version check.
bind: address already in use. Another instance or process is on that
port — change PORT in the instance’s env file (or stop the conflicting
process).
Clients reject discovery. The _eon-keysign TXT fingerprint must
exactly match the current /api/server-key fingerprint. Re-check this
after any signing-key rotation — deleting the signing key file in the data
directory generates a new one, which means you must republish the TXT
record (see Operations).
Publish returns 401. Mailbox-bound publish auth needs
--dovecot-auth-socket (set at install time) and a reachable Dovecot.