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 a static build (below) or link a newer OpenSSL. |
| 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 build below, which sidesteps the host’s
OpenSSL entirely.
Get the package
Prebuilt tarball (built with make package-linux or
make package-linux-static on a matching Linux/arch host):
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
└── VERSIONBuild from source (on the target Linux host, or an identical OS/arch — this is a cgo build and does not cross-compile):
git clone <repo> && cd eon-mail
make package-linux # links system OpenSSL >= 3.5
# or, self-contained — no system OpenSSL/libc dependency needed on the install host:
make package-linux-static
# -> dist/eon-keyserver-<version>-linux-amd64.tar.gzThe static build (make package-linux-static) clones and builds a
private OpenSSL ≥ 3.5 into third_party/openssl-static/ and links it (plus,
optionally, glibc) into the binary, so the install host needs no matching
OpenSSL or libc at all. This is the recommended path for a clean Ubuntu
24.04 host, and is what packaging/make-dist.sh produces by default.
Verify a static binary has no runtime dependency with ldd bin/eon-keyserver
→ not a dynamic executable.
Install 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 binary is a 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). - 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.
Provisioning a fresh box as mail server + both keyservers
packaging/provision-mail-keyserver.sh provisions a fresh Ubuntu 24.04 box
as an iRedMail mail server plus both keyserver instances in one run
(preflight → iRedMail install → authoritative instance →
directory instance → printed nginx snippets and DNS anchors). It does not
touch nginx vhosts or run certbot itself — it prints ready-to-use server{}
blocks for you to drop in.
sudo IREDMAIL_VERSION=1.8.2 ./provision-mail-keyserver.sh \
eon-keyserver-<version>-linux-amd64.tar.gzIf iRedMail is already installed and only the keyservers need
(re-)provisioning, source the script and call its install_keyservers
function directly instead of re-running the whole thing (re-running
iRedMail.sh on an already-installed box will refuse).
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.
packaging/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 are additive
(CREATE TABLE IF NOT EXISTS ...) and 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 build with make package-linux-static (or
make keyserver OPENSSL_PREFIX=/path/to/openssl35, rpath-linked) 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. The
register/login endpoints are only a standalone fallback for when
Dovecot isn’t wired up.