Skip to content

Install

Requirements

OSLinux with systemd
Privilegesroot (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).
TLSA reverse proxy (nginx/Caddy) in front, terminating HTTPS
DNSAbility 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
    └── VERSION

Install a single instance

tar xzf eon-keyserver-<version>-linux-amd64.tar.gz
cd eon-keyserver-<version>/
sudo ./install.sh

The installer is interactive (defaults shown, Enter accepts them):

  1. Verifies systemd and OpenSSL ≥ 3.5 (skip this check by exporting OPENSSL_PREFIX when the package is the static build).
  2. Asks for:
    • Domain — e.g. keys.example.com (used only in the printed DNS hints).
    • Bind address (default 0.0.0.0) and port (default 8443). In production, bind to 127.0.0.1 behind 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.
  3. Creates the eonkeys system user/group, the data dir, and /etc/eon-keyserver/keyserver.env (mode 0600).
  4. 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.sh

Re-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:

INSTANCEunitenv filedata dir
(empty)eon-keyserver.servicekeyserver.env/var/lib/eon-keys
autheon-keyserver-auth.serviceauth.env/var/lib/eon-keys/auth
directoryeon-keyserver-directory.servicedirectory.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.sh

See 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.

  1. 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]*"'
  2. 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 discovery

    The _eon-keysign TXT 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.

  3. 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> (or https://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/fingerprint

End-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 user

upgrade.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.