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 a static build (below) or link a newer OpenSSL.
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 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
    └── VERSION

Build 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.gz

The 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-keyservernot a dynamic executable.

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 binary is a 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).
    • 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.

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

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

  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.

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