# Eoncore Documentation — Full Documentation > Documentation for Eoncore's software products — post-quantum secure networking and communication, network monitoring, and cryptographic visibility. --- title: Client url: https://docs.eoncore.eu/eon-phoenix/client/ --- ## What's in here This section is for anyone using the Eon Phoenix app on desktop or Android — no server administration, no technical background in cryptography required. - [**Getting started**](/eon-phoenix/client/getting-started/) — install the app on your platform and add your first email account (including Google and Microsoft sign-in) - [**Encrypted mail**](/eon-phoenix/client/encrypted-mail/) — sending and receiving encrypted email, how key verification works, and the special message types beyond plain email - [**D-Mail**](/eon-phoenix/client/d-mail/) — chat-style direct messages and encrypted group chat that live inside your regular inbox - [**Interface**](/eon-phoenix/client/interface/) — themes, fonts, and layout options ## The idea in short Eon Phoenix is one inbox for two kinds of conversation: - **Email** — ordinary mail, to and from anyone, exactly as you'd expect. - **D-Mail** — a chat-style conversation with a contact, shown as a message thread instead of a list of separate emails, and always encrypted. Everything you send that isn't plain email is automatically protected with post-quantum encryption, so that only you and your recipient can ever read it — not the mail provider in between, and not (as far as anyone can tell today, or expects to be able to tell even with a future quantum computer) an eavesdropper who recorded the traffic to crack open later. --- title: Concepts url: https://docs.eoncore.eu/eon-path/concepts/ --- These pages explain how EON Path is put together and why it makes the design choices it does: which components exist and how they talk to each other, how post-quantum cryptography is used for authentication and key exchange, how the WireGuard tunnel is established and keyed, and how stateless IPv4/IPv6 translation (SIIT) lets IPv4 traffic reach IPv6-only server networks. - [Architecture](architecture/) — components and how they interact - [Post-Quantum Cryptography](post-quantum/) — ML-DSA-87 authentication and ML-KEM-1024 key exchange - [Tunneling](tunneling/) — WireGuard as the transport - [Address Translation (SIIT)](siit/) — IPv4 ↔ IPv6 translation inside the tunnel --- title: EON Path url: https://docs.eoncore.eu/eon-path/ --- ## What is EON Path? EON Path is a VPN built around post-quantum cryptography. Instead of relying on classical-only key exchange and bearer tokens, it authenticates every client with **ML-DSA-87** (FIPS 204) digital signatures, negotiates session key material with **ML-KEM-1024** (FIPS 203) key encapsulation, and carries traffic over a standard **WireGuard** tunnel. The result is a VPN that is fast to use today and resistant to "harvest now, decrypt later" attacks, in which an adversary records encrypted traffic now in the hope of decrypting it once a sufficiently powerful quantum computer exists. A server daemon terminates tunnels and works with a management API for key lookup; desktop and mobile clients authenticate to it, establish a WireGuard tunnel secured with a post-quantum-derived pre-shared key, and route traffic through it. Where a deployment's internal network is IPv6-only, EON Path can translate IPv4 traffic to IPv6 and back inside the tunnel using stateless IP/ICMP translation (SIIT), so IPv4-only client applications keep working unmodified. ## Key properties - **Post-quantum authentication.** Clients and servers authenticate with ML-DSA-87 public-key signatures in a challenge-response handshake — no passwords, no bearer tokens. - **WireGuard transport.** Once authenticated, traffic flows over a WireGuard tunnel, keeping the low latency and small attack surface WireGuard is known for. - **Mandatory server identity pinning.** Clients verify the server's ML-DSA-87 identity against a pinned fingerprint (or a DNS TXT record) before trusting it, protecting against endpoint impersonation. - **IPv4 ↔ IPv6 translation (SIIT).** Stateless translation lets IPv4 client traffic reach IPv6-only server networks without extra configuration on the client. - **Multi-server clients.** A client is configured with several server endpoints rather than a server with many clients, and the mobile clients hold more than one tunnel open at the same time — each session with its own SIIT prefix, so traffic for each endpoint keeps its own address space. ## Explore the docs - [**Architecture**](/eon-path/concepts/architecture/) — server, API, desktop, and mobile components and how they interact - [**Post-Quantum Cryptography**](/eon-path/concepts/post-quantum/) — ML-DSA-87 challenge-response authentication and ML-KEM-1024 key exchange - [**Tunneling**](/eon-path/concepts/tunneling/) — WireGuard as the transport, keyed from an authenticated exchange - [**Address Translation (SIIT)**](/eon-path/concepts/siit/) — stateless IPv4/IPv6 translation inside the tunnel - [**Installation**](/eon-path/install/) — set up the server, desktop, and mobile clients - [**Configuration**](/eon-path/configuration/) — server and client configuration reference - [**API**](/eon-path/api/) — management API and protocol reference - [**Security**](/eon-path/security/) — threat model and how to report a vulnerability - [**FAQ**](/eon-path/faq/) — frequently asked questions --- title: Getting Started url: https://docs.eoncore.eu/eon-phoenix/client/getting-started/ --- ## Installing the app Eon Phoenix is distributed as beta builds, one set per version, with no installer on any platform — you unpack a file and run it. | Platform | File | Notes | |---|---|---| | macOS | `eon-phoenix--darwin-arm64.zip` (Apple Silicon) or `…-darwin-amd64.zip` (Intel) | unpack, drag **Eon Phoenix.app** into `Applications` | | Windows | `eon-phoenix--windows-amd64.exe` | runs from wherever you put it | | Linux | `eon-phoenix--linux-amd64.tar.gz` | one binary; the cryptography is inside it, no system OpenSSL needed | | Android | `eon-phoenix--android-arm64.apk` | sideload; also runs on /e/OS and other builds without Google services | The Windows build is **Authenticode signed** — Windows shows the publisher as *Quantum Gateway Solutions B.V.* SmartScreen can still ask once, because it warns about any program few people have run yet; check the publisher line before clicking through, and don't run it if the publisher is missing or names someone else. The macOS build is **not signed yet**. macOS calls an unsigned app "damaged", which is the same word it uses for "no publisher certificate" — nothing is actually broken. Right-click the app and choose **Open**, twice, or clear the quarantine flag: ```bash xattr -dr com.apple.quarantine "/Applications/Eon Phoenix.app" ``` The Android APK is signed with a **test key**. A new beta installs straight over the previous one and keeps your data; if the build later moves to a real publisher key, you have to remove the app once and install it again, since Android refuses an update with a different signature — and removing it wipes the app's data. ### Building it yourself `make wails` builds the main client (webview + React frontend), `make gio-app` the native Gio variant on macOS. A bundle you build yourself points at the OpenSSL on your machine and can't be moved to another computer; the published builds carry the cryptography inside the binary. ## Adding your first account The first time you open Eon Phoenix, you'll be prompted to add a mail account. You can add as many accounts as you like later from **Settings → Accounts**. ### Google (Gmail) and Microsoft (Outlook/Office 365) Type your email address. If it's on `gmail.com`, `googlemail.com`, `outlook.com`, `hotmail.com`, `live.com`, or an Office 365 address, Eon Phoenix recognizes the provider automatically and offers **sign-in with Google/Microsoft** instead of asking for a password: 1. Click **Sign in with Google** (or **Microsoft**). 2. Your system's web browser opens the provider's normal sign-in and consent screen. Approve access. 3. Back in Eon Phoenix, click **Add account** — your mailbox is ready. If you're on a custom company domain that uses Google Workspace or Office 365 but doesn't match one of the recognized consumer domains, use the **Microsoft (OAuth)** / **Google (OAuth)** buttons that appear next to the email field to choose the provider by hand. Sign-in this way never hands Eon Phoenix your account password — if you ever revoke access from your Google or Microsoft account security settings, Eon Phoenix will prompt you to sign in again the next time it needs to send or check mail. ### Any other IMAP/SMTP mailbox For any other provider (a company mail server, a self-hosted domain, etc.), enter your email address and password, then click **Detect servers from DNS** — Eon Phoenix looks up the standard mail server records for your domain and fills in the incoming (IMAP) and outgoing (SMTP) server addresses automatically. If nothing is found, your mail provider or IT department can give you the IMAP and SMTP hostnames to enter by hand. There's also an optional **Keyserver** field for this account's domain — leave it blank unless someone (for example, your organization's IT team) has told you to point it at a specific address; otherwise Eon Phoenix finds it automatically when it needs to. ### What happens next Once an account is added, Eon Phoenix generates your personal encryption identity for that address automatically (you'll see this reflected in **Settings → Encryption**), and starts checking your mailbox. See [Encrypted mail](/eon-phoenix/client/encrypted-mail/) for what that identity is used for. ## Multiple accounts Add more accounts anytime from **Settings → Accounts**. Each account gets its **own** separate encryption identity — a fingerprint you see for one address is not the same as the fingerprint for another, even if both addresses belong to you. Switching the active account in the sidebar switches which identity "Publish my keys," "Verify email," and the Encryption settings act on. ## Backing up and moving to a new device Your encryption identity is what makes your encrypted mail readable — losing it without a backup means losing access to messages encrypted to it. From **Settings → Encryption** for an account: - **Reveal recovery phrase + QR** shows a 24-word phrase (and a QR code) that fully restores that account's identity on another device. It's shown once, hides itself automatically after 30 seconds, and should never be photographed, screenshotted, or shared with anyone — whoever has it controls that identity. - On the new device, add the same account, then use **Restore from recovery phrase / code** and paste the phrase (or scan the QR elsewhere and paste the resulting code — the app itself doesn't scan a live camera feed). - **Export account backup** seals your full account list and mailbox passwords to your own identity into a single block of text, which you can paste into **Import account backup** on a device that already has your restored identity — bringing every account over without re-entering passwords or signing in to OAuth accounts again. **Rotating** your identity (also under Settings → Encryption) throws away the old one and generates a brand new one — useful if you believe your identity may have been exposed. Contacts have to re-verify you afterward, and you need to re-publish your new key (a one-click reminder appears right after rotating). Older mail encrypted to the identity you had before your first-ever setup remains readable, since that legacy key is kept specifically for that purpose. --- title: insights url: https://docs.eoncore.eu/eon-insights/tools/insights/ --- ## Purpose `insights` is the main REST API server. It serves the endpoints the web dashboard uses to query captured traffic, hosts, certificates, compliance status, and SCA results, and it handles authentication (JWT sessions backed by FusionAuth, with optional two-factor authentication). It connects to ClickHouse for all data and to Redis for background task enqueueing (TLS/nmap scans triggered from the dashboard) and the WebSocket status feed. ## Usage ``` insights -c [-debug] ``` | Flag | Default | Description | |------|---------|-------------| | `-c` | `/etc/insights.yaml` | Path to the configuration file | | `-debug` | `false` | Enable debug-level logging | The config file also carries a `debug: true` setting; either the flag or the config value turns on debug logging. ## Configuration `configs/insights.yaml` is the template. Key sections: ```yaml name: insights debug: true listen: "localhost:3000" uploadpath: ~/tmp backup_path: ~/tmp/backups db: # ClickHouse (note: uses the generic `db` key, not `clickhouse`) host: localhost port: 19000 database: insights username: insights password: secret fusionauth: baseurl: http://localhost:9011 apikey: applicationid: clientsecret: tenantid: redis: host: localhost port: 6379 channel: insights sca: webhook_base_url: "" oauth: encryption_key: "${SCA_ENCRYPTION_KEY}" # 32-byte hex string github: client_id: "${GITHUB_CLIENT_ID}" client_secret: "${GITHUB_CLIENT_SECRET}" redirect_url: "https://your-domain/oauth/github/callback" gitlab: client_id: "${GITLAB_CLIENT_ID}" client_secret: "${GITLAB_CLIENT_SECRET}" redirect_url: "https://your-domain/oauth/gitlab/callback" ``` On startup, `insights` connects to ClickHouse, loads kickstart defaults into the ClickHouse `config` table (see `internal/config`), and then starts serving HTTP on `listen`. ## API surface All routes are mounted under `/api` and require a JWT (`Authorization: Bearer `) except `/auth/*`, the OAuth callback endpoints, and the webhook endpoints. Route groups include: - `/api/clients`, `/api/hosts`, `/api/handshakes`, `/api/certificates` — query captured traffic and inventory - `/api/hosts/{uuid}/tlsscan`, `/api/hosts/{uuid}/nmapscan` — queue a scan for a specific host - `/api/traffic/{hourly,sixhour,daily,weekly,monthly}` — time-bucketed traffic volume for dashboard charts - `/api/compliance/*` — compliance stats, findings, framework/rule management (see [Compliance](/eon-insights/compliance/)) - `/api/cbom/*` — Cryptographic Bill of Materials generation/download - `/api/sca/*` — repository connections, OAuth, scan triggers and results - `/api/chat/*` — an AI assistant that can execute read-only SQL against the schema for ad-hoc questions - `/api/upload/pcap`, `/api/pcap/*` — PCAP file upload and management (see [pcapworker](/eon-insights/tools/pcapworker/)) - `/api/status`, `/api/status/queues`, `/api/status/workers` — service and asynq queue health - `/auth/login`, `/auth/refresh`, `/auth/two-factor` — authentication - `/oauth/github`, `/oauth/gitlab` (+ `/callback`) — SCA repository OAuth - `/webhooks/github`, `/webhooks/gitlab` — repository push webhooks that trigger SCA scans A global rate limit of 100 requests/minute per IP applies to `/api/*`; a stricter 10 requests/minute applies to `/auth/*` to slow brute-force login attempts. ## Example ```bash go run cmd/insights/insights.go -c configs/insights.yaml -debug ``` --- title: Install url: https://docs.eoncore.eu/eon-phoenix/codex-server/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--linux-amd64.tar.gz └── eon-keyserver-/ ├── 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): ```bash git clone && 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--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-keyserver` → `not a dynamic executable`. ## Install a single instance ```bash tar xzf eon-keyserver--linux-amd64.tar.gz cd eon-keyserver-/ 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`: ```bash 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](/eon-phoenix/codex-server/): 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` | ```bash # 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](/eon-phoenix/codex-server/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. ```bash sudo IREDMAIL_VERSION=1.8.2 ./provision-mail-keyserver.sh \ eon-keyserver--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: ```bash 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.. IN TXT "v=eon-keysign1; fpr=" _eon-keys._tcp.. IN SRV 0 5 443 keys.. ; 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](/eon-phoenix/codex-server/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.` (or `https://codex.`). Never expose the raw HTTP port to the internet. `packaging/check-domain.sh ` probes a domain's keyserver, mail endpoints, and DNS, and prints exactly which records to publish based on what the server actually serves. ## Verify ```bash systemctl status eon-keyserver # or eon-keyserver-auth / -directory curl -s https://keys./api/health # {"status":"ok",...} curl -s https://keys./api/server-key # mldsa_pub/ed25519_pub/fingerprint ``` End-to-end smoke test with the bundled CLI: ```bash eon-cli identity --data ./d --addr you@ eon-cli discover --data ./d --addr them@ --keyserver https://keys. ``` ## Upgrade and uninstall ```bash # 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](/eon-phoenix/codex-server/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. --- title: capture url: https://docs.eoncore.eu/eon-insights/tools/capture/ --- ## Purpose `capture` passively captures handshakes for five protocols — TLS, QUIC (HTTP/3), SSH, IKE/IPsec, and OpenVPN — either from a live network interface or from a PCAP file, and writes extracted metadata to ClickHouse. It never needs private keys and never decrypts payload; it only parses the cleartext parts of each handshake (ClientHello, key-exchange algorithms, certificates, banners) to fingerprint the connection. On Linux, live capture uses raw AF_PACKET sockets; on macOS/BSD it uses libpcap (BPF). ## Usage ``` capture [options] ``` | Command | Captures | |---------|----------| | `tls` | TLS/SSL handshakes (ClientHello/ServerHello) | | `quic` | QUIC/HTTP3 handshakes (ClientHello extracted from Initial packets) | | `ssh` | SSH connections and key exchanges | | `ike` | IKE/IPsec negotiations | | `vpn` | OpenVPN connections | | `all` | All of the above simultaneously | | `version` | Print the tool version | | `help` | Print usage | Options common to the protocol subcommands: | Flag | Description | |------|--------------| | `-c ` | Path to a YAML configuration file | | `-i ` | Network interface to capture from | | `-r ` | Read packets from a PCAP file instead of a live interface | | `-ports ` | Comma-separated list of ports to capture (per-protocol default if omitted: TLS 443, SSH 22, QUIC 443, IKE 500/4500, VPN 1194) | | `-debug` | Enable debug logging | | `-batch ` | Batch size for ClickHouse inserts | | `-host ` | ClickHouse host | | `-port ` | ClickHouse port | | `-db ` | ClickHouse database | | `-dbuser ` | ClickHouse username | | `-dbpass ` | ClickHouse password | | `-redis-host ` | Redis host, for heartbeat/status reporting | | `-redis-port ` | Redis port, for heartbeat/status reporting | | `-noheartbeat` | Disable heartbeat reporting to Redis | `capture all` additionally accepts per-protocol port lists — `-tls-ports`, `-quic-ports`, `-ssh-ports`, `-ike-ports`, `-vpn-ports` — since `-ports` alone would be ambiguous across five protocols. ## Configuration file Instead of (or in addition to) CLI flags, `capture` reads `configs/capture.yaml`: ```yaml pcapfile: "" batchsize: 1 enable_tls: true enable_quic: true enable_ssh: true enable_ike: true enable_vpn: true tls_ports: [443, 8443, 4450, 4451, 4452, 4453] quic_ports: [443, 8443] ssh_ports: [22] ike_ports: [500, 4500] vpn_ports: [1194] debug: false clickhouse: host: localhost port: 19000 database: insights username: insights password: "secret" redis: host: localhost port: 6379 channel: insights ``` Internal-network classification and traffic direction filtering (`internal_networks`, `direction_filter`) are *not* set in this file — they live in the ClickHouse `config` table, editable from the dashboard or directly: ```sql INSERT INTO config (key, values) VALUES ('internal_networks', ['10.0.0.0/8', '192.168.0.0/16']); INSERT INTO config (key, values) VALUES ('direction_filter', ['inbound']); ``` ## Examples ```bash # Capture TLS handshakes live from eth0 sudo capture tls -i eth0 -debug # Capture QUIC/HTTP3 handshakes sudo capture quic -i eth0 -debug # Capture TLS with heartbeat/status reporting to Redis sudo capture tls -i eth0 -redis-host localhost -redis-port 6379 # Capture SSH connections from a PCAP file (no privileges needed) capture ssh -r traffic.pcap # Capture IKE/IPsec on non-default ports sudo capture ike -i eth0 -ports 500,4500,10500 # Capture OpenVPN traffic sudo capture vpn -i eth0 -ports 1194 # Capture all five protocols at once, driven by a config file sudo capture all -i eth0 -c configs/capture.yaml ``` To generate a test PCAP with TLS ClientHello traffic for replay: ```bash sudo tcpdump -i enp4s0 \ "tcp port 443 and (tcp[((tcp[12] & 0xf0) >>2)] = 0x16) && (tcp[((tcp[12] & 0xf0) >>2)+5] = 0x01)" \ -w sample.pcap ``` --- title: Codex Server url: https://docs.eoncore.eu/eon-phoenix/codex-server/ --- ## What is Codex? Codex is Eon Phoenix's per-domain public-key directory: a small standalone service (binary `eon-keyserver`, from the separate Go module `eon-mail/keyserver`) that you deploy alongside a mail domain's existing SMTP/IMAP server. The binary itself serves no web UI — it is an HTTP/JSON API consumed by the Eon Phoenix client (and the `eon-cli` helper), plus an optional administrative API on a separate listener that a standalone admin console talks to. Codex stores and serves **hybrid post-quantum public key bundles** keyed by full email address, so that a Phoenix client anywhere can find and verify the key for `alice@yourdomain.example` before encrypting to her. A bundle is one signing identity (ML-DSA-87 + Ed25519) plus a *set* of encryption keys, one per KEM algorithm — ML-KEM-1024 and FrodoKEM-1344 today — each paired with X25519 and each carrying its own binding signature by that identity. Codex never holds a private key and never sees message content — clients encrypt end-to-end; Codex and the mail server are both untrusted transport. ## The two deployment modes The same `eon-keyserver` binary runs in one of two modes, and a real deployment typically runs **both**, as two separate instances: | Mode | Typical hostname | Who it's for | Publish auth | |---|---|---|---| | **Authoritative** | `keys.` | addresses on a domain you control | bound to mailbox login (Dovecot) | | **Directory ("Codex")** | `codex.` | any mailbox, including Gmail/Outlook/third-party domains you don't control | email-challenge (a code mailed to the address) | The **authoritative** instance is bound to your own mail domain: it only lets a user publish keys for `alice@yourdomain.example` after they prove they can log into that mailbox (via Dovecot). The **directory** instance (commonly the one actually referred to by the name "Codex") accepts open enrollment from *any* mailbox — it mails an 8-digit code to the address and issues the same kind of authorization token once the code is confirmed back. This is the lever that lets a Gmail or Outlook user publish and be discovered under Eon Phoenix's post-quantum scheme without their provider running any Eon software at all. Both instances are the same binary, just started with different flags (see [Configuration](/eon-phoenix/codex-server/configuration/)) and, in production, usually run as two separate systemd services with separate data directories. ## Security model, in one paragraph Private keys are generated on the client and never uploaded — Codex only ever stores **public** key bundles, and verifies each bundle's self-binding before storing it, so a hijacked transport can't substitute an attacker's encryption key. Every discovery response is signed by Codex's own ML-DSA signing key. For the authoritative instance, clients anchor that signing key to a DNSSEC `_eon-keysign` TXT record; for the directory instance (no domain of its own to anchor in DNS), clients instead pin the signing key's fingerprint directly. DKIM/SPF/DMARC on the surrounding mail infrastructure only prove domain routing — sender authenticity inside a message rests solely on the per-user ML-DSA+Ed25519 signature that Codex never sees or touches. ## In this section - [**Install**](/eon-phoenix/codex-server/install/) — build or download the package, install one or both instances, and set up DNS/TLS - [**Configuration**](/eon-phoenix/codex-server/configuration/) — flags, environment file, and the multi-instance layout - [**Operations**](/eon-phoenix/codex-server/operations/) — the mailauth/email-challenge flow, key bundles, escrow tiers, signing-key rotation, and day-to-day service management --- title: Configuration url: https://docs.eoncore.eu/eon-phoenix/codex-server/configuration/ --- ## Where configuration lives `install.sh` writes one environment file per instance to `/etc/eon-keyserver/` (mode `0600`): | Instance | Env file | systemd unit | Data dir | |---|---|---|---| | default (single-instance) | `keyserver.env` | `eon-keyserver.service` | `/var/lib/eon-keys` | | `INSTANCE=auth` | `auth.env` | `eon-keyserver-auth.service` | `/var/lib/eon-keys/auth` | | `INSTANCE=directory` | `directory.env` | `eon-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`: | Flag | Env | Meaning | |---|---|---| | `--data-dir` | — | Directory for the database and signing identity (default `.`) | | `--bind` | — | Bind address (default `127.0.0.1`) | | `--port` | — | Listen port (default `8443`) | | `--jwt-secret` | `JWT_SECRET` | Signs the mailauth/mailverify JWTs | | `--db-key` | `DB_ENCRYPTION_KEY` | SQLCipher database encryption key | | `--dovecot-auth-socket` | — | Dovecot auth-client socket path; enables mailbox-bound publish auth (**authoritative** mode) | | `--smtp-addr` | — | SMTP 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-enrollment` | — | Allow 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-change` | — | Let 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-listen` | — | `host: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-eyes` | — | Comma-separated administrative actions that need a second administrator's approval: `user.delete`, `user.disable`, `user.enable`. Empty (default) gates nothing | | `--allow-unencrypted-db` | — | Start 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-from` | — | `From:` address for verification emails; **required** when `--smtp-addr` is set (the process refuses to start otherwise) | | `--trusted-proxy` | — | Comma-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-fingerprint` | — | Print the signing-key fingerprint (and DNS anchor line) and exit — offline, no server start, no JWT/DB needed | | `--version` | — | Print 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): ```bash JWT_SECRET="" DB_ENCRYPTION_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): ```bash JWT_SECRET="" DB_ENCRYPTION_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: ```bash 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: ```bash curl -sk -o /dev/null -w '%{http_code}\n' -X POST \ -u 'user@domain:PASSWORD' https://keys./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.. IN TXT "v=eon-keysign1; fpr=" _eon-keys._tcp.. IN SRV 0 5 443 keys.. ; optional discovery keys.. IN A ``` 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. --- title: Encrypted Mail url: https://docs.eoncore.eu/eon-phoenix/client/encrypted-mail/ --- ## Encrypting a regular email When composing a plain email, two checkboxes at the bottom of the compose window control protection: **Encrypt** and **Sign**. Turning either one on reveals a scheme picker: - **PGP** — standard OpenPGP encryption, compatible with other PGP-capable mail clients. - **S/MIME** — standard S/MIME (certificate-based) encryption, compatible with other S/MIME-capable clients. - **Eon-PQC (hybrid, E2E)** — Eon Phoenix's own post-quantum scheme, described below. Recommended when your recipient also uses Eon Phoenix. Signing proves the message really came from you (recipients can verify the signature); encrypting hides the content from everyone but the recipient. You can do either independently, or both together. Some accounts have **mandatory encryption** turned on (an option in account settings) — for those, the compose window refuses to send plain, unencrypted mail and reminds you to pick a secure option instead. ## The Eon-PQC scheme Eon-PQC is a hybrid post-quantum encryption scheme: it combines a post-quantum algorithm with a classical one for both confidentiality and signing, so that breaking the encryption would require breaking *both* the post-quantum and the classical half at once. It protects attachments as well as the message body, and a copy of what you send is always re-encrypted to yourself too, so your own Sent folder stays readable. This is the same underlying protection used for D-Mail (see [D-Mail](/eon-phoenix/client/d-mail/)) and for the special message types below — the difference between them is not *how strong* the encryption is (it's always the same), but *who can read it, for how long, and whether the metadata is hidden too*. ### Your key set Your identity holds a *set* of encryption keys, one per post-quantum algorithm, not a single fixed key. ML-KEM-1024 and FrodoKEM-1344 are derived for every identity; both are paired with X25519. Whoever writes to you is sent the strongest algorithm the two of you have in common, chosen by their client without asking you or them anything. **Settings → Encryption → Security** lists the set: per key the algorithm, its family, NIST level, status, and whether the keyserver has it yet. An algorithm that arrives with an update gets its key derived and published on its own — nothing to do, and your fingerprint does not change, so contacts don't have to re-verify you. A retired algorithm keeps opening old mail; it is only no longer offered to senders. If your identity has no recovery code, the table says so too: keys created on that device cannot be recovered anywhere else. ### What protected one message Right-click a message and choose **Info** (on Android: open it and pick **Info** among the actions) for what actually happened to that one message: | Block | Shows | |---|---| | Protection | the tier, the modifiers (read-once, discrete) and the envelope version | | Encryption | the post-quantum algorithm that sealed your copy, its family and NIST level, the classical half (X25519), the content cipher (AES-256-GCM), and which of your keys opened it | | Signature | ML-DSA-87 and Ed25519, whether they verified, the sender's fingerprint, and how that sender is trusted | | Transport | Message-ID and the number of attachments | These facts are captured the moment the message is opened. A read-once message you haven't opened yet only reports that it is still sealed — filling the dialog would spend the single read. ## Finding and verifying a contact's key To send Eon-PQC-encrypted mail, Eon Phoenix needs your recipient's public key. This usually happens automatically: - The first time you send to a new address, Eon Phoenix looks the recipient up and, if found, **pins** their key — this is "trust on first use": the very first key you see for someone is trusted, and any *later* change to that key is flagged so you can decide whether to accept it. - In the compose window, each recipient shows a small indicator next to their address: a lock icon means Eon Phoenix already has a pinned key for them and can send Eon-PQC mail immediately; a dash means it doesn't yet, and it will try to find one automatically when you hit send. - If a recipient can't be found (for example, an address that doesn't use Eon Phoenix or a compatible key directory yet), Eon Phoenix simply falls back to whatever encryption you've set up for that message — it never silently sends something as plaintext that you asked to be encrypted. ### If you and a contact aren't discoverable yet Two people can still get to encrypted mail with each other before any automatic discovery works, in two ways: - **Share your key bundle directly.** In **Settings → Encryption → My key bundle**, click **Show my key bundle** and copy the block of text (or send it to your contact by any channel you trust). They paste it into **Import contact key bundle**, with your email address, on their side — and vice versa. - **Cold-start via a regular email.** If you send an ordinary (unencrypted or non-Eon-PQC) email, Eon Phoenix quietly attaches your own key bundle to it. A contact who also uses Eon Phoenix picks it up automatically the next time they reply, and from then on you're both set up for encrypted mail — no manual steps needed on either side. ### When a contact's key changes If a contact's key ever changes — for example because they set up a new device or their key was rotated — Eon Phoenix pauses sending to them and shows a banner explaining that their key changed, with the old and new fingerprints shown side by side. You choose **Accept** to trust the new key and resume, or leave it paused if the change looks unexpected (for example, if you haven't heard from them about switching devices). A setting in **Settings → Encryption → Key-change trust** controls how cautious this is by default: **Trust keyserver** auto-accepts changes that come from your contact's own verified key directory, while **Strict** requires you to manually approve every single key change yourself. ## Special message types Beyond plain email and D-Mail chat threads (see [D-Mail](/eon-phoenix/client/d-mail/)), the message-type picker at the top of the compose window offers a few more options for specific situations. Each is built on the same Eon-PQC encryption — they differ in *who can read it and for how long*, not in strength: - **Apax** (single-release) — a message that can be opened exactly once. Good for sharing a one-time secret or access code. Combine it with the **self-erase** option and the message is destroyed the moment it's opened, leaving nothing behind afterward. - **Confidential** — an online-only message: your recipient's app fetches and decrypts it fresh every time they open it, and it's never actually stored anywhere as plaintext. This lets *you*, the sender, truly **unsend** it later — one click destroys every copy, everywhere, immediately, even if the recipient already read it once. - **Dual-control** — a message that only opens once *both* the recipient opens it **and** a separate named "approver" you designate also consents. Useful when a message should require two people to agree before it's released. These, along with D-Mail, all support an optional **Discrete** toggle, described next. ## The Discrete toggle D-Mail and the special message types above share one more compose-window option: a **Discrete** toggle. Clicking it cycles through three states — off, **discrete** (the default level once you turn it on), and **Discrete (full)** — and each level protects something different. Neither level makes a conversation anonymous to the two people actually having it, and neither hides that a message was sent, when, or roughly how large it was. - **Discrete** (one click) — stops the outgoing message from labeling itself, in cleartext, with *which kind* of message it is and *which conversation or group* it belongs to, once it's sitting in your recipient's mailbox. This is genuinely useful — without it, anyone with access to the mailbox at rest (a compromised account, a backup, an admin) can see at a glance which of your messages are D-Mail, Dual-control, etc., and which ones belong together, without decrypting anything. **What it does not do:** your and your recipient's real email addresses are still the actual `From`/`To` on the message — mail simply can't be delivered otherwise — so anyone who can see those addresses (either mailbox, or a mail server relaying the message) can still tell *who is talking to whom*. This level only hides the extra grouping tags, not the conversation itself. - **Discrete (full)** (a second click) — a separate, stronger step you opt into explicitly. It asks a cooperating Eon Phoenix mail gateway to swap your and your recipient's real addresses for one-time, throwaway tokens while the message is in transit between mail servers, so a server in between sees only meaningless tokens, never your real addresses. This only works when both your organization and your recipient's have set up cooperating gateways with each other in advance — it isn't something a single user can turn on unilaterally. Even then, the domain names on both ends (e.g. `yourcompany.com` ↔ `theirs.example`) stay visible, and of course you and your recipient both still know who you're talking to. A third click turns Discrete back off. --- title: Eon Insights url: https://docs.eoncore.eu/eon-insights/ --- ## What is Eon Insights? Eon Insights is a passive and active network security monitoring platform. A set of collector daemons capture TLS, QUIC, SSH, IKE/IPsec, and OpenVPN handshakes from live interfaces or PCAP files and extract metadata — negotiated protocol version, cipher suite, JA3/JA3S/JA4 fingerprints, certificate details — without decrypting the traffic itself. That metadata lands in ClickHouse, where it's queried by a REST API and a Svelte dashboard to answer questions like "which hosts still speak TLS 1.0?" or "which certificates expire this month?" Beyond passive capture, Eon Insights actively scans discovered hosts: `tlsscand` re-probes each host's TLS configuration, `nmapscand` fingerprints open ports and services, and an SCA (Software Composition Analysis) subsystem scans connected Git repositories for vulnerable dependencies. A compliance engine evaluates the collected data against configurable rules for PCI DSS, NIST SP 800-52, GDPR, HIPAA, NIS2, and DORA, and a notification daemon alerts on expiring certificates and known-malicious TLS fingerprints. ## Key properties - **Passive capture, no decryption.** `capture` reads TLS/QUIC ClientHello and ServerHello messages, SSH key exchange banners, IKE/IPsec SA proposals, and OpenVPN handshakes off the wire (or from a PCAP file) and extracts metadata only — no private keys are needed and no payload is decrypted. - **Fingerprinting.** JA3, JA3S, and JA4 client/server fingerprints are computed for every TLS handshake and checked against a database of known browser, tool, and malware fingerprints (`ja3import`, `fingerprintd`). - **Active scanning.** `tlsscand` and `nmapscand` are asynq-backed worker daemons that scan hosts discovered through capture (or queued via the API) for supported TLS versions/ciphers and open ports/services. - **Software Composition Analysis.** The `sca` CLI and `scascand` daemon scan Maven, npm, Go, and Cargo manifests in Git repositories for known vulnerable dependencies. - **Compliance reporting.** A rule engine evaluates captured traffic and certificates against PCI DSS v4.0, NIST SP 800-52 Rev 2, GDPR Article 32, HIPAA, NIS2, and DORA, and can be queried through the dashboard or the `/api/compliance` endpoints. - **ClickHouse-backed.** All traffic metadata, scan results, certificates, and compliance rules are stored in ClickHouse, and migrations are managed by the separate `dbmigrate` tool. ## Explore the docs - [**Getting Started**](/eon-insights/getting-started/) — install, set up the database, run your first capture and scan, and view the results - [**Tools**](/eon-insights/tools/) — every CLI tool and daemon: purpose, flags, and examples - [**Dashboard**](/eon-insights/dashboard/) — what the web dashboard shows and how to run it - [**Compliance**](/eon-insights/compliance/) — the frameworks, rules, and how compliance reports are generated --- title: Getting Started url: https://docs.eoncore.eu/eon-insights/getting-started/ --- ## Overview This page walks through building Eon Insights from source, initializing the ClickHouse schema, running a capture and a scan, and viewing the results in the dashboard. It assumes a Linux or macOS development machine with Go 1.24+, Docker, and Redis available. ## Prerequisites - Go 1.24 or newer (see `go.mod`) - ClickHouse (traffic metadata, scan results, certificates, compliance rules) - Redis (task queue for the `tlsscand`/`nmapscand`/`notifyd` daemons, and service heartbeats) - `libpcap` headers for the `capture` tool's live-capture build (macOS/BSD path; Linux uses raw sockets) ## Build the tools Eon Insights is a Go monorepo (module `insights`). Every command lives under `cmd/` and has a matching Makefile target — **do not** run `go build` directly, since that scatters binaries into the repo root instead of `bin///`: ```bash # Build everything make all # Or build one tool for linux/arm64, linux/amd64, and darwin/arm64 make insights make capture ``` Binaries land in `bin///`, e.g. `bin/darwin/arm64/insights`. ## Set up the database Database migrations are handled by a standalone tool, `dbmigrate`, not by anything inside this repo: ```bash go install github.com/quantumgateway/dbmigrate@latest ``` Start ClickHouse (a plain container is enough for development): ```bash docker run -d --name insights-clickhouse \ -p 19000:9000 -p 18123:8123 \ clickhouse/clickhouse-server ``` Then initialize the schema, pointing `dbmigrate` at the SQL migration list shipped in this repo: ```bash dbmigrate -e clickhouse -h localhost -p 19000 -U default \ -db insights -path deployments/insights/sql/2.0.0/index.lst ``` `dbmigrate` also accepts `-W` to prompt for a password interactively, or `-password ` to pass one on the command line. Add `-data test/data/csv` to load the CSV fixtures alongside the schema, useful for a throwaway development database. Start Redis as well, since the scan/notification daemons depend on it: ```bash docker run --name insights-redis -d -p 6379:6379 redis ``` ## Configure and run the API server `insights` is the REST API the dashboard talks to. Copy and edit `configs/insights.yaml` (ClickHouse connection, listen address, FusionAuth settings for login), then run it: ```bash go run cmd/insights/insights.go -c configs/insights.yaml -debug ``` See [insights](/eon-insights/tools/insights/) for the full flag reference. ## Generate test data (optional) If you don't have a live network segment to capture from, `datagen` populates ClickHouse with realistic synthetic hosts, clients, TLS handshakes, and certificates so you have something to look at immediately: ```bash go run cmd/datagen/datagen.go -h localhost -p 19000 -U default -db insights \ -d 30 -q 5000 -hosts 50 -clients 100 ``` See [datagen](/eon-insights/tools/datagen/) for all flags. ## Run your first capture To capture real traffic instead, run `capture` against a live interface (requires root/raw-socket privileges) or an existing PCAP file: ```bash # Live TLS capture (needs sudo on most systems) sudo go run ./cmd/capture tls -i eth0 -debug \ -host localhost -port 19000 -db insights # Or replay a PCAP file — no privileges required go run ./cmd/capture tls -r /path/to/capture.pcap \ -host localhost -port 19000 -db insights ``` `capture` is a multi-file `package main` (`main.go` plus one file per protocol), so it must be run as a directory (`./cmd/capture`), unlike the single-file tools below. See [capture](/eon-insights/tools/capture/) for the other protocols (`quic`, `ssh`, `ike`, `vpn`, `all`) and their flags. ## Run your first scan Once hosts start showing up in the `hosts` table (either from `capture` or `datagen`), start `hostsync` to discover them and enqueue scan tasks, and `tlsscand`/`nmapscand` to process the queue: ```bash go run cmd/hostsync/hostsync.go -c configs/hostsync.yaml -once go run cmd/tlsscand/tlsscand.go -c configs/tlsscand.yaml -debug go run cmd/nmapscand/nmapscand.go -c configs/nmapscand.yaml -debug ``` Or scan a single endpoint directly from the command line without going through the queue at all: ```bash go run cmd/tlsscan/tlsscan.go https://example.com ``` ## View it in the dashboard The dashboard is a Svelte app under `web/dashboard`. In development it proxies API calls to the `insights` server: ```bash cd web/dashboard npm install npm run dev ``` Open the printed local URL (typically `http://localhost:5173`) and log in against the FusionAuth instance configured in `configs/insights.yaml`. See [Dashboard](/eon-insights/dashboard/) for what each page shows. ## All-in-one: devctl For local development, `devctl` is a terminal UI that starts/stops every daemon (`insights`, `capture`, `hostsync`, `tlsscand`, `nmapscand`, `scascand`, `pcapworker`, `notifyd`, `fingerprintd`, the dashboard dev server, and the `tlslab` TLS test server) from one screen, streams their logs, and can fire off `datagen` or a synthetic-traffic generator on demand. Run it from the project root: ```bash go run cmd/devctl/devctl.go ``` See [devctl](/eon-insights/tools/devctl/) for details. --- title: Install url: https://docs.eoncore.eu/eon-path/install/ --- ## Installing EON Path EON Path has three kinds of components: a server you deploy on Debian or Ubuntu, a desktop client for connecting to it, and mobile apps for iOS and Android. Pick the guide for what you're installing: - [**Server**](server/) — install the `.deb` packages on Debian/Ubuntu, start the systemd services, and open the firewall - [**Desktop**](desktop/) — supported platforms, installing the client, and connecting to a server for the first time - [**Mobile**](mobile/) — current status of the iOS and Android apps --- title: Configuration url: https://docs.eoncore.eu/eon-path/configuration/ --- ## Configuration surfaces EON Path's components are configured differently depending on how they're deployed: - The **server daemon** (`eonpath-server`) is configured through an environment file, `/etc/eonpath/server.env`, read by its systemd unit. A small number of less common settings are only available as command-line flags. See [Server](server/). - The **desktop and mobile clients** are configured through a YAML file, `eonpath.conf`, that lists the servers and endpoints you connect to. See [Client](client/). - The **API server** (`eonpath-api`) is configured through its own environment file, `/etc/eonpath/api.env`, covered in the [server install guide](/eon-path/install/server/#configure-the-server) since it ships alongside the server daemon. ## File locations | Component | Configuration file | State directory | |-----------|--------------------|-----------------| | `eonpath-server` | `/etc/eonpath/server.env` | `/var/lib/eonpath/` | | `eonpath-api` | `/etc/eonpath/api.env` | `/var/lib/eonpath/api/` | | Desktop client | `~/.config/eonpath/eonpath.conf` (Linux/Windows) or `~/eonpath.conf` (macOS) | OS keyring (auth tokens), `~/.config/eonpath/` | After editing a server-side environment file, restart the corresponding systemd service to apply the change: ```bash sudo systemctl restart eonpath-server sudo systemctl restart eonpath-api ``` ## Format notes - **Environment files** (`server.env`, `api.env`) use `KEY=value` lines, parsed by systemd's `EnvironmentFile` directive. Values are not quoted. Lines starting with `#` are comments. - **`eonpath.conf`** is a YAML file. It's read and (when you change settings in the client UI) rewritten by the application, so hand edits are preserved but comments are not. See [Server](server/) and [Client](client/) for the full key reference, or [API](/eon-path/api/) for the REST management API and the VPN authentication protocol. --- title: D-Mail url: https://docs.eoncore.eu/eon-phoenix/client/d-mail/ --- ## What is D-Mail? D-Mail (short for **Discrete Mail**) turns an email address into a direct-message identity. It's still email under the hood — reachable at the same address, carried by the same mail servers — but Eon Phoenix presents it completely differently in your inbox: as a **chat-style conversation** with one contact, rather than a list of separate messages. D-Mail is **always** end-to-end encrypted with Eon Phoenix's hybrid post-quantum scheme — there's no "unencrypted D-Mail" option. If Eon Phoenix can't find a usable encryption key for the person you're chatting with, it refuses to send rather than quietly falling back to something less secure. Your inbox shows email and D-Mail side by side, clearly labeled, for example: ``` [Email] Subject: Invoice Q2 [D-Mail] Subject: Encryption key update ``` ## Starting and using a conversation Open or start a D-Mail conversation from the inbox the same way you'd open any item — pick an existing conversation, or start a new one and type in the contact's address. Type your message in the chat box at the bottom and send; replies from your contact appear in the same thread. ### Subject threads A D-Mail conversation with one contact can carry several separate sub-conversations at once, grouped by an optional **subject**. Leave the subject blank for a running, general chat; set a subject (shown as a small bar above the message box) to start — or continue — a labeled sub-thread within the same conversation, for example "Project X" or "Trip planning." The chat view shows a labeled divider each time the subject changes, so separate topics with the same contact stay visually distinct without opening a whole new conversation. Because the subject travels inside the encrypted message itself, it never appears in plain text to anything carrying the mail along the way. ### Read-once messages Any message in a D-Mail conversation can be sent with the **read-once** (burn-after-reading) option turned on. Your own copy always stays visible to you, marked as sent read-once; for your recipient, opening it reveals the content once — after that, reopening the conversation shows only that it was burned. Just seeing a new-message notification or a background refresh does **not** burn it; only your recipient actually opening it does. ### Discrete conversations D-Mail conversations support the same **Discrete** toggle described in [Encrypted mail](/eon-phoenix/client/encrypted-mail/#the-discrete-toggle). The default **discrete** level hides the cleartext tags Eon Phoenix uses to label and group your messages at rest in your recipient's mailbox — it does **not**, by itself, hide who you're talking to, since the real addresses are still needed to deliver the mail. A second, stronger **Discrete (full)** level asks a cooperating mail gateway to substitute disposable tokens for your addresses while the message is in transit; that only works between organizations that have set gateways up with each other in advance. See [Encrypted mail](/eon-phoenix/client/encrypted-mail/#the-discrete-toggle) for the full breakdown of what each level protects. ## Encrypted group chat Besides one-to-one conversations, D-Mail also supports encrypted **group** chats. Click the group icon next to **Compose** in the sidebar, give the group a name, and enter your contacts' email addresses (comma-separated) to create it — Eon Phoenix looks up an encryption key for every member first and refuses to create the group if it can't find one for someone, the same fail-closed behavior as a regular D-Mail. Once created, the group appears in your inbox list alongside your other conversations, labeled with its name and a post-quantum group badge. A group chat works like a D-Mail conversation with more than one other person in it: everyone's messages appear in one shared, end-to-end encrypted thread, each labeled with its sender, and the whole group is kept in sync every time you open it. Groups don't currently support subject threads or the read-once/Discrete modifiers available in one-to-one D-Mail. ## Deleting You can remove a single message from a conversation, or delete an entire conversation, from the chat view. Deleting removes it from your own view; it does not reach into your contact's mailbox. ## D-Mail vs. regular email, at a glance | | Email | D-Mail | |---|---|---| | Shown as | separate messages | one chat thread per contact | | Encryption | optional, your choice | always on | | Reaches | anyone with an email address | contacts Eon Phoenix can find an encryption key for | | Best for | ordinary correspondence | private, back-and-forth conversation | --- title: Eon Aethis url: https://docs.eoncore.eu/eon-aethis/ --- ## What is Eon Aethis? Eon Aethis is a monitoring platform that combines the Icinga 2 monitoring engine with an AI configuration layer and passive network visibility. Instead of hand-writing Icinga host/service objects, an operator can type a prompt like "monitor CPU and memory on server-A and put it in hostgroup-web" and Eon Aethis translates it into the corresponding Icinga 2 API calls and executes them immediately, returning the results in the same response. Alongside active checks (ping, HTTP, CPU, disk, and other Icinga 2 checks), Eon Aethis passively observes network traffic: Zeek extracts protocol-level logs from a monitored interface, and the Aethis Probe parses live packets and NetFlow/IPFIX/sFlow exports to classify devices and flows without needing an agent on every host. A separate Aethis Agent can be installed on individual hosts for local metrics and Nagios-plugin-compatible checks, and a multi-tenant Perimeter Probe performs external reconnaissance scans against a tenant's public-facing infrastructure. The platform is delivered as a set of Docker Compose services (a Go API backend, React frontend, Icinga 2, PostgreSQL, VictoriaMetrics, ClickHouse, Redis, Zeek, and a Caddy reverse proxy) plus standalone binaries for the Agent, Probe, and Perimeter Probe that are installed separately on the hosts and networks they observe. ## Key properties - **AI-assisted configuration.** A chat-style interface turns natural-language prompts into Icinga 2 host, service, and hostgroup changes. Proposed actions execute immediately in the same request — there is currently no dry-run, conflict check, or operator-confirmation gate before changes are applied. - **Active and passive monitoring together.** Icinga 2 active checks sit alongside Zeek protocol logs and NetFlow/IPFIX/sFlow flow data in the same dashboards. - **Automatic discovery.** Network scans (Nmap-style port scanning), SNMP, cloud provider APIs (Azure, AWS), and passive traffic analysis feed an approval inbox that proposes hosts, services, and dependencies instead of requiring them to be entered by hand. - **Custom dashboards.** A drag-and-drop dashboard builder with its own Apache ECharts-based graph engine (no Grafana dependency), plus templates for common host types. - **Distributed collection.** The Aethis Agent, Aethis Probe, and Perimeter Probe are independent binaries that pair with the central platform over a token or certificate-based flow, so collection can run close to the hosts and networks being observed. - **Hybrid and multi-cloud.** Cloud connections for AWS and Azure import resources as monitored hosts alongside on-premise infrastructure. ## Explore the docs - [**Architecture**](/eon-aethis/architecture/) — the backend, Icinga 2, storage, and collection components and how they interact - [**Installation**](/eon-aethis/install/) — deploy the central platform with Docker Compose - [**Agents & Probes**](/eon-aethis/agents-and-probes/) — install the Aethis Agent, Aethis Probe, and Perimeter Probe - [**Configuration**](/eon-aethis/configuration/) — environment variables and config file reference --- title: hostsync url: https://docs.eoncore.eu/eon-insights/tools/hostsync/ --- ## Purpose `hostsync` is a daemon that periodically scans the `tls_hellos` table for destination IPs that aren't yet in the `hosts` table (and source IPs not yet in `clients`), inserts them, and enqueues TLS and nmap scan tasks (via Redis/asynq) for each newly discovered host. It also refreshes `last_seen` timestamps for clients seen in the last hour. Without `hostsync` running, hosts observed by `capture` never get scanned or shown as "hosts" in the dashboard — they only exist as rows in `tls_hellos`. ## Usage ``` hostsync -c [-debug] [-once] ``` | Flag | Default | Description | |------|---------|-------------| | `-c` | — | Path to config file (required) | | `-debug` | `false` | Enable debug logging | | `-once` | `false` | Run a single sync pass and exit, instead of looping | Running with no flags at all prints usage and exits without doing anything. ## Configuration `configs/hostsync.yaml`: ```yaml name: hostsync clickhouse: host: localhost port: 19000 database: insights username: insights password: secret redis: host: localhost port: 6379 debug: false sync_interval: 5m # e.g. "5m", "1h" — defaults to 5m if unset ``` On startup, `hostsync` validates that the `hosts`, `clients`, and `tls_hellos` tables exist in ClickHouse before starting the sync loop, and reports a Redis heartbeat (visible on the dashboard's status page) if `redis.host` is set. ## Examples ```bash # Run as a daemon, syncing every sync_interval hostsync -c configs/hostsync.yaml # Run a single sync and exit (useful right after a fresh capture) hostsync -c configs/hostsync.yaml -once # Run with debug logging hostsync -c configs/hostsync.yaml -debug ``` --- title: Operations url: https://docs.eoncore.eu/eon-phoenix/codex-server/operations/ --- ## HTTP API surface Codex has no web UI; everything is a JSON endpoint consumed by the Eon Phoenix client and `eon-cli`: | Method | Path | Auth | Purpose | |---|---|---|---| | GET | `/api/health` | — | liveness | | POST | `/api/auth/register`, `/api/auth/login` | — | standalone account (fallback when Dovecot isn't wired) | | POST | `/api/mailauth/token` | HTTP Basic (mailbox password) | mailbox-bound token (needs `--dovecot-auth-socket`) | | POST | `/api/mailverify/start` | — | directory mode: mail an enrollment code to a mailbox | | POST | `/api/mailverify/confirm` | — | directory mode: confirm the code, issue a mailauth token | | POST | `/api/my/bundle` | Bearer | publish a key bundle — identity plus its whole KEM key set; every per-key binding signature is verified before storage | | POST | `/api/my/bundle/confirm` | Bearer | confirm a staged key *replacement* with the code mailed to the mailbox | | POST | `/api/my/bundle/deny` | Bearer | report a staged replacement as not yours | | GET | `/api/bundle/user/:username` | — | discover a bundle (includes `server_sig`) | | GET | `/.well-known/eon-bundle/:username` | — | discovery alias | | GET | `/api/server-key` | — | Codex's own signing key (for the DNSSEC anchor / fingerprint pin) | | GET/POST | `/api/apax/{challenge,fetch}`, `/api/apax/escrow` (Bearer), `/api/apax/burn` (Bearer) | mixed | single-release ("Apax") escrow | | GET/POST | `/api/cmail/{challenge,fetch}`, `/api/cmail/escrow` (Bearer), `/api/cmail/burn` (Bearer) | mixed | online-only ("Confidential") escrow | | GET/POST | `/api/dctrl/{challenge,consent,fetch}`, `/api/dctrl/escrow` (Bearer), `/api/dctrl/burn` (Bearer) | mixed | dual-control escrow | | GET/POST/DELETE | `/api/ephemeral/{challenge,fetch}`, `/api/ephemeral/escrow` (Bearer), `/api/ephemeral/:keyId` (Bearer, DELETE) | mixed | single-read ephemeral escrow (backs D-Mail's self-erase) | ### Administrative API The `/api/admin/*` surface is mounted **only** when the daemon is started with `--admin-listen`; without that flag the routes don't exist at all, rather than existing and returning `401`. Mount it on its own listener (loopback, fronted by your own proxy and access control) — it decides which public keys the world sees for the domain. | Method | Path | Purpose | |---|---|---| | POST | `/api/admin/login` | administrator login, returns a bearer token (rate-limited) | | GET | `/api/admin/whoami` | the calling administrator | | GET | `/api/admin/stats` | instance counters | | GET | `/api/admin/users`, `/api/admin/users/{username}` | list and inspect accounts | | POST | `/api/admin/users/{username}/disable` | disable an account | | DELETE | `/api/admin/users/{username}` | delete an account | | GET | `/api/admin/keys/{username}` | the published key set for an account | | GET | `/api/admin/audit`, `/api/admin/audit/verify` | read the hash-chained admin audit trail, and verify the chain | | GET | `/api/admin/pending` | the four-eyes queue | | POST | `/api/admin/pending/{id}/approve`, `/api/admin/pending/{id}/withdraw` | approve (by a *different* administrator) or withdraw a queued action | Nothing on this surface touches key material, releases a key half, or changes escrow. ## Publishing a key set, and replacing a key A bundle is an identity (ML-DSA-87 + Ed25519) plus a **set** of hybrid KEM keys, one per algorithm, each carrying its own binding signature by that identity. Codex verifies every binding before storing it and can neither add nor swap a key of its own. Republishing the same identity with valid bindings updates the key set — that is how a client that gained a new algorithm gets it published, without a fingerprint change and without touching anyone's pin. **Replacing** an existing published key is a different operation, and the one an attacker wants: it makes every future correspondent encrypt to their key instead of yours. A replacement is therefore staged as pending, an 8-digit code is mailed to the mailbox, and nothing changes until the owner enters that code in their own client (`/api/my/bundle/confirm`). The code is entered in the client, never clicked as a link in mail. | Bound | Value | |---|---| | Staged replacement expires after | 24 hours | | Code attempts | 5 | | Replacement mails per account per day | 3 | | After a denial (`/api/my/bundle/deny`) | replacement stays blocked for a cooldown | Republishing a key that is already staged reports as **pending**, not as an error. This flow needs `--smtp-addr`; an operator who runs no mail at all can fall back to the pre-confirmation behaviour with `--allow-unconfirmed-key-change`, which means a valid publish token is again enough to swap someone's published key. Confirmation does not beat mailbox takeover — an attacker who fully controls the mailbox reads the code too. It closes the window, raises the alarm and adds delay. ## Mailauth: the two ways a client proves ownership of an address Publishing a key bundle for an address requires proving control of that mailbox first. Codex supports two proofs, selected by how the instance is configured (see [Configuration](/eon-phoenix/codex-server/configuration/)): - **Dovecot mailbox auth** (authoritative instances) — the client presents the mailbox's IMAP/SMTP username and password to `POST /api/mailauth/token`; Codex checks them against Dovecot over the `auth-client` socket and, on success, issues a bearer token scoped to that address. This happens transparently the first time the desktop app logs into an account on a domain running its own Codex instance. - **Email-challenge / "mailverify"** (directory instances) — for mailboxes Codex doesn't control the domain of (Gmail, Outlook, anything else): 1. `POST /api/mailverify/start {"mailbox": "..."}` always returns `{"status":"sent"}` (never reveals whether the address exists) and emails an 8-digit code to that mailbox via the configured SMTP relay. 2. `POST /api/mailverify/confirm {"mailbox": "...", "code": "..."}` returns `{"token": ""}` on the right code within the window, or `401` on a wrong/expired one. 3. Codes expire after **10 minutes**, are **single-use**, and are capped at **5 attempts**. A background sweep clears expired challenge rows periodically; nothing manual is required. 4. `404` from either endpoint means the instance was started **without** `--smtp-addr` — it isn't running in directory mode. The issued JWT is the same kind of mailbox-ownership proof the Dovecot path produces, so once mailverify succeeds, every escrow tier (Apax, Confidential, dual-control, ephemeral) is unlocked the same way — they all sit behind the same bearer-token middleware. ### Mail deliverability for the challenge sender Because the directory instance only *sends* mail (it never receives), all the usual anti-spam plumbing has to be right for Gmail/Outlook to actually inbox the code: - Relay through your existing mail server rather than sending directly — set `--smtp-addr` to a null-client Postfix relay (or your MTA on `:25`, not `:587`/submission, since the keyserver sends unauthenticated) and `--smtp-from` to an address on your primary, already-warmed domain. - SPF must list the relay's IP for the `--smtp-from` domain; DKIM must be signed **by that same domain** (check your MTA's per-domain signing config — a catch-all "sign everything as the server's own domain" rule will misalign DKIM for `--smtp-from`). - Force IPv4 to Gmail if your relay has IPv6 configured but lacks a matching PTR — Gmail rejects IPv6 senders that fail its stricter requirements there. - Even with SPF/DKIM/DMARC all passing, a cold sending domain can still get spam-foldered on reputation alone; this improves as the domain sends more legitimate mail over time, and recipients marking messages "not spam" helps immediately. ## Key bundles A published bundle is the recipient's public material for one address: an ML-KEM-1024 encapsulation public key, an X25519 public key, an ML-DSA-87 signature public key, and an Ed25519 signature public key — never anything private. `POST /api/my/bundle` verifies the bundle is **self-bound** (signed by the identity it claims to belong to) before storing it, so a compromised transport can't swap in an attacker's key without the attacker also holding the user's identity private keys. `GET /api/bundle/user/:username` (and its `/.well-known/eon-bundle/:username` alias) returns the stored bundle plus `server_sig` — Codex's own ML-DSA signature over the bundle, so a client can verify the response actually came from the domain's Codex instance and wasn't tampered with in transit. ## Escrow tiers Beyond plain key discovery, Codex also implements the server-gated message tiers that Eon Phoenix's composer exposes to end users as Apax (single-release), Confidential, and Dual-control (see the client's [Encrypted mail](/eon-phoenix/client/encrypted-mail/) docs for what these mean to a sender). Mechanically, each stores only a **key half**, never a message body: - **Apax** (`apax_shares`) — a single-release key half; released exactly once via `challenge` → `fetch`, then optionally destroyed immediately (the "self-erase"/burner variant). - **Confidential** (`cmail_recipients`) — an online-only key half released on every authenticated read; `burn` secure-deletes it (the sender's "unsend"), killing every copy everywhere instantly. An `erase_after` flag makes it release-once instead of unlimited. - **Dual-control** (`dctrl_shares`) — release requires both the recipient's fetch *and* a named approver's consent (`/api/dctrl/consent`) before the key half is handed over; `burn` secure-deletes it the same way as Apax and Confidential. - **Ephemeral** (`ephemeral_recipients`) — the single-read escrow behind D-Mail's self-erase modifier. Every fetch is challenge-authenticated with ML-DSA — the reader proves control of their identity key before Codex releases its half (`challenge` → `fetch`). A burned or expired row returns a uniform "gone" response with no information leak about whether it ever existed. Codex never sees the message body or the client-held half of the key in any of these tiers — compromising it yields key halves and public bundles, never message content. ## Signing key and fingerprint pinning Each Codex instance generates its own ML-DSA signing identity on first start (`keyserver_signer.json` in the data directory) and signs every discovery/bundle response with it. Clients need to know this key is genuinely the domain's Codex instance and not an impostor: - **Authoritative instances** anchor the fingerprint in DNS: publish `_eon-keysign. IN TXT "v=eon-keysign1; fpr="` under DNSSEC. Clients cross-check the signing key against this record. - **Directory instances** have no domain of their own to anchor this way, so their fingerprint is **pinned directly in the client** instead (a compiled-in default, or a per-install override — see [Configuration](/eon-phoenix/codex-server/configuration/)). Until a fingerprint is pinned somewhere, clients fail closed and simply refuse to trust that directory. Get the current fingerprint two ways: ```bash # online, once the service is running: curl -s https://keys./api/server-key # offline / admin, no server needed — just reads the signer file: eon-keyserver --data-dir /var/lib/eon-keys --print-fingerprint ``` **Rotation.** Deleting `keyserver_signer.json` from the data directory generates a fresh signing identity on next start — a new fingerprint. Every client that pinned the old one (via DNS anchor or compiled-in pin) needs that pin updated before it will trust the instance again, so treat this as a deliberate, coordinated operation: update the DNS TXT record (or ship a new client build with the new pinned fingerprint) at the same time you rotate. ## Day-to-day service management ```bash systemctl {status|restart|stop} eon-keyserver # or eon-keyserver-auth / eon-keyserver-directory journalctl -u eon-keyserver -f # logs cat /etc/eon-keyserver/keyserver.env # config (0600) ls /var/lib/eon-keys # data: SQLCipher db + signing key ``` Upgrades (`sudo ./upgrade.sh`) replace only the binary and systemd unit and restart the service; they never touch the data directory, env file, secrets, or DNS, and database schema changes apply automatically (additive migrations) on restart — no manual migration step for routine version bumps. See [Install](/eon-phoenix/codex-server/install/) for uninstall and troubleshooting. --- title: Tools url: https://docs.eoncore.eu/eon-insights/tools/ --- ## Overview Eon Insights is a monorepo of Go binaries, one per `cmd/` directory. Each has a Makefile target (`make `) that builds it for `linux/arm64`, `linux/amd64`, and `darwin/arm64` into `bin///`. Run `go run cmd//.go ...` during development, or the built binary in production — except for `capture` and `scascand`, which are multi-file `package main` directories built via `go run ./cmd/ ...` (no single `.go` entry point); see their respective pages. Most daemons follow the same conventions: a `-c ` flag pointing at a YAML config file, a `-debug` flag for verbose zerolog output, and a ClickHouse connection block (`host`/`port`/`database`/`username`/`password`) in that config. Daemons that consume a task queue (`tlsscand`, `nmapscand`, `notifyd`, `pcapworker`, `scascand`) connect to Redis via [asynq](https://github.com/hibiken/asynq) and shut down gracefully on SIGINT/SIGTERM. | Tool | Type | Purpose | |------|------|---------| | [insights](/eon-insights/tools/insights/) | HTTP API | Main REST API the dashboard talks to | | [capture](/eon-insights/tools/capture/) | Daemon/CLI | Passive capture of TLS, QUIC, SSH, IKE/IPsec, OpenVPN | | [hostsync](/eon-insights/tools/hostsync/) | Daemon | Discovers hosts/clients from captured traffic, enqueues scans | | [tlsscand](/eon-insights/tools/tlsscand/) | Worker daemon | Processes queued TLS scan tasks | | [nmapscand](/eon-insights/tools/nmapscand/) | Worker daemon | Processes queued nmap port/service scan tasks | | [tlsscan](/eon-insights/tools/tlsscan/) | CLI | One-shot TLS/SSL scan of a single endpoint | | [tlslab](/eon-insights/tools/tlslab/) | Test server | TLS protocol simulator for testing capture/scan tools | | [datagen](/eon-insights/tools/datagen/) | CLI | Generates synthetic hosts/clients/handshakes for development | | [ja3import](/eon-insights/tools/ja3import/) | CLI | Imports JA3 fingerprint databases (Trisul, SSLBL, browsers) | | [fingerprintd](/eon-insights/tools/fingerprintd/) | HTTP API | Centralized TLS fingerprint database service | | [pcapworker](/eon-insights/tools/pcapworker/) | Worker daemon | Extracts TLS handshakes from uploaded PCAP files | | [notifyd](/eon-insights/tools/notifyd/) | Worker + scheduler | Certificate-expiry and malware-fingerprint alerts (email/Slack/webhook) | | [sca](/eon-insights/tools/sca/) | CLI | One-shot Software Composition Analysis scan of a repository | | [scascand](/eon-insights/tools/scascand/) | Worker daemon | Processes queued SCA repository scans | | [devctl](/eon-insights/tools/devctl/) | Dev tool (TUI) | Starts/stops/monitors all of the above during local development | --- title: API url: https://docs.eoncore.eu/eon-path/api/ --- ## Two protocol surfaces EON Path has two distinct APIs, and it's easy to conflate them: 1. **VPN authentication protocol** — a raw TCP handshake that authenticates a client to the server and establishes a VPN session, using ML-DSA-87 challenge-response authentication and an ML-KEM-1024 key exchange. This is not an HTTP API. 2. **Management API** — a REST/JSON API, served by `eonpath-api`, for registering accounts, publishing and querying ML-DSA public keys, and managing VPN IP leases. It has its own authentication (username/password login returning a bearer token) that is unrelated to the VPN handshake. The two touch at exactly one point: during the VPN handshake, the server calls the management API to confirm a connecting client's public key is registered and active (see step 4 below). ## VPN authentication protocol (ML-DSA-87 challenge-response) - **Transport:** plain TCP to the WireGuard routing method's key-exchange listener — `EONPATH_WG_KEX_ADDR` on the server, default `0.0.0.0:9998` (see [Configuration: Server](/eon-path/configuration/server/)). - **Framing:** a custom, length-prefixed binary protocol (4-byte big-endian length, then the payload) — not HTTP, so there are no JSON request/response bodies for this part of the API. - **Why ML-DSA-87 and ML-KEM-1024:** see [Post-Quantum Cryptography](/eon-path/concepts/post-quantum/). ### Challenge request The first phase authenticates the client's identity key to the server: | Step | Direction | Message | Description | |------|-----------|---------|--------------| | 1 | Client → Server | Protocol version (1 byte) | Identifies the handshake version in use. | | 2 | Client → Server | ML-DSA-87 public key (2,592 bytes) | The client's identity key. | | 3 | Client → Server | API server URL | Tells the server which management API to check the key against. | | 4 | Server → Management API | `GET /api/keys/fingerprint/:fingerprint` | Server looks up the client's key fingerprint to confirm it's registered and active. | | 5 | Server → Client | Auth result (1 byte) | `0x01` if the key is registered and active, `0x00` otherwise. The server closes the connection on `0x00`. | | 6 | Server → Client | Challenge nonce (32 random bytes) | | | 7 | Client → Server | ML-DSA-87 signature (4,627 bytes) | The client signs a domain-separated hash of the protocol version, its own public key, and the nonce, using its ML-DSA-87 private key. | | 8 | Server → Client | Verification result (1 byte) | `0x01` if the signature verifies against the public key from step 2, `0x00` otherwise. The server closes the connection on failure. | ### Session establishment Once the client is authenticated, the server proves its own identity and both sides derive the key material for the WireGuard tunnel: | Step | Direction | Message | Description | |------|-----------|---------|--------------| | 9 | Server → Client | Server ML-DSA-87 identity key, ephemeral ML-KEM-1024 public key, and a signature over both plus the prior transcript | Lets the client verify the key came from the server it expects, not an on-path attacker. | | 10 | Client (local) | — | Client verifies the server's ML-DSA-87 identity against a pinned hash or DNS TXT record configured for the endpoint. See [Server identity pinning](/eon-path/concepts/post-quantum/#server-identity-pinning) — this step fails closed if neither is configured. | | 11 | Client (local) | — | Client verifies the signature from step 9, then encapsulates a shared secret against the server's ML-KEM-1024 public key. | | 12 | Client → Server | ML-KEM-1024 ciphertext | | | 13 | Both sides (local) | — | Both sides derive the WireGuard pre-shared key from the shared secret, bound to a hash of the full handshake transcript (including the ciphertext from step 12). Any tampering with an earlier message produces mismatched keys on the two sides, so a tampered handshake fails closed instead of silently succeeding. | | 14 | Both sides | WireGuard public keys | The two sides exchange WireGuard public keys; the server adds the client as a peer, and the WireGuard tunnel comes up using the derived pre-shared key. | ## Management API ### Base URL ``` http://localhost:8080/api ``` The default address matches `EONPATH_API_ENDPOINT` in the server's environment file — see [Configuration: Server](/eon-path/configuration/server/). ### Authentication Most endpoints require a bearer token, obtained from `/api/auth/login`: ``` Authorization: Bearer ``` This token authenticates account operations (managing your profile, uploading keys, requesting IP leases) — it is **not** used anywhere in the VPN authentication protocol described above, which authenticates the VPN connection itself using ML-DSA-87 signatures instead of passwords or tokens. ### Endpoints | Method | Path | Auth | Purpose | |--------|------|------|---------| | GET | `/api/health` | none | Health check. | | GET | `/api/algorithms` | none | List supported PQC algorithms. | | POST | `/api/auth/register` | none | Create a new user account. | | POST | `/api/auth/login` | none | Authenticate and receive a bearer token. | | GET | `/publickey/:userid` | none | Get a user's primary public key. | | GET | `/publickeys/:userid` | none | Get all of a user's public keys. | | GET | `/api/keys/user/:username` | none | Get public keys by username. | | GET | `/api/keys/fingerprint/:fingerprint` | none | Get a key by its fingerprint (used by the VPN handshake, see above). | | GET | `/api/keys/search` | none | Search public keys by username or comment. | | GET | `/api/me` | bearer | Get the current user's profile. | | PUT | `/api/me` | bearer | Update the current user's profile. | | POST | `/api/me/password` | bearer | Change the current user's password. | | GET | `/api/my/keys` | bearer | List the current user's public keys. | | POST | `/api/my/keys` | bearer | Upload a new public key. | | PUT | `/api/my/keys/:id` | bearer | Update key metadata (for example, its comment). | | POST | `/api/my/keys/:id/revoke` | bearer | Mark a key as revoked. | | DELETE | `/api/my/keys/:id` | bearer | Permanently delete a key. | | POST | `/publickey` | bearer | Upload a public key (legacy endpoint). | | GET | `/api/my/ip` | bearer | Get the current user's VPN IP lease. | | POST | `/api/my/ip` | bearer | Request a new IP lease. | | DELETE | `/api/my/ip` | bearer | Release the current IP lease. | | POST | `/endpoints` | signed payload | Endpoint registration: a server pushes the endpoints it facilitates, signed with its ML-DSA-87 identity key and, on the first push, its licence key. | | GET | `/endpoints` | none | List the endpoints clients may connect to. Standby endpoints are not in the list. | | GET | `/metrics` | none | Prometheus metrics. | | POST/GET | `/legacy/publickey`, `/legacy/publickeys/:userid`, `/legacy/publickey/:userid[/:keyname]` | mixed | File-based public-key endpoints kept for old clients; new clients use `/api/keys/*`. | ### Endpoint registration `POST /endpoints` replaced an older `POST /endpoints/:name`, which was trust-on-first-use — whatever `X-API-Key` the first caller sent became the key for that name. Registration is now a signed payload: the server's ML-DSA-87 identity signs it, the first push also presents the licence key, and after that the signature alone authenticates the server. The URL the server registers with is part of what it signs, so it must match one of the API's `EONPATH_API_AUDIENCES` entries. Replays are bounded by a nonce cache. An endpoint carries a state — `standby` or `active` — set on the server, not in the API (see [Configuration: Server](/eon-path/configuration/server/#mode-standby-and-mode-active)). `GET /endpoints` returns only the active ones. Unlicensed endpoints in a push are skipped; the rest of the push is accepted rather than the whole thing being refused. ### Administrative and billing routes Two route groups exist for EON Core, not for tenants, and each is registered **only** when its token is configured — without the token the routes don't exist rather than refusing. Both take the token in the `X-Admin-Token` header. | Method | Path | Token | Purpose | |--------|------|-------|---------| | POST/GET | `/api/admin/organizations` | admin | Create and list organizations. | | POST/DELETE/GET | `/api/admin/organizations/:id/prefixes[/:prefix]` | admin | Reserve, release and list the endpoint-name prefixes an organization may register under. | | POST/GET | `/api/admin/licenses` | admin | Issue and list licences. | | PATCH | `/api/admin/licenses/:id` | admin | Amend a licence in place, instead of reissuing the key. | | POST | `/api/admin/licenses/:id/revoke` | admin | Revoke a licence. | | GET | `/api/admin/endpoints` | admin | List every registered endpoint. | | DELETE | `/api/admin/endpoints/:id` | admin | Remove a registered endpoint. | | GET | `/api/admin/billing/summary` | admin or billing | Billing digest over the endpoint transition log. | | GET | `/api/admin/billing/endpoints/:name/history` | admin or billing | The transition history for one endpoint. | The billing group accepts a second, read-only token (`EONPATH_BILLING_TOKEN`) so an invoicing job never holds a token that can issue or revoke a licence. Neither token replaces restricting `/api/admin` at the reverse proxy — a shared secret on a published path is one leak away from a self-issued licence. ### Register ``` POST /api/auth/register ``` Request: ```json { "username": "alice", "email": "alice@example.com", "password": "a-strong-password" } ``` Response (`201 Created`): ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "username": "alice", "email": "alice@example.com", "created_at": "2026-01-15T10:30:00Z" } ``` ### Login ``` POST /api/auth/login ``` Request: ```json { "username": "alice", "password": "a-strong-password" } ``` Response (`200 OK`): ```json { "token": "eyJhbGciOiJIUzI1NiIs...", "expires_at": "2026-01-16T10:30:00Z", "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "username": "alice" } } ``` ### Errors Error responses share a common shape: ```json { "error": "error message here" } ``` | Code | Meaning | |------|---------| | 400 | Bad request — invalid input | | 401 | Unauthorized — missing or invalid token | | 403 | Forbidden — insufficient permissions | | 404 | Not found | | 409 | Conflict — resource already exists | | 429 | Too many requests — rate limit exceeded | | 500 | Internal server error | Authentication endpoints are rate-limited to 5 requests/minute; other endpoints to 30 requests/minute. --- title: Dashboard url: https://docs.eoncore.eu/eon-insights/dashboard/ --- ## Overview The dashboard is a Svelte 5 + TypeScript single-page application (in `web/dashboard`) that gives a human-readable view over everything captured and scanned by Eon Insights. It talks exclusively to the [insights](/eon-insights/tools/insights/) REST API — it holds no database connection of its own — and authenticates against FusionAuth, with optional two-factor authentication. ## Running it In development, Vite proxies `/auth`, `/api`, and `/oauth` to the `insights` server on `http://localhost:3000`, so `insights` must already be running: ```bash cd web/dashboard npm install npm run dev ``` This serves the dashboard (typically on `http://localhost:5173`, though Vite will pick another port if that one is busy) with hot reload. Other scripts: ```bash npm run build # production build to dist/ npm run preview # preview a production build locally npm run check # svelte-check (type checking) ``` In production, the built `dist/` output is served as static files (see the `dashboard-container` Docker build target), fronted by nginx or similar, with the same `/api`, `/auth`, `/oauth` paths reverse-proxied to `insights`. ## What it shows The dashboard's pages map fairly directly onto the `insights` API's route groups: - **Home** — landing overview/dashboard summary - **Hosts** / **Host Details** — discovered hosts, their security level, TLS scan history, nmap scan results, and tags; can trigger a fresh TLS or nmap scan on demand - **Clients** / **Client Details** — discovered TLS clients (source IPs), their fingerprints, and tags - **Handshakes** / **Connection Details** — captured TLS handshake records, searchable and filterable, with per-connection detail (cipher, version, JA3/JA3S, certificate) - **Certificates** — the certificate inventory (deduplicated by SHA-256 fingerprint), including expiry status - **Compliance** / **Compliance Rules** — compliance framework scores and findings, and a rule editor (see [Compliance](/eon-insights/compliance/)) - **Cbom** — Cryptographic Bill of Materials generation, browsing, and download - **Sca** / **Sca Repo** — connected Git repositories, OAuth connections, scan history, and vulnerability findings ([SCA](/eon-insights/tools/sca/)) - **Upload** — PCAP file upload for offline analysis (processed by [pcapworker](/eon-insights/tools/pcapworker/)) - **Status** — service and asynq queue/worker health, driven by the `/api/status` endpoints and a WebSocket status feed - **Chats** — an AI assistant that can run read-only SQL against the ClickHouse schema to answer ad-hoc questions about the data - **Custom View** — user-defined saved views (`/api/views`) - **Settings** / **Profile** / **Two-Factor Settings** — account and application configuration, including two-factor authentication setup - **Login** / **Forgot Password** — authentication flows against FusionAuth - **About** — static copyright/info page ## Custom periods and sharing Time ranges used across charts and tables can be saved as reusable "custom periods" (`/api/periods`). Data views can be exported as PDF or sent elsewhere via the sharing endpoints (`/api/sharing/pdf`, `/api/sharing/send`). --- title: Eon Kartis url: https://docs.eoncore.eu/eon-kartis/ --- ## What is Eon Kartis? Eon Kartis discovers and scans the cryptographic surface an organization exposes to the internet. Given a domain, it finds related subdomains and same-organization domains (WHOIS, Certificate Transparency logs, DNS reconnaissance, ASN expansion, and technology fingerprinting), resolves them to live hosts, and scans each one for the protocols it speaks: TLS, SSH, STARTTLS-wrapped SMTP/IMAP/POP3, OpenVPN, IKE/IPsec, and plaintext services running on ports where encryption would normally be expected. Findings are scored against a post-quantum-cryptography readiness baseline — hybrid key-exchange groups, certificate signature algorithms, and known-weak protocol versions — and can be rendered into a Markdown report with an executive summary, prioritized findings, and PQC guidance. Kartis has two consumers of the same data. Machines — **Eon Center**, **Eon Aethis**, **Eon Insights** — read it over the bearer-token `/v1/...` API and render it in their own dashboards. Humans read it in the **Kartis portal**, a server-rendered surface at `/portal/...` where a customer logs in and sees their own estate: hosts, certificates, crypto findings, run history and reports. Both resolve to the same tenant boundary; see [Portal](/eon-kartis/portal/). ## Key properties - **Two discovery/scan daemons, a subdomain-enumeration worker, and a scheduler.** `kartis-discovery` finds related domains and hands subdomain enumeration to `kartis-recon`; `kartis-scan` scans each resolved host and serves both APIs; `kartis-scheduler` materialises due runs on a monthly or biweekly cadence. They communicate over Redis pub/sub and share a Postgres database as the source of truth. - **Depth-controlled scanning.** Every scan picks a max penetration depth — `discover`, `passive`, `active`, or `vuln` — and a category filter (`discovery`, `crypto`, `web`, `infoleak`). There is deliberately no "offensive" tier reachable through the API or the scan chain; attack-shaped probing only exists as a separate, manually-authorized standalone tool. - **Honest scanning.** The scanner identifies itself on every protocol it speaks — HTTP User-Agent, SSH identification string, SMTP EHLO — with an optional contact URL, and performs no evasion. - **PQC posture detection.** Analyzers classify TLS key-exchange groups as classical, hybrid, or post-quantum-only, and flag certificates signed with classical-only algorithms, so a scan answers "is this endpoint ready for harvest-now-decrypt-later" directly. - **Crypto-posture classification.** One engine, three verdicts: **shadow** (plausibly yours, not in the central register), **rogue** (looks like yours but isn't, or a certificate exists for your name that you never ordered), and **drift** (a registered asset whose crypto deviates from policy). The baseline is either *declared* (a register uploaded as xlsx/csv) or *learned* from the estate itself, and every finding records which — so a report can tell "deviates from your policy" apart from "deviates from your own norm". - **Customer-facing reporting.** `kartis-scan report` turns a consolidated scan document into a Markdown report (Dutch or English) with an anonymization pass for sharing sample findings safely. - **Bearer-token HTTP API.** Sessions, runs, findings, services, and discovery trees are all reachable over `/v1/...` endpoints, which is how Eon Center, Eon Aethis, and Eon Insights consume Kartis data. - **Export and replay.** A full-scan export can be replayed back into the schema, so a scan captured elsewhere lands as ordinary rows. ## Explore the docs - [**Scanning**](/eon-kartis/scanning/) — the discovery and scan pipeline, the depth/category model, and the CLI and API for running scans - [**Portal**](/eon-kartis/portal/) — the customer-facing web surface: pages, login, tenancy, domain verification, and the scan schedule - [**Reports**](/eon-kartis/reports/) — generating and anonymizing Markdown reports from scan results - [**Configuration**](/eon-kartis/configuration/) — config files, profiles, and environment variables for each component --- title: Interface url: https://docs.eoncore.eu/eon-phoenix/client/interface/ --- All appearance settings live under **Settings → Appearance** and apply immediately. ## Themes Five built-in themes plus **Auto**, which follows your system, or **Custom** to set your own palette with live color pickers. An **accent color** picker sits on top of whichever theme you choose, letting you tweak the highlight color independently of the base theme; button color is separate again. Your choices are written to a file next to your other settings, not only to the window's browser storage, so they survive a reinstall and are identical in every window. ## Fonts Choose the app's typeface from a preset list (including Eon Phoenix's own typeface, Inter, Space Grotesk, and a few others), or supply your own custom font. Each of the three panels — sidebar, message list, reading pane — can carry its own font and its own text size, so you can read messages in a serif face while menus and buttons stay in the default sans-serif. In the main client you type the name of a font your system knows. The native Gio client can't look fonts up by name: there you pick a **file** (`.ttf` or `.otf`). `woff2` is refused — that client can't read the format. ## Layout - **Density** — compact, cozy, or comfortable spacing throughout the app. - **Shape** — sharp, rounded, or fully round corners on buttons and panels. - **Reading pane** — show the selected message beside the list (on) or open it full-width (off). - **Compact sidebar** — collapse the folder sidebar to icons only, hiding labels, to save horizontal space. - **Avatars** and **message preview** — toggle sender initials and preview snippets in the message list on or off. ## Folders and tags Drag folders in the sidebar to reorder them. Messages can carry one or more **tags**, which you can filter by; combine several tags at once to narrow the list further. Deterministic rules (Settings → Filters) can apply tags or route mail automatically based on conditions you define, similar to filters in other mail clients. ## Typing cursor style A small personalization option (Settings → Appearance) lets you pick the shape of the text cursor used while composing a message — a thin bar, a solid block, or an outlined block. --- title: tlsscand url: https://docs.eoncore.eu/eon-insights/tools/tlsscand/ --- ## Purpose `tlsscand` is an [asynq](https://github.com/hibiken/asynq) worker daemon that processes queued TLS scan tasks — actively connecting to a target host and probing its supported TLS versions, cipher suites, and certificate chain (the same scan logic as the standalone [tlsscan](/eon-insights/tools/tlsscan/) CLI, but running as a pool of workers pulling from Redis). Tasks are enqueued by [hostsync](/eon-insights/tools/hostsync/) for newly discovered hosts, or by the dashboard/API (`POST /api/hosts/{uuid}/tlsscan`). ## Usage ``` tlsscand -c [-debug] ``` | Flag | Default | Description | |------|---------|-------------| | `-c` | — | Path to config file (required) | | `-debug` | `false` | Enable debug logging | ## Configuration `configs/tlsscand.yaml`: ```yaml name: tlsscand clickhouse: host: localhost port: 19000 database: insights username: insights password: secret redis: host: localhost port: 6379 debug: false concurrency: 5 # max concurrent scans, default 5 if unset/0 scan_interval: 24h # minimum time between re-scans of the same host, e.g. 1h, 24h, 7d ``` If `scan_interval` is left empty, every queued task triggers a scan regardless of when the host was last scanned. On startup, `tlsscand` validates that the `tls_scans` and `certificates` tables exist, and loads the capture config (for internal-network classification) and scanner config from ClickHouse. ## Example ```bash # Run as a daemon with a config file tlsscand -c configs/tlsscand.yaml # Run with debug logging tlsscand -c configs/tlsscand.yaml -debug ``` --- title: Compliance url: https://docs.eoncore.eu/eon-insights/compliance/ --- ## Overview Eon Insights evaluates captured traffic and certificate data against a set of compliance frameworks, each with its own scored rule set. Rules and frameworks are defined in YAML (`configs/compliance/frameworks.yaml` and `configs/compliance/rules.yaml`), seeded into ClickHouse on first startup, editable afterwards through the dashboard or the `/api/compliance/rules` API, and reloadable from YAML at runtime without a restart. ## Frameworks Six frameworks ship by default: | Framework | Display name | |-----------|--------------| | `PCI-DSS` | PCI DSS v4.0 | | `NIST SP 800-52` | NIST SP 800-52 Rev 2 | | `GDPR` | GDPR Article 32 | | `HIPAA` | HIPAA Security Rule | | `NIS2` | NIS2 Directive | | `DORA` | Digital Operational Resilience Act (EU financial sector) | Each framework starts from a base score of 100. Rules are additive deductions: every triggered rule subtracts its configured `weight` from that framework's score (clamped at 0). The resulting score maps to a status: | Score | Status | |-------|--------| | ≥ 95 | compliant | | 80–94 | partial | | < 80 | non-compliant | ## Rules The default rule set ships 26 rules across the six frameworks (`PCI-DSS`: 3, `NIST SP 800-52`: 5, `GDPR`: 3, `HIPAA`: 3, `NIS2`: 4, `DORA`: 8), built from 11 distinct finding types: | Finding type | Checks for | |--------------|------------| | `deprecated_tls` | TLS versions below 1.2 (or below 1.1, depending on the framework) in use | | `weak_cipher` | Cipher suites classified `insecure` or `weak` in the `ciphersuites` reference table | | `non_aead` | Non-AEAD cipher modes (CBC) instead of GCM/CCM/ChaCha20-Poly1305 | | `quantum_vulnerable` | Key exchange using RSA, ECDHE, or DHE (not post-quantum/hybrid) | | `small_key` | Certificates with RSA keys under 2048 bits | | `self_signed` | Self-signed certificates | | `expiring_cert` | Certificates expiring within 30 days | | `expired_cert` | Certificates already expired | | `sha1_cert` | Certificates signed with SHA-1 | | `weak_ssh` | SSH connections with weak key exchange, cipher, or MAC algorithms | | `weak_ipsec` | IPsec connections with weak encryption, PRF, integrity, or DH group | Each rule record (`internal/rules.Rule`) carries: - `rule_expression` — a boolean expression evaluated against the evaluation context (see below), e.g. `"Ctx.DeprecatedTLSCount > 0"` - `finding_type`, `severity` (`critical`/`high`/`medium`/`low`), `requirement` (a citation into the framework, e.g. "Article 9.2" or "164.312(e)(1)"), `recommendation` — the text shown in a finding - `weight` and `is_critical` — how much the rule deducts from the score, and whether it counts toward `criticalFindings` - `count_query` — the ClickHouse query used to compute how many hosts/ certificates/connections are affected, for display ## How evaluation works `GET /api/compliance/stats` (backed by `internal/insights/compliance.go`) runs on a rolling 30-day window by default: 1. Fetch aggregate metrics from ClickHouse into an `EvaluationContext` (counts of deprecated-TLS hosts, weak-cipher hosts, weak SSH/IPsec connections, small-key/self-signed/expiring/expired/SHA-1 certificates, quantum-vulnerable connections, and non-AEAD usage). 2. For each enabled framework, evaluate every enabled rule's expression against that context using the [Grule](https://github.com/hyperjumptech/grule-rule-engine) rule engine. A rule that fails to parse falls back to a hardcoded finding-type check so a malformed custom expression can't silently disable a whole framework. 3. For each triggered rule, run its `count_query` to get the affected host/certificate/connection count, deduct its `weight` from the framework's score, and record it as a finding (deduplicated across frameworks that share the same finding type). 4. Compute PQC (post-quantum cryptography) readiness stats separately: total vs. quantum-vulnerable connections, unique "critical" hosts (quantum-vulnerable key exchange), high-priority hosts (>100 quantum-vulnerable connections), and hosts already using PQC/hybrid key exchange (`Kyber`, `ML-KEM`, `X25519Kyber` in the negotiated cipher/key-exchange name). `GET /api/compliance/findings/{type}` returns up to 100 individual connection or certificate records behind a given finding type, for drill-down from the dashboard's summary view. ## Managing rules The dashboard's Compliance Rules page (and the underlying `/api/compliance/frameworks` and `/api/compliance/rules` endpoints) let you view, create, edit, and delete individual rules and see which framework(s) they belong to. Changes take effect immediately — creating, updating, or deleting a rule reloads the in-memory rule engine from ClickHouse. To reset the rule set back to what's defined in the YAML files (adding new rules and updating existing ones by name, without touching custom-created rules): ``` POST /api/compliance/reload?from_yaml=true ``` Without `from_yaml=true`, the same endpoint just reloads the engine's in-memory frameworks from whatever is currently in ClickHouse (useful after a change made directly in the database). --- title: Eon Phoenix url: https://docs.eoncore.eu/eon-phoenix/ --- ## What is Eon Phoenix? Eon Phoenix is a native mail client that speaks ordinary IMAP/SMTP but adds a hybrid post-quantum encryption layer, so that the mail server never sees the content of a message. It ships in two builds from one core: a desktop client (Wails: Go backend, React frontend) for macOS, Windows and Linux, and a Gio client (pure Go, no webview) that runs on Android and on the desktop. Both converge two streams of communication into one inbox: - **Email** — regular correspondence over standard IMAP/SMTP, reaching anyone with an email address. - **D-Mail (Discrete Mail)** — an always-encrypted, chat-style direct message that rides on the same email address, shown in the inbox as a threaded conversation rather than a message list. ## The cryptographic foundation Every protected message is sealed hybrid: a post-quantum KEM plus X25519 for confidentiality, ML-DSA-87 plus Ed25519 for authenticity, AES-256-GCM over the body, sign-then-encrypt. Breaking the scheme requires breaking *both* the post-quantum and the classical primitive. | Layer | Algorithms | |---|---| | Identity / signing | ML-DSA-87 (FIPS 204) + Ed25519 — fixed; defines the fingerprint | | Key encapsulation | a *set* of hybrid keys, one per algorithm; ML-KEM-1024 (FIPS 203) and FrodoKEM-1344-SHAKE are derived for every identity, each paired with X25519 | | Message body | AES-256-GCM | Which post-quantum algorithm seals a given message is not fixed. An identity publishes its whole key set, and a sender picks the strongest algorithm the recipient also publishes as active — preference order is compiled into the client, with family diversity ranked above the security level inside one family. One message can address recipients with different key sets, because the algorithm lives in the per-recipient block of the envelope rather than in a suite name. Adding or retiring an algorithm leaves the fingerprint unchanged, does not trigger the key-change flow and does not invalidate a pin. A retired key keeps decrypting; it is only no longer offered to senders. Messages sealed before crypto agility (envelope v1, ML-KEM-1024) stay readable. ## Key discovery To encrypt to a contact, Eon Phoenix needs to find their public key. It does this through **Codex**, a small per-domain directory service: a sender's client resolves the recipient's domain to a keyserver, fetches the recipient's key bundle, and verifies it before trusting it — all automatically, with no manual key exchange for domains that run their own Codex instance. Contacts on providers that don't run their own keyserver (Gmail, Outlook, and similar) are reached through an open-enrollment Codex instance instead, using an email-challenge to prove mailbox ownership. ## Two audiences, two sets of docs This documentation is split by who is reading it: - **[Client](/eon-phoenix/client/)** — for anyone using the Eon Phoenix app to read and send mail: installing the app, adding accounts, sending and receiving encrypted mail, using D-Mail, and verifying a contact's key. - **[Codex Server](/eon-phoenix/codex-server/)** — for the sysadmin installing and operating a Codex directory keyserver for a mail domain: deployment, configuration, and day-to-day operations. If you only use the app, start with **Client**. If you're standing up a keyserver for your organization's domain, start with **Codex Server**. ## Status Eon Phoenix ships open-core: the mail/D-Mail engine and cryptographic implementation (**Phoenix-core**) are open source (AGPL-3.0), so the implementation can be independently reviewed. The current release covers the three encryption schemes (PGP/MIME, S/MIME, Eon-PQC), D-Mail, and Codex discovery/publish/trust-on-first-use. Beta builds are published per version for five platforms: macOS arm64 and amd64, Linux amd64, Windows amd64, and Android arm64 (APK). Windows builds are Authenticode signed; the macOS build is not signed yet, so Gatekeeper still warns on first open, and the Android APK carries a test key. Real-world interop with other mail clients (Thunderbird, Apple Mail) over PGP/S-MIME has not yet been broadly tested. --- title: nmapscand url: https://docs.eoncore.eu/eon-insights/tools/nmapscand/ --- ## Purpose `nmapscand` is an asynq worker daemon that processes queued nmap scan tasks — running an nmap port/service/OS scan against a target host and storing the results (open ports, detected services and versions, OS fingerprint) in ClickHouse. Tasks are enqueued by [hostsync](/eon-insights/tools/hostsync/) for newly discovered hosts, or by the dashboard/API (`POST /api/hosts/{uuid}/nmapscan`). ## Usage ``` nmapscand -c [-debug] ``` | Flag | Default | Description | |------|---------|-------------| | `-c` | — | Path to config file (required) | | `-debug` | `false` | Enable debug logging | ## Configuration `configs/nmapscand.yaml`: ```yaml name: nmapscand clickhouse: host: localhost port: 19000 database: insights username: insights password: secret redis: host: localhost port: 6379 debug: false concurrency: 3 # max concurrent scans, default 3 if unset/0 (nmap is resource-intensive) scan_interval: 24h # minimum time between re-scans of the same host ``` On startup, `nmapscand` validates that the `nmap_scans` table exists, and loads the capture and scanner configuration from ClickHouse (the same configuration `tlsscand` uses for internal-network classification). ## Example ```bash # Run as a daemon nmapscand -c configs/nmapscand.yaml # Run with debug logging nmapscand -c configs/nmapscand.yaml -debug ``` --- title: Security Model url: https://docs.eoncore.eu/eon-path/security/ --- ## Threat model EON Path's routing method is designed to resist the following threats: | Threat | Mitigation | |--------|------------| | On-path / man-in-the-middle attacks against the VPN handshake | Mandatory server identity pinning (pinned hash or DNS TXT record) verified before the client trusts the server's key material | | Endpoint impersonation | Same identity pinning: a client will not proceed with a tunnel to a server whose identity it cannot verify | | Handshake tampering or replay | The WireGuard pre-shared key is bound to a hash of the full handshake transcript, so tampering with any earlier message produces mismatched keys and the handshake fails closed | | Quantum computer attacks on key exchange and signatures, including "harvest now, decrypt later" | ML-KEM-1024 key encapsulation and ML-DSA-87 signatures, both at NIST Level 5, the highest standardized security level for these algorithms | | Connections from unregistered or revoked clients | The server checks a connecting client's public-key fingerprint against the management API before completing the handshake | | Brute-force and credential-stuffing attacks against the management API | Per-IP rate limiting (lower limits on authentication endpoints than on general endpoints) | | Password and API-key theft from the management API | Passwords and API keys are hashed before storage, never stored or compared in plaintext | | Data-at-rest exposure of the management API's database | Database encryption | ### Out of scope Some threats are explicitly out of scope for EON Path's own security controls, and are the operator's or user's responsibility instead: - Physical access to a client or server endpoint - A compromised operating system on either end of the connection - Side-channel attacks against the underlying cryptographic implementations - Denial-of-service attacks beyond basic per-IP rate limiting ## Authentication: ML-DSA-87 challenge-response Clients authenticate to the server with an ML-DSA-87 public-key challenge-response protocol: the client presents its public key, the server checks it against the management API, issues a random challenge, and the client proves possession of the matching private key by signing that challenge. There is no password or bearer token involved in the VPN handshake itself, so there is no such secret in transit and nothing of that kind to steal or replay. See [Post-Quantum Cryptography](/eon-path/concepts/post-quantum/) for the full protocol walkthrough. ## Mandatory server identity pinning Authenticating the client is only half of the handshake — the client also needs to know it is talking to the genuine server before it trusts that server's key material. EON Path makes this check mandatory rather than optional: before deriving any tunnel key material, the client verifies the server's ML-DSA-87 identity against either a pinned fingerprint hash configured for that endpoint, or a DNS TXT record published for the server's domain. If neither is configured, or the identity presented does not match, the client refuses to proceed — it fails closed rather than falling back to an unauthenticated connection. This closes off a class of attacks where an adversary who controls the network path, or who can otherwise get in front of a client's connection attempt, presents their own key instead of the real server's. ## Transcript binding The handshake does not stop at authenticating the client and verifying the server's identity. The final WireGuard pre-shared key is derived from the post-quantum key exchange's shared secret combined with a hash of the entire handshake transcript up to that point — every message exchanged during authentication and key exchange feeds into the value both sides use to compute their key. If an attacker were to tamper with, reorder, or substitute any earlier message in the exchange, the two sides would derive different pre-shared keys and the resulting WireGuard tunnel simply would not come up, rather than silently succeeding with attacker-influenced key material. ## Transport security: WireGuard Once the handshake completes, traffic flows over a standard WireGuard tunnel. WireGuard's own handshake and transport are built on the Noise protocol framework, and packets are encrypted with ChaCha20-Poly1305. EON Path does not modify WireGuard's wire format or packet handling; it strengthens the tunnel's key material by feeding the post-quantum-derived pre-shared key into WireGuard's own key derivation alongside its native X25519 exchange, so an attacker would need to defeat both the classical and the post-quantum contribution to recover tunnel traffic. See [Tunneling](/eon-path/concepts/tunneling/) for how the two are combined. ## Key management principles - **No long-lived shared secrets for tunnel traffic.** Every session derives a fresh WireGuard pre-shared key from a fresh ML-KEM-1024 exchange; keys are not reused across sessions. - **Server identity keys are generated automatically.** A server's ML-DSA-87 identity key pair is created the first time the server daemon starts, removing a manual key-provisioning step that could otherwise be skipped or done insecurely. - **Client keys stay with the client.** A client's ML-DSA-87 private key is generated and kept on the client; only the corresponding public key is registered with the management API. - **Revocation is supported, not just registration.** A registered public key can be marked as revoked through the management API, after which the server will no longer complete a handshake for that key. - **Credentials for the management API are never stored in plaintext.** Account passwords and API keys are hashed before storage; comparisons use constant-time checks to avoid leaking timing information. ## A separate surface: the management API The threats and mitigations above cover the VPN authentication protocol. The management API is a distinct system with its own authentication (username/password login, JWT-based bearer tokens with expiration) used for account operations — registering keys, requesting IP leases, and similar account management. It is not part of the VPN handshake's trust path beyond the single fingerprint lookup described above. See [API](/eon-path/api/) for how the two surfaces relate. ## Reporting a vulnerability If you believe you've found a security vulnerability in EON Path, please report it to **security@eoncore.eu**. Include: - A description of the vulnerability and its potential impact - Steps to reproduce it - A suggested fix, if you have one Please report vulnerabilities privately and give us a reasonable amount of time to address the issue before any public disclosure. --- title: FAQ url: https://docs.eoncore.eu/eon-path/faq/ --- ## Why use post-quantum cryptography now, if large quantum computers don't exist yet? Because an adversary doesn't need a quantum computer today to benefit from one in the future. Traffic encrypted with classical algorithms can be recorded now and decrypted later, once a sufficiently powerful quantum computer exists — a strategy known as "harvest now, decrypt later." For long-lived infrastructure and long-lived secrets, that risk is worth protecting against today rather than waiting. See [Post-Quantum Cryptography](/eon-path/concepts/post-quantum/) for the algorithms EON Path uses and why. ## Can I connect to an EON Path server with a plain WireGuard client? No. EON Path's routing method uses a standard WireGuard tunnel for the data plane, but getting to that tunnel requires the ML-DSA-87 challenge-response handshake and mandatory server identity verification described in [Security Model](/eon-path/security/) — a plain WireGuard client has no way to perform that handshake or obtain the resulting pre-shared key. Once the tunnel is up, though, it behaves like a normal WireGuard interface and can be inspected with standard WireGuard tooling (`wg show`, for example). ## What's the performance overhead of the post-quantum handshake? Very little, and it's a one-time cost. ML-KEM and ML-DSA are designed to be fast: individual operations (key generation, signing, verification, encapsulation, decapsulation) are measured in microseconds on modern hardware, even at the largest standardized parameter sizes EON Path uses (ML-KEM-1024, ML-DSA-87). That cost is paid once, when a client connects to a server; after the tunnel comes up, traffic is carried by ordinary WireGuard with ChaCha20-Poly1305 encryption, so ongoing throughput and latency are the same as any other WireGuard tunnel. See [Post-Quantum Cryptography](/eon-path/concepts/post-quantum/) for the algorithms involved. ## Can EON Path be used with an IPv6-only backend network? Yes. EON Path's SIIT (stateless IP/ICMP translation) feature lets an IPv4-only client application reach an IPv6-only server-side network without any changes on the client: the client's TUN device still sees and sends plain IPv4 packets, and SIIT translates them to and from IPv6 immediately before and after they cross the WireGuard tunnel. See [Address Translation (SIIT)](/eon-path/concepts/siit/) for how the translation works and when it applies. ## What platforms are supported? The server runs on Debian 11+ and Ubuntu 22.04+ (amd64), packaged as `.deb` files. The desktop client supports macOS (Apple Silicon and Intel), Windows, and Linux. Native iOS and Android apps exist and share the same authentication and tunnel code as the desktop client; public app-store availability is pending. See [Install](/eon-path/install/) for details on each platform. ## What license is EON Path released under? EON Path is proprietary, commercially licensed software — it is not open source. Use is governed by a commercial license agreement; unauthorized copying, modification, or distribution of the software is not permitted. ## How is EON Path different from setting up plain WireGuard myself? Plain WireGuard leaves key distribution and peer verification up to you: you generate and exchange public keys out of band, and there's no built-in way to check that the peer you're configuring is who you think it is. EON Path adds a post-quantum-authenticated handshake in front of WireGuard that automates and hardens that step — clients and servers authenticate each other with ML-DSA-87 signatures, server identity is verified against a pin or DNS record before any key material is trusted, and the WireGuard pre-shared key is derived fresh for each session from an ML-KEM-1024 key exchange rather than being configured once and reused indefinitely. A management API also handles key registration, revocation, and IP-lease assignment, which plain WireGuard has no equivalent for. ## What happens if a server's identity can't be verified? The client refuses to connect. Server identity pinning is mandatory for EON Path's routing method: if an endpoint has no pinned identity hash or DNS TXT record configured, or if the identity presented during the handshake doesn't match what's expected, the client fails closed rather than falling back to an unverified connection. See [Server identity pinning](/eon-path/concepts/post-quantum/#server-identity-pinning) for details. ## Does EON Path support connecting to more than one server? Yes. Desktop and mobile clients can hold configuration for multiple server endpoints and switch between them, rather than being tied to a single hard-coded server. See [Tunneling](/eon-path/concepts/tunneling/) for how this works at the WireGuard configuration level. ## Are the VPN handshake and the management API the same authentication system? No, and it's a common point of confusion. The VPN handshake authenticates a client to a server using ML-DSA-87 signatures, with no password or token involved. The management API is a separate REST service with its own username/password login and JWT bearer tokens, used only for account operations like registering keys and requesting IP leases. The two meet at exactly one point: the server looks up a connecting client's public-key fingerprint through the management API during the handshake. See [API](/eon-path/api/) for the full picture. ## How do I report a security vulnerability? See [Security Model](/eon-path/security/#reporting-a-vulnerability) for the disclosure process and contact address. --- title: tlsscan url: https://docs.eoncore.eu/eon-insights/tools/tlsscan/ --- ## Purpose `tlsscan` is a one-shot CLI that scans a single TLS/SSL endpoint for supported protocol versions, cipher suites, and known vulnerabilities, and prints the result to the terminal (or as JSON). It's the same scan engine `tlsscand` runs as a queued worker, exposed directly for ad-hoc use — no ClickHouse or Redis connection required. ## Usage ``` tlsscan [options] [URL or HOST:PORT] ``` | Flag | Description | |------|-------------| | `-json` | Output results in JSON format instead of formatted text | | `-debug` | Enable debug logging | The target can be given as a bare `host:port`, a hostname alone (defaults to port 443), or a full `http://`/`https://` URL (port inferred from the scheme if not explicit). ## What it checks The formatted output covers: - Supported protocols and cipher suites per protocol, flagging the server's preferred cipher and its key size/strength - **Compression** — whether TLS compression is enabled (CRIME attack exposure) - **Renegotiation** — supported and, if so, whether it's secure - **Fallback SCSV** — protection against protocol downgrade attacks - **Heartbleed** — per-protocol-version vulnerability check - Supported elliptic curve/DH groups - Certificate chain details (subject, issuer, validity, SHA-256 fingerprint) ## Examples ```bash tlsscan https://www.example.com tlsscan https://www.example.com:8443 tlsscan www.example.com:443 tlsscan -json 8.8.8.8:443 ``` --- title: tlslab url: https://docs.eoncore.eu/eon-insights/tools/tlslab/ --- ## Purpose `tlslab` is a TLS protocol simulator used to test `capture`, `tlsscan`, and `nmapscand` against controlled, reproducible TLS endpoints. It can serve deprecated protocols (SSLv2, SSLv3, TLS 1.0/1.1) alongside modern TLS 1.3, and supports 366+ cipher suites, so you can point the other tools at endpoints with a known-bad configuration and verify they detect it correctly — without touching a real host. ## Usage ``` tlslab -c [-debug] ``` | Flag | Description | |------|-------------| | `-c` | Path to configuration file (required) | | `-debug` | Enable debug logging | ## Configuration ```yaml name: tlslab debug: false cipher_groups: modern: - TLS_AES_128_GCM_SHA256 - TLS_AES_256_GCM_SHA384 endpoints: - name: modern-tls13 listen: ":4430" protocol: tls13 ciphers: [modern] sni: ["modern.tls.test"] ``` Each entry under `endpoints` opens a listener on its own port with its own protocol version and cipher group — this is how `devctl`'s bundled `tlslab` config exposes ten endpoints (ports 4450–4459) covering the "recommended / secure / weak / insecure" security-profile hostnames that `datagen -realistic` generates. See `configs/tlslab.yaml` for a complete worked example. ## Examples ```bash # Run the simulator tlslab -c configs/tlslab.yaml # Run with debug logging tlslab -c configs/tlslab.yaml -debug # Probe an endpoint with openssl openssl s_client -connect localhost:4430 -servername modern.tls.test # Probe with curl (and let `capture` observe the handshake) curl -k https://localhost:4430/ ``` --- title: datagen url: https://docs.eoncore.eu/eon-insights/tools/datagen/ --- ## Purpose `datagen` populates ClickHouse with realistic synthetic data — hosts, clients, certificates, TLS handshakes, and TLS scans — so a development or demo environment has something to look at without needing live network traffic. Generated handshakes are modeled on real browser/tool JA3 fingerprints (Chrome, Firefox, Safari, Edge, curl, Python Requests, Go's HTTP client) plus a small percentage of known-malicious fingerprints (TrickBot, Emotet, Dridex, Cobalt Strike, Metasploit) so malware-detection and compliance views have something to trigger on. ## Usage ``` datagen [options] ``` | Flag | Default | Description | |------|---------|-------------| | `-h` | `localhost` | ClickHouse hostname | | `-p` | `9000` | ClickHouse port | | `-U` | `default` | ClickHouse user | | `-W` | — | ClickHouse password (prompted interactively if omitted) | | `-db` | `insights` | ClickHouse database name | | `-q` | `10000` | Number of TLS handshakes to generate | | `-hosts` | `50` | Number of unique hosts to generate | | `-clients` | `100` | Number of unique clients to generate | | `-d` | `7` | Number of days to spread random timestamps over | | `-debug` | `false` | Enable debug logging | | `-realistic` | `false` | Generate hostnames under security-profile domains (`*.recommended.test`, `*.secure.test`, `*.weak.test`, `*.insecure.test`) for use against the `tlslab` Docker test network | | `-docker-hosts` | — | Output path for a dnsmasq hosts file (only used with `-realistic`) | ## Realistic mode With `-realistic`, generated hostnames are distributed across four security profiles — 30% recommended, 35% secure, 25% weak, 10% insecure — plus a fixed set of certificate edge-case hosts (`expired.certs.test`, `selfsigned.certs.test`, `wronghost.certs.test`, `shortkey.certs.test`, `sha1.certs.test`, `expiring-soon.certs.test`). Combined with `-docker-hosts`, it writes a dnsmasq hosts file mapping each profile to a fixed IP on the `172.30.0.0/24` test network (`.10`–`.14`), matching the addresses `tlslab`'s Docker test endpoints listen on. ## Examples ```bash # Quick local dataset: 90 days, 300 hosts, 5000 handshakes go run cmd/datagen/datagen.go -d 90 -q 5000 -hosts 300 -db insights -U default # Larger dataset with explicit ClickHouse connection details go run cmd/datagen/datagen.go -h localhost -p 19000 -U insights -db insights \ -d 30 -q 10000 -hosts 50 -clients 100 # Realistic mode for testing against the tlslab Docker network go run cmd/datagen/datagen.go -realistic -docker-hosts /tmp/dnsmasq-hosts.conf ``` Before regenerating a large dataset, clear the existing tables: ```sql TRUNCATE TABLE tls_hellos; TRUNCATE TABLE ja3_fingerprints; TRUNCATE TABLE ja3_malicious; ``` --- title: ja3import url: https://docs.eoncore.eu/eon-insights/tools/ja3import/ --- ## Purpose `ja3import` populates ClickHouse's JA3 fingerprint tables (`ja3_fingerprints`, `ja3_malicious`) from three sources: a built-in list of common browser/tool fingerprints, the Trisul JA3 fingerprint database, and abuse.ch's SSLBL malicious-JA3 feed. It can also import from a local CSV seed file. These tables are what `capture` and the dashboard use to label a TLS client as a known browser, tool, or malware family. ## Usage ``` ja3import -c [-seed ] [-debug] ``` | Flag | Description | |------|-------------| | `-c` | Path to config file (required) | | `-seed` | Path to a CSV seed file to import fingerprints from | | `-debug` | Enable debug logging | ## Configuration ```yaml name: ja3import clickhouse: host: localhost port: 19000 database: insights username: insights password: secret debug: false ``` The Trisul and SSLBL source URLs are *not* set in this file — they're read from the ClickHouse `config` table (keys `ja3_trisul_url` and `ja3_sslbl_url`), so they can be changed without redeploying. Every run imports, in order: (1) the built-in common-browser fingerprints, (2) the CSV seed file if `-seed` is given, (3) Trisul fingerprints, (4) SSLBL malicious fingerprints. A failure in one source is logged but does not stop the others from running. ### Seed CSV format ``` ja3_hash,description,category,os,tls_library ``` `ja3_hash` must be exactly 32 hex characters; malformed rows are skipped. ## Examples ```bash # Import from the online sources (Trisul + SSLBL) plus common browsers ja3import -c configs/ja3import.yaml # Also import a local seed file ja3import -c configs/ja3import.yaml -seed test/data/ja3_fingerprints_seed.csv ``` --- title: Architecture url: https://docs.eoncore.eu/eon-aethis/architecture/ --- ## Central platform The central platform is a set of Docker Compose services: - **Backend (Go).** The API server (chi router, pgx for PostgreSQL). Owns authentication (JWT), the host/service/hostgroup/dashboard/discovery/ dependency/cloud/notification REST API, the AI prompt endpoint (direct HTTP calls to the Anthropic Messages API — no SDK), and ingestion endpoints for the Agent, Probe, and Perimeter Probe. Reads Zeek's rotated logs directly off a shared volume and writes bootstrap/migration state to PostgreSQL on startup. - **Frontend (React + TypeScript + Vite).** The web UI: dashboard builder, host/service views, discovery inbox, dependency topology map, AI chat panel, and admin screens. Served by the Vite dev server behind Caddy in development; a static production build (`npm run build`) served by the `serve` static file server behind Caddy in production. Talks to the backend over `/api`. - **Icinga 2.** The active-check monitoring engine (hosts, services, notifications). The backend drives it entirely through its REST API and config sync — Eon Aethis does not modify or embed Icinga 2 code. - **PostgreSQL.** The application database: users, hosts, services, dashboards, discovery jobs, dependencies, cloud connections, audit log, and other relational state (see the `go-backend/migrations` history for the full schema evolution). - **VictoriaMetrics.** Time-series storage for Icinga performance data, queried by the backend's `/metrics` endpoints in Prometheus format. - **ClickHouse.** Storage for high-volume probe telemetry — `probe_events`, `network_flows`, and enrichment data from the Aethis Probe and Zeek. The probe *entities* (which probes exist, their pairing state) live in PostgreSQL; the *events* they produce live in ClickHouse. - **Redis.** Cache and job/event queue for the backend. - **Zeek.** Passive network traffic analysis on a monitored interface, running with `network_mode: host` and `NET_RAW`/`NET_ADMIN` capabilities. Writes rotated logs to a shared volume that the backend reads and a janitor process prunes on a retention/disk-size policy. - **Caddy.** Reverse proxy and TLS termination in front of the backend and frontend. Routes `/api/*` to the backend with the prefix stripped, and `/agents/*`, `/probes/*`, `/probe/*` directly to the backend without stripping (these are the Agent/Probe ingestion and pairing endpoints). Everything else falls through to the frontend. ## Distributed collection components These run outside Docker Compose, on the hosts and networks they observe, and pair with the central platform over the network: - **Aethis Agent.** A Go binary installed on individual monitored hosts. Runs local system metrics collection (CPU, memory, disk, network, processes), executes Nagios/monitoring-plugins-compatible checks and submits results to the backend (replacing NSCA), and optionally parses local web server access logs for web analytics. Registers with the backend by token or by an interactive pairing flow. - **Aethis Probe.** A Go binary that passively observes a network segment: it captures live packets and NetFlow/IPFIX/sFlow exports, classifies traffic and devices, watches ARP/DHCP traffic to build a picture of what's on the segment, and buffers events locally (SQLite) before pushing them to the backend over HTTPS at a configurable interval. Used for network discovery and flow visibility without installing an agent on every host. - **Perimeter Probe.** A separately hosted, multi-tenant reconnaissance service. It performs external scans against a tenant's public-facing infrastructure on request and calls back to that tenant's Eon Aethis instance with results. Tenants and the probe operator authenticate each other with mutually-trusted certificates issued by an operator CA; the probe extracts the tenant ID from the client certificate's CN on each incoming job request. ## How they interact ```mermaid flowchart TB subgraph Central[Central platform — Docker Compose] Caddy[Caddy reverse proxy] FE[Frontend] BE[Backend Go API] Icinga[Icinga 2] PG[(PostgreSQL)] VM[(VictoriaMetrics)] CH[(ClickHouse)] Redis[(Redis)] Zeek[Zeek] end Agent[Aethis Agent
on monitored hosts] Probe[Aethis Probe
on a network segment] Perimeter[Perimeter Probe
external, multi-tenant] User((Operator / browser)) User -- HTTPS --> Caddy Caddy -- "/api/*" --> BE Caddy -- "everything else" --> FE Caddy -- "/agents/*, /probes/*, /probe/*" --> BE BE -- REST API --> Icinga BE --> PG BE -- perf data query --> VM BE -- probe telemetry --> CH BE --> Redis Zeek -- rotated logs (volume) --> BE Icinga -- perf data writer --> VM Agent -- "register, heartbeat,
metrics, check results" --> BE Probe -- "pair, push buffered events" --> BE Perimeter -- "mTLS callback with scan results" --> BE BE -- "dispatch scan job (mTLS)" --> Perimeter ``` A browser request goes through Caddy, which routes API calls to the backend and everything else to the frontend. The backend is the hub: it drives Icinga 2's REST API for active checks, reads VictoriaMetrics for performance graphs, reads/writes ClickHouse for probe telemetry, and reads Zeek's logs directly off a shared volume. The Aethis Agent and Aethis Probe are pulled in the other direction — they initiate outbound connections to the backend to register, heartbeat, and push data, so no inbound port needs to be opened on the hosts or segments they run on. The Perimeter Probe is the exception: the backend calls out to it (over mutual TLS) to start a scan, and the probe calls back with results. ## AI configuration flow The AI engine sits inside the backend and uses the Claude API's tool-use support to turn a prompt into a sequence of Icinga 2 API calls: it loads the current hosts/services/hostgroups as context and asks Claude to produce the calls needed. The backend executes every proposed action immediately, in the same request, before the response reaches the browser — there is no dry-run, conflict/dependency check, or operator-confirmation gate. The response carries a `requires_confirmation` flag and the frontend's chat UI renders Confirm/Cancel buttons, but they are currently non-functional stubs: they only relabel the chat message ("Actions confirmed and applied" / "Actions cancelled") and do not call the backend — by the time they're shown, the actions have already executed, so neither button blocks or undoes anything. Network discovery is different: AI-assisted discovery seeds subnets and classifies devices, but discovered hosts and dependency proposals land in an approval inbox (`/discovery/inbox/*`) that genuinely requires an operator to approve or bulk-approve them before they take effect — unlike the AI chat assistant above, this is a real confirmation gate. --- title: Architecture url: https://docs.eoncore.eu/eon-path/concepts/architecture/ --- ## Components EON Path is split into a small number of independently deployable components: - **Server daemon.** Terminates client connections, runs the post-quantum key exchange, and manages the WireGuard tunnel and, where enabled, the IPv4/IPv6 SIIT translation and NAT for internet access. - **API server.** A web management service that stores registered public keys and endpoint configuration. During authentication, the server daemon looks up a client's public key fingerprint against the API to confirm it is registered and active. - **Desktop client.** A GUI application (Wails: Go backend, web frontend) that manages one or more server connections, verifies server identity, and drives the tunnel handshake. - **Mobile apps.** Android and iOS clients built on the same Go core as the desktop client, sharing the cryptographic identity and key-exchange code so behavior is consistent across platforms. ## How they interact A connection starts as a TCP key-exchange session between a client (desktop or mobile) and the server daemon: the client authenticates itself with an ML-DSA-87 signature, the two sides verify each other's identity, and they derive a WireGuard pre-shared key from a post-quantum key encapsulation exchange. During authentication, the server daemon calls out to the API server to confirm the client's public key is registered. Once the handshake completes, the client and server exchange WireGuard public keys and bring up a WireGuard tunnel configured with the derived key material — from that point on, traffic flows over the encrypted tunnel like any other WireGuard session. ```mermaid flowchart LR subgraph Clients D[Desktop client] M[Mobile app] end subgraph Server[Server daemon] K[Key-exchange listener] W[WireGuard tunnel] end A[API server] N((Internet)) D -- "ML-DSA-87 challenge-response + ML-KEM-1024 KEX" --> K M -- "ML-DSA-87 challenge-response + ML-KEM-1024 KEX" --> K K -- "verify key fingerprint" --> A K -- "configure peer" --> W D <-. "encrypted VPN traffic" .-> W M <-. "encrypted VPN traffic" .-> W W --> N ``` ## Two ways to connect EON Path supports two connection methods. The **routing method** — the focus of this documentation — brings up a full Layer 3 WireGuard VPN tunnel, keyed through the post-quantum handshake described above, and is available on desktop and mobile clients. An older **proxy method** tunnels individual application connections through an SSH channel authenticated with the same family of post-quantum key material, exposed to local applications as a SOCKS/HTTP proxy; it is desktop-only and does not provide a full VPN tunnel. Both methods authenticate clients and servers with post-quantum cryptography; this documentation focuses on the routing method, since it is the one used by both desktop and mobile clients and the one that pairs with WireGuard and SIIT. ## Where things run The server daemon and API server are typically deployed together on server-side infrastructure that the desktop and mobile clients connect to over the network — for example a server reachable at `vpn.example.com`. The API server does not need to be reachable by end users directly; only the server daemon's key-exchange and WireGuard ports need to be open to clients. --- title: fingerprintd url: https://docs.eoncore.eu/eon-insights/tools/fingerprintd/ --- ## Purpose `fingerprintd` is a small standalone HTTP API that serves a centralized TLS fingerprint database — the same `fingerprints`, `malicious_fingerprints`, `fingerprint_sources`, and `fingerprint_changelog` ClickHouse tables that back JA3 lookups elsewhere in the system. It runs a scheduled aggregation job (cron-configured) that rolls up fingerprint statistics, and exposes a health check and the fingerprint-service routes over HTTP with CORS enabled for browser access. ## Usage ``` fingerprintd -c [-debug] ``` | Flag | Description | |------|-------------| | `-c` | Path to config file (required) | | `-debug` | Enable debug logging | ## Configuration ```yaml name: fingerprintd debug: true listen: "localhost:8080" clickhouse: host: localhost port: 19000 database: insights username: insights password: secret # Scheduled aggregation (collects fingerprint stats) aggregate_schedule: "0 2 * * *" # cron format, default: daily at 2am aggregate_enabled: true # default: true ``` `listen` defaults to `:8080` and `aggregate_schedule` to `"0 2 * * *"` if left unset. On startup, `fingerprintd` validates that the four fingerprint tables exist before starting the HTTP server and aggregation scheduler; it stops the scheduler and drains in-flight requests (30s timeout) on SIGINT/SIGTERM. ## Example ```bash fingerprintd -c configs/fingerprintd.yaml -debug ``` A health check is available at `GET /health` once the server is running. --- title: Scanning url: https://docs.eoncore.eu/eon-kartis/scanning/ --- Eon Kartis runs discovery and scanning as a pipeline across three processes, Postgres, and Redis pub/sub: ``` Redis "discovery" Redis "scan" operator ────────publish────► kartis-discovery ──publish──► kartis-scan │ ↓ ↓ (Redis "enum") discovered_domains scan_runs → scan_findings ↓ scan_services kartis-recon scan_analyzer_results scan_pqc_signals ``` ## Stage A — domain discovery `kartis-discovery` subscribes to the Redis channel `discovery` and, for a target domain, runs a set of discovery sources in parallel. Every result is stored in the `discovered_domains` table: | Source | Code | What it does | |---|---|---| | WHOIS | `internal/discovery/whois.go` | Registrant / nameserver pivots | | CT logs | `internal/discovery/ctlogs.go` | Certificate Transparency subdomain harvest | | DNS recon | `internal/discovery/dnsrecon.go` | Brute-force / DNSDB / reverse lookups | | IP-ASN | `internal/discovery/ipasn.go` | ASN expansion, neighboring prefixes | | Tech fingerprint | `internal/discovery/techfp.go` | HTTP/TLS fingerprint → related hosts | | AI correlation | `internal/ai/` | Optional LLM pass correlating the harvest into same-org relations | Subdomain enumeration itself is delegated over Redis (channel `enum`) to a dedicated worker, `kartis-recon`, which runs the enumeration toolchain (subfinder + DNS brute-force, plus optional AI-suggested candidates that are DNS-validated before use) and an `httpx`-based liveness/certificate/IP-capture pass. It publishes completion on `enum-result`; if Redis or the worker is unavailable, `kartis-discovery` falls back to enumerating in-process so a scan degrades rather than stalling. The orchestrator entry point is `internal/discovery/orchestrator.go` → `RunDiscovery(ctx, jobUUID)`. ### Recursive organization discovery Beyond a single domain's subdomains, `kartis-discovery run ` expands into related same-organization domains (found via WHOIS/DNS-recon/reverse-IP pivots) and enumerates each of those in turn, up to a configurable depth: ``` kartis-discovery run [flags] --depth related-expansion hops; 0 = unlimited (default 2) --min-confidence minimum confidence to expand a related domain --max-related-per-job per-job cap on related domains expanded --max-jobs per-tree total job cap --timeout max time to wait for the tree to finish (default 30m) --json emit JSON instead of a tree (default false) --judge final AI review pass flagging related domains that may not belong to the org — needs ai.api_key (default true) ``` The command prints a job tree annotated with each related domain's discovery source and confidence, and — when `--judge` is enabled and an AI provider is configured — a `⚠ review` flag on relations the model doesn't think actually belong to the seed organization. ## Stage B — DNS filter and scan queue Internally to `kartis-discovery` (`internal/discovery/scand_bridge.go`), once discovery for a job finishes: 1. Every `discovered_domains` row for the job is DNS-resolved (4-second timeout per host). 2. One `scan_session` and one `scan_run` is created per resolvable host — one session per host avoids an N² blow-up in nmap targets. 3. Each new run UUID is published on the Redis channel `scan`. There is no manual trigger for this stage in isolation; it always runs immediately after Stage A completes. ## Stage C — per-host scan `kartis-scan daemon` subscribes to the Redis channel `scan`. For each run it: 1. Runs **port discovery** (`internal/portscan/`), backed by nmap or a bare TCP-connect probe, at a chosen pacing profile. 2. Runs every matching **analyzer** in parallel for each discovered service. 3. On the DB-write path only, runs an **`ipleak`** post-pass (`internal/analyzers/iplook/`) over every analyzer's raw evidence, looking for leaked internal IP addresses (RFC 1918 / CGN / link-local / ULA) and recording one `scan_findings` row per leak. The orchestrator entry point is `internal/scan/orchestrator.go` → `RunSession(ctx, runUUID)`. ## Depth and category model Every scan chooses a **max depth** and a **category** filter. `kartis` runs every module at or below the chosen depth, shallow-first (`internal/scan/level.go`, `internal/scan/registry.go`): | Depth (`max_level`) | Meaning | Intrusiveness | |---|---|---| | `discover` | Port/service discovery only | Benign | | `passive` | Reads what the target offers, no crafted input (TLS cert/handshake read, tech fingerprint, WAF header sniff) | Benign | | `active` | Speaks the protocol interactively but benignly (SSH/IKE/OpenVPN/STARTTLS negotiation) | Benign | | `vuln` | Safe vulnerability **detection**, without exploitation | Non-destructive | There is deliberately no `offensive`/attack level: attack-shaped probing (for example WAF XSS/SQLi/LFI payloads) is not reachable through the API or the scan chain by construction. Requesting an unknown level returns an error (HTTP 400 on the API). | Category | Modules | |---|---| | `discovery` | port scan | | `crypto` | tls, starttls, ssh, ike, openvpn, plaintext, certinfo — the PQC/crypto posture core of the product | | `web` | waf (passive only in the chain) | | `infoleak` | internal-IP-leak detection (`ipleak`) | Omitting categories runs all four; passing a subset (e.g. `["crypto","web"]`) scopes the scan. ## CLI ### `kartis-scan scan` — standalone, no DB, no daemon Prints JSON to stdout. Best for fast iteration on a single host; runs nmap plus every analyzer at or below the chosen depth. The `ipleak` post-pass does **not** run in this mode (it only fires on the DB write path). ``` kartis-scan scan [flags] --profile scan profile / nmap pacing (default "polite") --max-level discover|passive|active|vuln (default "active") --categories discovery,crypto,web,infoleak (default: all) --only run a single chain module by name --engine port discovery engine: nmap|connect (default "nmap") --json output JSON to stdout (default true) ``` `--engine connect` uses a single TCP connect per port — the gentlest liveness probe, with no nmap dependency. ```bash kartis-scan scan qgf.io kartis-scan scan qgf.io --profile normal --max-level passive --categories crypto ``` `--profile` accepts `polite`, `normal`, or `aggressive` (see the profiles table above); any other value silently falls back to `polite` rather than erroring. ### `kartis-scan daemon` — DB-backed dispatch ``` kartis-scan daemon -c /etc/kartis/kartis-scan.yaml ``` Claims pending rows from `scan_runs` on a 5-second poll (the DB is the source of truth), with an optional Redis pub/sub nudge for low-latency dispatch. A bounded worker pool executes up to `concurrent_runs` (from the active scan profile) runs at once, and a stale-run reconciler fails runs orphaned by a previous crash. ### `kartis-scan serve` — HTTP API ``` kartis-scan serve -c /etc/kartis/kartis-scan.yaml ``` Serves the bearer-token-authenticated `/v1/...` API (sessions, runs, findings, services, discovery reads) that Eon Center, Eon Aethis, and Eon Insights consume. See [API](#api) below. ### `kartis-discovery` / `kartis-recon` ``` kartis-discovery [daemon] # subscribes to "discovery", runs the daemon (default with no subcommand) kartis-discovery run # recursive organization discovery, see above kartis-recon # subscribes to "enum", runs the enumeration worker ``` Both take `-c/--config ` (defaults `/etc/kartis/kartis-discovery.yaml` and `/etc/kartis/kartis-recon.yaml`) and `-d/--debug`. ### `kartis-waf` — standalone WAF fingerprinting A Go port of [wafw00f](https://github.com/EnableSecurity/wafw00f), detecting around 172 named WAFs plus generic/behavioral detection. Its engine (`internal/waf`) is also reusable as a scan-flow analyzer; the `waf` module in the chain above runs it passive-only. ``` kartis-waf scan [flags] --active send attack-payload probes (requires --authorized) --authorized confirm you are authorized to send active probes --all report all matching WAFs, not just the first --json emit JSON (default true; --json=false for human output) --timeout per-request timeout (default 30s) kartis-waf serve --addr :8091 --token ``` Passive detection (the default) sends one benign `GET /` and matches header/cookie signatures. Active detection additionally sends attack-shaped payloads (XSS/SQLi/LFI) and is refused unless `--authorized` (CLI) or `"authorized": true` (the `POST /waf` service body) is set. Every request carries an honest `scanid` User-Agent. ## API `kartis-scan serve` exposes, under bearer-token auth: ``` POST /v1/sessions create a session (name + targets) GET /v1/sessions list sessions GET /v1/sessions/{uuid} get a session POST /v1/sessions/{uuid}/runs start a run — this is where profile/max_level/categories go GET /v1/sessions/{uuid}/runs list runs for a session GET /v1/runs/{uuid} run status: pending|running|completed|failed GET /v1/runs/{uuid}/findings findings for a run GET /v1/runs/{uuid}/services discovered services for a run GET /v1/services/{uuid}/tls TLS detail for a service GET /v1/discovery/trees, /trees/{root}, /trees/{root}/hosts POST /v1/discovery/runs, /runs/batch trigger discovery GET /v1/discovery/certificates, /dns, /tech, /domains, /hosts, /aggregate GET /v1/healthz, /v1/readyz unauthenticated health checks ``` `POST /v1/sessions/{uuid}/runs` accepts an optional body: ```json { "profile": "normal", "max_level": "active", "categories": ["crypto", "web"] } ``` An empty body reproduces the historical default (`profile=polite`, `max_level=active`, all categories). The run currently executes synchronously — the `POST` blocks until the scan completes and returns a summary — but clients should still poll `GET /v1/runs/{uuid}` until a terminal status rather than assuming completion, since Kartis may move run execution to asynchronous later. ## Operator scripts Two wrapper scripts (`scripts/scan.sh`, `scripts/full-scan.sh`; deployed copies live at `/opt/kartis/scripts/`) drive the API for manual, one-off runs: ```bash scripts/scan.sh qgf.io # one host via the API — creates a session, runs sync, prints services/findings/TLS detail scripts/full-scan.sh qgf.io > qgf.json # full pipeline: publish discover → poll → aggregate everything into one JSON document ``` `full-scan.sh`'s output is the consolidated JSON document that [`kartis-scan report`](/eon-kartis/reports/) consumes. --- title: Server url: https://docs.eoncore.eu/eon-path/configuration/server/ --- ## Environment file `eonpath-server` reads its configuration from `/etc/eonpath/server.env`, an environment file loaded by the systemd unit (`EnvironmentFile=`). This is the configuration surface installed and pre-populated by the `eonpath-server` `.deb` package; see [Install: Server](/eon-path/install/server/) for the install steps. ```bash # /etc/eonpath/server.env # SSH listener (post-quantum SSH proxy method) EONPATH_SSH_ADDR=0.0.0.0:2022 # Local PQKS API endpoint (used to look up client keys when a client connects) EONPATH_API_ENDPOINT=http://localhost:8080 # HTTP CONNECT proxy address (for the proxy method's tunnels) EONPATH_PROXY_ADDR=localhost:2290 # SSH host key path (generated by the postinstall script on first install) EONPATH_HOST_KEY=/var/lib/eonpath/host_key # Per-user authorized_keys directory EONPATH_AUTHORIZED_KEYS_DIR=/var/lib/eonpath/authorized_keys # WireGuard VPN settings (for the routing method) EONPATH_WG_KEX_ADDR=0.0.0.0:9998 EONPATH_WG_LISTEN_PORT=51820 # Security audit log (empty discards the events) EONPATH_AUDIT_LOG=/var/lib/eonpath/audit.log ``` After editing, apply the change with: ```bash sudo systemctl restart eonpath-server ``` | Key | Type | Default | Description | |-----|------|---------|-------------| | `EONPATH_SSH_ADDR` | address | `0.0.0.0:2022` | Listen address for the post-quantum SSH proxy method. | | `EONPATH_API_ENDPOINT` | URL | `http://localhost:8080` | Address of the `eonpath-api` server, used to look up a connecting client's registered public key. | | `EONPATH_PROXY_ADDR` | address | `localhost:2290` | Listen address for the HTTP CONNECT proxy that backs the proxy method's tunnels. | | `EONPATH_HOST_KEY` | path | `/var/lib/eonpath/host_key` | Path to the server's SSH host key, generated on first install. | | `EONPATH_AUTHORIZED_KEYS_DIR` | path | `/var/lib/eonpath/authorized_keys` | Directory of per-user `authorized_keys` files consulted by the SSH proxy method. | | `EONPATH_WG_KEX_ADDR` | address | `0.0.0.0:9998` | TCP listen address for the WireGuard routing method's key-exchange handshake (see [API](/eon-path/api/) for the protocol). | | `EONPATH_WG_LISTEN_PORT` | int | `51820` | UDP port for the WireGuard data tunnel. | | `EONPATH_AUDIT_LOG` | path | `/var/lib/eonpath/audit.log` | Security audit log. It has to stay under a `ReadWritePaths` directory, since the unit sets `ProtectSystem=strict`; set it empty to discard the events. Rotated by `/etc/logrotate.d/eonpath-server`. | The routing method itself (`-wg-enabled`) is always passed by the packaged systemd unit's `ExecStart` line — the shipped `.deb` runs both the SSH proxy method and the WireGuard routing method from a single `eonpath-server` process. Disabling the routing method requires editing the systemd unit directly, since it isn't exposed through the environment file. ## `server.conf`: registration and endpoints The unit also passes `-config /etc/eonpath/server.conf`. Flags override the file, and the unit passes a flag for every `ssh:`, `proxy:`, `api:` and `wireguard:` value — so on a stock `.deb` install **those sections have no effect**; change them in `server.env` instead, or drop the corresponding flag with `systemctl edit eonpath-server`. The `registration:` and `endpoints:` sections have no flag counterpart. They are why the file exists. ```yaml registration: enabled: true api_url: "https://api.example.com" # also the signed audience license_key_file: /etc/eonpath/license_key timeout: 10s max_attempts: 8 endpoints: - name: example-ams-1 method: routing # WireGuard VPN with SIIT/NAT mode: standby # standby | active tunnel_ip: 203.0.113.10 # public address clients connect to tunnel_port: 51820 kex_address: 203.0.113.10:9998 # required for method: routing companion_hosts: - git.example.com - name: example-ams-1-proxy method: proxy # HTTP CONNECT proxy + SSH port forwarding mode: standby tunnel_ip: 203.0.113.10 port: 2022 # required for method: proxy forward_port: 9999 ``` The server publishes these endpoints to the API, signed with its ML-DSA-87 identity key. The first push also presents the licence key; after that the signature alone authenticates the server, and the key can be removed from the host. Registration failing never stops the server — an invalid endpoint entry is skipped with an error in the journal and everything else keeps serving. `tunnel_ip` is mandatory and never auto-detected: behind NAT, detection would register the private address and produce an endpoint no client can reach. Every name has to be covered by the licence *and* fall inside a name prefix reserved for your organization. The licence key is read from the first source that has it: `-license-key`, `EONPATH_LICENSE_KEY`, `license_key` in this file, `license_key_file`. Prefer the file or the environment — the example ships world-readable. After editing the endpoint list: ```bash sudo systemctl reload eonpath-server # re-reads server.conf, re-pushes, drops no tunnel ``` ### `mode: standby` and `mode: active` | Mode | What the server does | Published to clients | |---|---|---| | `standby` | Registers itself and nothing else — no WireGuard interface, no kex listener, no proxy listener | No | | `active` | Serves traffic normally | Yes | A standby endpoint doesn't appear in `GET /endpoints` at all, so no client ever learns it exists. The state is a commercial fact with a technical consequence, so it's recorded in the API and enforced on the server: every transition is logged with a timestamp, and the first activation starts a per-endpoint free evaluation window (`trial_days` on the licence, 30 by default) that is granted once and does not restart. Switching state is an edit here plus `systemctl restart eonpath-server` — a restart, not a reload. In standby there was nothing serving to interrupt. Omitting `mode` means `active`, so configs written before the option existed keep working. A field that is present but empty, or holds an unknown value, rejects that endpoint with an error in the journal rather than guessing: a guess costs money in one direction or the other. A fresh install ships `mode: standby`. ## WireGuard VPN settings (command-line flags) A few less common WireGuard routing-method settings are only available as `eonpath-server` command-line flags — they can be added to the `ExecStart=` line in `/lib/systemd/system/eonpath-server.service` (or a systemd drop-in) if you need to change them from their defaults. | Key | Type | Default | Description | |-----|------|---------|-------------| | `-wg-interface` | string | `wg0` | WireGuard network interface name. | | `-wg-server-ip` | string | `fd00:4464::a00:1` | Server's internal address inside the VPN's IPv6 lease range. | | `-wg-subnet` | string | `fd00:4464::a00:0/112` | IPv6 subnet the server leases client addresses from. | | `-wg-masquerade` | bool | `true` | Enable NAT masquerading so VPN clients can reach the internet through the server. | ## SSH proxy method and general server flags | Key | Type | Default | Description | |-----|------|---------|-------------| | `-config` | path | `/etc/eonpath/server.conf` on a packaged install | Path to the YAML configuration file described above; command-line flags override values loaded from it. | | `-license-key` | string | *(none)* | Licence key for endpoint registration; takes precedence over `EONPATH_LICENSE_KEY` and both `server.conf` sources. | | `-disable-ssh` | bool | `false` | Disable the SSH proxy method's listener entirely. | | `-proxy-validate-destinations` | bool | `false` | Enable SSRF protection: validate HTTP CONNECT destinations against security rules before dialing them. | | `-key-revocation-interval` | duration | `5m` | How often the server polls the API for revoked keys and removes them from `authorized_keys`. Set to `0` to disable polling. | | `-drop-privileges` | bool | `false` | Drop root privileges after binding to listen ports. | | `-run-as-user` | string | *(auto-detected)* | User to run as after dropping privileges; used with `-drop-privileges`. | ## Post-quantum key exchange (SSH proxy method) The WireGuard routing method's key exchange (ML-DSA-87 authentication, ML-KEM-1024 encapsulation) is fixed and not configurable — see [Post-Quantum Cryptography](/eon-path/concepts/post-quantum/). The SSH proxy method's key exchange algorithm selection can be tuned independently, either as command-line flags or the equivalent environment variables: | Key | Type | Default | Description | |-----|------|---------|-------------| | `-pqc-mlkem` / `PQKEX_MLKEM` | bool | `true` | Enable ML-KEM-768 hybrid key exchange for the SSH proxy method. | | `-pqc-frodokem` / `PQKEX_FRODOKEM` | bool | `false` | Enable FrodoKEM-640 hybrid key exchange (experimental). | | `-pqc-ntruprime` / `PQKEX_NTRUPRIME` | bool | `false` | Enable sntrup761 hybrid key exchange (experimental). | | `-pqc-prefer-classical` / `PQKEX_PREFER_CLASSICAL` | bool | `false` | Prefer classical (non-PQC) key exchange algorithms when negotiating with a peer. | | `-pqc-order` / `PQKEX_ORDER` | string | *(none)* | Comma-separated algorithm priority order (for example `mlkem,frodokem`); setting this auto-enables the listed algorithms and overrides the individual flags above. | Command-line flags take precedence over the equivalent environment variable. --- title: Server url: https://docs.eoncore.eu/eon-path/install/server/ --- ## Overview The EON Path server is distributed as two Debian packages: - **`eonpath-api`** — the management API (PQKS: post-quantum key server). Stores registered client public keys and endpoint configuration, and exposes a REST API and web UI. - **`eonpath-server`** — the tunnel server daemon. It authenticates clients with the ML-DSA-87 challenge-response protocol and, in a single process, serves both of EON Path's connection methods: an SSH-based proxy method and a WireGuard-based routing method (the VPN this documentation focuses on). The routing method is enabled by default in the packaged systemd unit. Both packages install systemd units, create a dedicated `eonpath` system user, and generate the secrets and host keys the services need on first install. ## Prerequisites - Debian 11+ or Ubuntu 22.04+, amd64 - Root or sudo access - A firewall that allows inbound UDP on the WireGuard port (see [Firewall](#firewall) below) Only amd64 `.deb` packages are published; there is currently no arm64 package. ## Install the packages Download the `eonpath-api` and `eonpath-server` `.deb` files for your release, then install the API first so the server's public-key lookups work immediately: ```bash sudo apt install ./eonpath-api_*.deb # API + web UI sudo apt install ./eonpath-server_*.deb # SSH proxy + WireGuard VPN ``` Installing with `apt install ./.deb` (rather than `dpkg -i`) also resolves the packages' dependencies (`libc6`, `iproute2`) automatically. On first install, each package's postinstall script: - creates the `eonpath` system user and group (shared by both packages) - creates `/etc/eonpath` and `/var/lib/eonpath` - generates an ed25519 SSH host key for `eonpath-server` - generates random `JWT_SECRET` and `DB_ENCRYPTION_KEY` values for `eonpath-api` - seeds `/etc/eonpath/server.conf` and `/etc/eonpath/server.env` from their `.example` files if they don't exist yet — an existing file is left alone - enables and starts the corresponding systemd service ### What gets installed | Path | Description | |------|-------------| | `/usr/bin/eonpath-server` | server binary | | `/usr/bin/eonpath-api` | API binary | | `/lib/systemd/system/eonpath-server.service` | systemd unit | | `/lib/systemd/system/eonpath-api.service` | systemd unit | | `/usr/bin/eonpath-license` | licence helper shipped with the API package | | `/etc/eonpath/server.env` | server configuration (environment file), seeded from `server.env.example` | | `/etc/eonpath/server.conf` | endpoint registration and endpoint list, seeded from `server.conf.example` (mode `0640`) | | `/etc/eonpath/api.env` | API configuration, including generated secrets | | `/usr/share/eonpath/web` | the API's web UI | | `/usr/share/doc/eonpath-*/ENDPOINT_LIFECYCLE.md` | standby/active states, activation and the evaluation window | | `/etc/logrotate.d/eonpath-{server,api}` | rotation for the audit logs | | `/var/lib/eonpath/` | state: SSH host key, `authorized_keys`, PQKS database | ## Configure the server Server settings live in `/etc/eonpath/server.env`, an environment file read by the systemd unit: ```bash # SSH listener (post-quantum SSH proxy method) EONPATH_SSH_ADDR=0.0.0.0:2022 # Local PQKS API endpoint (used to look up client keys when a client connects) EONPATH_API_ENDPOINT=http://localhost:8080 # HTTP CONNECT proxy address (for the proxy method's tunnels) EONPATH_PROXY_ADDR=localhost:2290 # WireGuard VPN settings (for the routing method) EONPATH_WG_KEX_ADDR=0.0.0.0:9998 EONPATH_WG_LISTEN_PORT=51820 ``` After editing, restart the service to apply the change: ```bash sudo systemctl restart eonpath-server ``` See [Configuration](/eon-path/configuration/) for the full reference of server settings. ## Start and enable the service The postinstall script already enables and starts `eonpath-server` on a fresh install. If you need to do it manually (for example after changing the unit file): ```bash sudo systemctl daemon-reload sudo systemctl enable eonpath-server sudo systemctl start eonpath-server ``` ## Verify it's running ```bash sudo systemctl status eonpath-server sudo journalctl -u eonpath-server -f ``` If the WireGuard routing method is enabled (the default), the server also brings up a `wg0` interface: ```bash ip addr show wg0 ``` ## Firewall Clients connect over UDP to the WireGuard routing method. Open the WireGuard port in your firewall: ```bash # UFW sudo ufw allow 51820/udp comment 'EON Path WireGuard' # iptables sudo iptables -A INPUT -p udp --dport 51820 -j ACCEPT ``` The routing method also needs its TCP key-exchange port reachable before a client can bring up a WireGuard tunnel: ```bash sudo ufw allow 9998/tcp comment 'EON Path WireGuard key exchange' ``` Clients connect to the server using its public address — a DNS name (for example `vpn.example.com`) or IP address (for example `203.0.113.10`). If you also use the SSH-based proxy method, open its port as well: ```bash sudo ufw allow 2022/tcp comment 'EON Path SSH proxy' ``` ## Upgrading and removing ```bash # Upgrade: install a newer .deb over the current install. Config, host # keys, and secrets are preserved. sudo apt install ./eonpath-server__amd64.deb # Remove, keeping state (host key, authorized_keys, api.env) sudo apt remove eonpath-server eonpath-api # Purge, wiping /etc/eonpath, /var/lib/eonpath, and the eonpath user sudo apt purge eonpath-server eonpath-api ``` --- title: pcapworker url: https://docs.eoncore.eu/eon-insights/tools/pcapworker/ --- ## Purpose `pcapworker` is an asynq worker daemon that processes `pcap:process` tasks — extracting TLS handshakes from PCAP files that users have uploaded through the dashboard (`POST /api/upload/pcap`) and writing the results to ClickHouse, the same way live `capture` traffic would be stored. It notifies progress back over Redis pub/sub so the dashboard can show upload processing status. ## Usage ``` pcapworker [-c ] [options] ``` | Flag | Default | Description | |------|---------|-------------| | `-c` | — | Path to config file | | `-debug` | — | Enable debug logging | | `-concurrency` | 10 | Number of concurrent workers | | `-uploadpath` | `/tmp` | Path where uploaded PCAP files are stored | | `-ports` | `443` | Comma-separated list of ports to filter for TLS | | `-redis-host` | `localhost` | Redis host | | `-redis-port` | `6379` | Redis port | | `-ch-host` | `localhost` | ClickHouse host | | `-ch-port` | `9000` | ClickHouse port | | `-ch-db` | `insights` | ClickHouse database | | `-ch-user` | `default` | ClickHouse username | | `-ch-pass` | — | ClickHouse password | CLI flags override the config file where both are given; the config file is optional and a built-in default config is used if `-c` is omitted. ## Configuration ```yaml name: pcapworker uploadpath: ~/tmp ports: - 443 concurrency: 10 debug: false redis: host: localhost port: 6379 password: "" db: 0 clickhouse: host: localhost port: 19000 database: insights username: insights password: "secret" ``` ## Example ```bash pcapworker -c configs/pcapworker.yaml -debug ``` --- title: notifyd url: https://docs.eoncore.eu/eon-insights/tools/notifyd/ --- ## Purpose `notifyd` is a scheduler-and-worker daemon that sends alerts for two recurring security events: certificates approaching expiry, and TLS handshakes matching a known-malicious JA3 fingerprint. It uses asynq's built-in cron-style scheduler to enqueue check tasks on a schedule, and a worker pool to process them and dispatch notifications over email (SMTP), Slack (webhook), or a generic webhook. Every notification sent is also logged to a ClickHouse `notifications` history table. ## Usage ``` notifyd -c [-debug] ``` | Flag | Description | |------|-------------| | `-c` | Path to config file (required) | | `-debug` | Enable debug logging | ## Configuration ```yaml name: notifyd clickhouse: host: localhost port: 19000 database: insights username: insights password: "secret" redis: host: localhost port: 6379 password: "" db: 0 debug: false concurrency: 3 # concurrent notification tasks, default 3 if unset/0 notifications: smtp_host: smtp.example.com smtp_port: 587 smtp_user: notifications@example.com smtp_password: "" smtp_from: "EonCore Insights " slack_webhook_url: "" cert_expiry_recipients: - security@example.com # - https://hooks.slack.com/services/xxx/yyy/zzz malware_recipients: - security@example.com - soc@example.com scheduler: cert_expiry_schedule: "0 9 * * *" # cron format cert_expiry_days: 30 # alert for certs expiring within N days malware_schedule: "*/5 * * * *" # cron format malware_minutes: 5 # check handshakes from the last N minutes ``` Recipients can be plain email addresses (routed through SMTP) or webhook URLs (Slack or generic), mixed freely in the same list. ### Cron schedule format ``` ┌───────────── minute (0 - 59) │ ┌───────────── hour (0 - 23) │ │ ┌───────────── day of month (1 - 31) │ │ │ ┌───────────── month (1 - 12) │ │ │ │ ┌───────────── day of week (0 - 6) │ │ │ │ │ * * * * * ``` Examples: `"0 9 * * *"` (daily at 9 AM), `"*/5 * * * *"` (every 5 minutes), `"0 */4 * * *"` (every 4 hours), `"0 9 * * 1"` (every Monday at 9 AM). ## Example ```bash # Run as a daemon notifyd -c configs/notifyd.yaml # Run with debug logging notifyd -c configs/notifyd.yaml -debug ``` --- title: sca url: https://docs.eoncore.eu/eon-insights/tools/sca/ --- ## Purpose `sca` is a one-shot Software Composition Analysis scanner. It scans a local repository path or clones a remote Git URL, detects Maven, npm, Go, and Cargo manifests, and reports known vulnerabilities in the resolved dependencies — without touching ClickHouse or any queue. It's the direct CLI equivalent of what [scascand](/eon-insights/tools/scascand/) runs as a queued daemon for repositories connected through the dashboard. ## Usage ``` sca -path | -url [options] ``` | Flag | Default | Description | |------|---------|-------------| | `-path` | — | Path to a local git repository to scan | | `-url` | — | URL of a git repository to clone and scan | | `-c` | — | Path to a configuration file | | `-format` | `text` | Output format: `text`, `json`, or `sarif` | | `-o` | stdout | Output file | | `-debug` | `false` | Enable debug logging | Exactly one of `-path` or `-url` must be given. The scanner exits with status 1 if any vulnerabilities are found (useful in CI), or 0 if the scan completed clean. ## Examples ```bash # Scan a local checkout sca -path /path/to/repo # Clone and scan a remote repository sca -url https://github.com/user/repo # JSON output written to a file, for CI pipelines sca -path /path/to/repo -format json -o results.json # SARIF output, for upload to GitHub code scanning sca -path /path/to/repo -format sarif -o results.sarif ``` --- title: scascand url: https://docs.eoncore.eu/eon-insights/tools/scascand/ --- ## Purpose `scascand` is the daemon counterpart to [sca](/eon-insights/tools/sca/): it polls ClickHouse for repositories flagged as pending a scan (connected through the dashboard via GitHub/GitLab OAuth, or added directly for public/SSH-accessible Git repos), authenticates using the stored, encrypted OAuth token or the local SSH agent, runs the same Maven/npm/Go/ Cargo dependency scan as `sca`, and writes findings back to ClickHouse. ## Usage ``` scascand -c [-debug] [-once] ``` | Flag | Description | |------|-------------| | `-c` | Path to config file (required) | | `-debug` | Enable debug logging | | `-once` | Run one polling pass and exit, instead of looping | ## Configuration ```yaml name: scascand clickhouse: host: localhost port: 19000 database: insights username: insights password: secret redis: host: localhost port: 6379 poll_interval: 30s # how often to check for pending scans, default 30s debug: true ``` For OAuth-connected repositories (GitHub, GitLab), the encrypted access token is decrypted using a key stored in the ClickHouse `config` table (`sca_encryption_key`, a 32-byte hex string — the same key `insights`'s `sca.oauth.encryption_key` setting uses). For repositories with provider `git` (generic, not OAuth-linked), `scascand` uses SSH-agent authentication for SSH URLs, or a plain unauthenticated clone for public HTTPS URLs. ## Example ```bash # Run as a daemon, polling every poll_interval scascand -c configs/scascand.yaml # Run a single poll pass and exit (useful for testing/cron) scascand -c configs/scascand.yaml -once ``` --- title: devctl url: https://docs.eoncore.eu/eon-insights/tools/devctl/ --- ## Purpose `devctl` is a terminal UI (built with Bubble Tea) for local development. It starts, stops, and monitors every Eon Insights service from one screen — `tlslab`, `insights`, `capture`, `hostsync`, `tlsscand`, `nmapscand`, `scascand`, `pcapworker`, `notifyd`, `fingerprintd`, and the dashboard's Vite dev server — using the binaries in `bin///`. It is a development convenience, not something you'd run in production. ## Usage ``` devctl ``` `devctl` takes no flags. It must be run from the project root (it checks for a `configs/` directory on startup) and opens a full-screen TUI. ## Key bindings | Key | Action | |-----|--------| | `↑`/`k`, `↓`/`j` | Move selection | | `enter` / `space` | Start or stop the selected service | | `l` | View the selected service's log tail | | `r` | Refresh status of all services | | `S` | Start all services | | `X` | Stop all services | | `t` | Run the integration test suite (`scripts/integration-test.sh`) | | `c` | Clean test data (`scripts/clean-testdata.sh --all -y`) | | `g` | Toggle synthetic TLS traffic generation against `tlslab` | | `s` | Queue a TLS and an nmap scan against `tlslab` (via the `insights` API) | | `d` | Open the `datagen` form (generate test data with custom parameters) | | `q` / `ctrl+c` | Quit (stops all running services first) | `capture` is marked as requiring sudo; `devctl` prompts for a password (cached via `sudo -n` for the rest of the session) before starting it. The dashboard's dev server (`npm run dev` in `web/dashboard`) has its actual listening port auto-detected from Vite's stdout, since Vite may pick a different port if 5173 is taken. ## Example ```bash # From the project root go run cmd/devctl/devctl.go # Or, once built ./bin/darwin/arm64/devctl ``` --- title: Portal url: https://docs.eoncore.eu/eon-kartis/portal/ --- The portal is the human-facing half of Kartis: a server-rendered web surface mounted at `/portal/...` inside the `kartis-scan` binary, next to the bearer-token `/v1/...` API that Eon Center, Eon Aethis and Eon Insights use. It is **off by default** — an existing deployment that only serves machine callers keeps exactly the surface it had. Turn it on in `kartis-scan.yaml` (see [Configuration](/eon-kartis/configuration/#portal)). No npm, no build step: `html/template` with `go:embed`, htmx vendored in `third_party/htmx/`, one hand-written stylesheet, and charts as server-generated inline SVG. ## Tenancy Both surfaces resolve to the same tenant boundary before any query runs: ``` kartis-scan ├── /v1/* bearer token machine callers: Eon Center, Eon Insights └── /portal/* cookie session humans │ └── both resolve to requested_by ``` A portal session resolves to the same `requested_by` value the API tokens already carry, rather than introducing a second scoping path. Eon Center's tokens keep `requested_by IS NULL` and stay cross-customer callers. ## Pages | Page | Route | Shows | |---|---|---| | Login | `/portal/login` | password plus a mailed one-time code | | Overview | `/portal/overview` | the estate as it stands after the newest completed run | | Hosts | `/portal/hosts` | one row per host from its newest completed run: IP, open ports, product/version, last seen | | Host detail | `/portal/hosts/{host}` | that host's services, certificates and findings, with per-finding advice | | Attack surface | `/portal/surface` | what exists and answers, from the recon pass — HTTP status and title, CNAME, A/AAAA, subdomain-takeover flags | | Certificates | `/portal/certificates` | the leaf certificate per TLS service, with expiry windows, wildcards and self-signed | | Crypto | `/portal/crypto` | crypto-relevant findings by severity, filterable down to PQC-relevant only | | Runs | `/portal/runs` | run history with status, trigger and counts | | Run detail | `/portal/runs/{uuid}` | one run's outcome | | Compare runs | `/portal/runs/compare?run=&run=` | two runs side by side | | Reports | `/portal/reports` | per run: date, domain, download | | Account | `/portal/account` | users, password change, domain verification status | **Hosts and Attack surface answer different questions.** Hosts shows what has been *scanned*; Attack surface shows what *exists and answers*, read from the recon pass's liveness data. The same estate can therefore look thin on one page and hold thousands of hosts on the other. Comparing two runs keys each kind of change on what makes it meaningful: | Change | Keyed on | |---|---| | hosts appeared / gone | host name | | ports opened / closed | `host:port` | | findings new / resolved | finding id + title, counted per severity | | certificates changed | `host:port` | ## Login and sessions | Concern | Behaviour | |---|---| | Cookie | `__Host-kartis_session`, HttpOnly, Secure, SameSite=Lax; 256-bit random, only its SHA-256 stored | | Lifetime | 12 hours idle, 7 days absolute | | Password | argon2id, 64 MiB / 3 iterations / 4 threads | | Second factor | six-digit code by email, 10 minutes, single use, hash stored — mandatory for every role | | CSRF | double submit: token cookie plus `hx-headers`, verified on every non-GET | | Brute force | backoff per account and per IP, lockout after 10 attempts | | Roles | `owner` (scope, on-demand runs, user management), `viewer` (read, report download) | | Audit | login, scope change, on-demand trigger, report download, verification change | The one-time code carries a code and nothing else — no findings, no host names, no customer name in the subject, so a misdelivered message discloses only that the address has a Kartis account. If SMTP submission fails the login returns a generic error and the failure is written to the audit table; the code is never shown in the response as a fallback, under any configuration. A login depends on mail arriving, so treat deliverability as part of the deployment: SPF authorising the sending path, DKIM signing on the sender address with DMARC aligned, and a dedicated transactional sender. ## Domain verification Per root domain, the customer publishes: ``` _kartis-verify.. TXT "kartis-verify=" ``` The scheduler refuses to run against an unverified domain. The record is re-checked monthly: one that disappears means the customer no longer controls the domain. Domains found by org expansion can't be furnished with a TXT record by definition — they're the ones the customer had forgotten. Two classes resolve that: | Class | Entry | Treatment | |---|---|---| | **Verified** | TXT record, or explicit in the signed scope | active scanning: port scan, TLS handshake, WAF detection | | **Observed** | found through the org graph | passive only: CT logs, DNS, WHOIS, GLEIF — no packet to their infrastructure | An observed domain renders with an *add to scope* action, which triggers a fresh verification. ## Scheduled runs `kartis-scheduler` is a separate binary that only needs the database — it doesn't scan, doesn't talk to Redis and serves no HTTP. On each tick it picks up sessions whose next slot has passed and inserts a pending run; the existing claim chain does the rest. Cadence is `monthly` or `biweekly` plus an anchor (day of month, or weekday), computed in Go — there is no cron expression to get wrong. The next slot is materialised, so the portal can show *next scan on 1 October* without recomputing it. Four rules worth knowing before you operate it: 1. A failed scheduled run is **not** retried automatically. The portal shows the failure; the customer restarts it. 2. Runs missed during downtime are caught up **once**, not once per missed slot. 3. On-demand quota is debited when a run **completes**, so a failed run costs the customer nothing. 4. Exceeding the host limit does not truncate the run. Everything is stored and the overage is recorded — a scan that silently stopped at host 501 would produce a report that lies. ## Deployment The portal is HTTPS-only by construction: its session cookie carries the `__Host-` prefix and the `Secure` flag, which a browser rejects over plain HTTP. Run it behind your own TLS front and set `trusted_proxies` to that proxy, so forwarded client addresses and the forwarded scheme are believed from it and nowhere else. `deploy/nginx/kartis.conf` in the source repository is a working reference: one public vhost carrying `/portal/` and nothing else, with `/v1/*` deliberately absent from it. The `insecure` option drops the cookie prefix and the `Secure` flag. It exists for a test over plain HTTP and for nothing else. --- title: Client url: https://docs.eoncore.eu/eon-path/configuration/client/ --- ## Config file location The desktop and mobile clients share the same configuration format, `eonpath.conf`, a YAML file. The desktop client looks for it at: | Platform | Path | |----------|------| | Linux / Windows | `~/.config/eonpath/eonpath.conf` | | macOS | `~/eonpath.conf` | The client rewrites this file when you change settings from the UI (adding a server, adding an endpoint, toggling the system proxy), keeping a `eonpath.conf.backup` of the previous version. Authentication tokens are **not** stored in this file — they're kept in the OS keyring (Keychain, Windows Credential Manager, or the Secret Service on Linux), keyed by API server address. ## Example ```yaml username: alice key: ~/.config/eonpath/alice-mldsa-key proxy: port: 9999 system_proxy_enabled: true auto_start_tunnel: false api_servers: - address: https://api.example.com endpoints: - name: web-server port: 8443 forwardport: 443 tunnelip: tunnel.example.com tunnelport: 2022 method: proxy - name: full-vpn tunnelip: vpn.example.com tunnelport: 51820 method: routing kex_address: vpn.example.com:9998 server_identity: "3b1e...c04a" auth: - address: https://api.example.com username: alice email: alice@example.com ``` ## Top-level settings | Key | Type | Default | Description | |-----|------|---------|-------------| | `username` | string | *(none)* | Account username used when authenticating to an API server. | | `key` | path | *(none)* | Path to the client's ML-DSA private key. | | `kex` | string | *(none)* | Optional key-exchange mode override; per-endpoint `kex` (below) takes precedence when set. | | `proxy.port` | int | *(none)* | Local port for the client's SOCKS/HTTP proxy, used by endpoints with `method: proxy`. | | `system_proxy_enabled` | bool | `false` | Whether the client configures the OS-wide HTTP/HTTPS proxy to point at its local proxy port. | | `auto_start_tunnel` | bool | `false` | Start the configured tunnel(s) automatically when the client launches. | | `api_servers` | list | *(empty)* | API servers and the endpoints registered under each — see below. | | `auth` | list | *(empty)* | Per-API-server account metadata (username, email, user ID). The actual bearer token is stored in the OS keyring, not in this file. | ## API servers Each entry under `api_servers` groups a set of endpoints under the API server that manages them: | Key | Type | Description | |-----|------|-------------| | `address` | string | Base URL of the API server, for example `https://api.example.com`. | | `endpoints` | list | Endpoints available through this API server — see below. | ## Endpoints Each endpoint describes one tunnel — either a single forwarded port (proxy method) or a full VPN connection (routing method): | Key | Type | Default | Description | |-----|------|---------|-------------| | `name` | string | *(required)* | Endpoint identifier, as registered on the API server. | | `port` | int | `0` | Local port to listen on (proxy method only). | | `forwardport` | int | `0` | Remote port to forward to on the tunnel server (proxy method only). | | `tunnelip` | string | *(required)* | Tunnel server hostname or IP address. | | `tunnelport` | int | *(required)* | Tunnel server port — the SSH port (proxy method) or WireGuard UDP port (routing method). | | `kex` | string | *(none)* | Per-endpoint key-exchange mode override. | | `method` | string | `proxy` | Connection method: `proxy` (SSH tunnel) or `routing` (WireGuard VPN). | | `kex_address` | string | *(none)* | TCP address of the routing method's key-exchange listener (for example `vpn.example.com:9998`). Required for `method: routing`. | | `server_identity` | string | *(none)* | Hex-encoded SHA-256 hash of the server's pinned ML-DSA-87 identity key. See [Server identity pinning](/eon-path/concepts/post-quantum/#server-identity-pinning). | | `server_identity_dns` | string | *(none)* | DNS TXT record name to fetch the expected identity hash from, as an alternative to `server_identity`. | | `companion_hosts` | list of strings | *(empty)* | Additional hostnames whose resolved IPv4 addresses are routed through the tunnel alongside the endpoint's main host (routing method). | Identity pinning (`server_identity` or `server_identity_dns`) is mandatory for the routing method: the client will not bring up a VPN tunnel to a server whose identity it cannot verify against one of these two sources. --- title: Desktop url: https://docs.eoncore.eu/eon-path/install/desktop/ --- ## Supported platforms The EON Path desktop client is a GUI application (Wails: Go backend, web frontend) built on the same Go core used by the mobile apps. It supports: | Platform | Architecture | |----------|--------------| | macOS | Apple Silicon (arm64) and Intel (x86_64), macOS 10.15+ | | Windows | x86_64, Windows 10+ | | Linux | x86_64 and arm64 | ## Install The desktop client is built from source per platform. Each platform has a dedicated build guide with prerequisites and packaging steps: - macOS: `cmd/eonpath_client/BUILD_MACOS.md` in the source repository - Windows: `cmd/eonpath_client/BUILD_WINDOWS.md` in the source repository - Linux: `make build-client` (see the project's `BUILDING.md`) General prerequisites are Go, Node.js/npm, and the Wails v2 CLI; see `BUILDING.md` for full per-platform setup instructions. On macOS, `make release` produces the release artifact end to end: a universal binary, Developer ID signing with the hardened runtime and a secure timestamp, a `.dmg`, and notarization with the ticket stapled into it. It needs `APPLE_DEVELOPER_ID`, `APPLE_ID`, `APPLE_APP_PASSWORD` and `APPLE_TEAM_ID` in the environment. ## Connection methods and privileges The client offers two kinds of endpoint, and the difference matters on a desktop: | Method | What it does | Privileges | |---|---|---| | **proxy** | HTTP CONNECT tunnel plus SSH port forwarding | none | | **routing** | full WireGuard tunnel with OS routes installed | root on Linux/macOS, Administrator on Windows | The client asks its own backend whether routes can actually be installed on this machine, rather than guessing from the platform name, and leaves routing endpoints out of the list when they can't. That is not cosmetic: starting a routing endpoint without the privileges gives a tunnel that establishes, reports connected and carries nothing, because installing the routes is the part that fails. A privilege-free path exists in principle on macOS (a NetworkExtension provider, which needs one-time user consent instead of root) and on Windows (a service installed once with administrator rights). Neither is implemented; the capability check is written so that it reports on the strength of the provider once one exists, without a platform check to update. ## First connection When you open the client for the first time, add the server you want to connect to by its address — a DNS name (for example `vpn.example.com`) or IP address (for example `203.0.113.10`). On the first connection to a new server, the client performs the ML-DSA-87 post-quantum key exchange and then verifies the server's identity before establishing the tunnel. This identity check is mandatory: the client will not connect to a server whose identity it cannot verify, using either a pinned identity hash configured for that endpoint or a DNS TXT record. See [Server identity pinning](/eon-path/concepts/post-quantum/#server-identity-pinning) for how this works, and [Configuration](/eon-path/configuration/) for how to set up an endpoint's identity pin or DNS TXT record. Once the server's identity is verified, the client brings up a WireGuard tunnel keyed from the post-quantum exchange, and traffic is routed through it. --- title: Installation url: https://docs.eoncore.eu/eon-aethis/install/ --- ## Overview The central platform (backend, frontend, Icinga 2, PostgreSQL, VictoriaMetrics, ClickHouse, Redis, Zeek, and Caddy) is deployed as a single Docker Compose stack. There are two compose files: - **`docker-compose.yml`** (repository root) — the development stack. Builds images from source, exposes database ports on the host for local inspection, and uses hardcoded development credentials. - **`deploy/docker-compose.prod.yml`** — the production stack. Pulls pre-built `eonaethis/*` images, keeps internal services off the host network (only Caddy's ports are published), and reads all credentials from `.env` with no built-in defaults for secrets. The Aethis Agent, Aethis Probe, and Perimeter Probe are installed separately on the hosts and networks they observe — see [Agents & Probes](/eon-aethis/agents-and-probes/). ## Prerequisites - Docker Engine with the Compose plugin (`docker compose version` works) - A host that can reach the internet for Let's Encrypt (production, if not using self-signed or a supplied certificate) - Inbound access on the ports Caddy publishes (443/80 by default) ## Development From the repository root: ```bash cp .env.example .env # set ANTHROPIC_API_KEY if you want the AI features; everything else has a # working development default docker compose up ``` This starts nine services: `caddy`, `backend`, `frontend`, `icinga2`, `postgres`, `victoriametrics`, `redis`, `zeek`, and `clickhouse`. The development compose file publishes each service's port on the host (`5432`, `8428`, `6379`, `5665`, `9000`/`8123`) for local debugging, and the `backend` and `frontend` containers bind-mount their source directories. Reach the UI at `https://localhost` (self-signed certificate by default — `CADDY_TLS=internal`). `ZEEK_INTERFACE` defaults to `eth0`; set it in `.env` to the interface you want Zeek to sniff. The `zeek` service runs with `network_mode: host` and needs `NET_RAW`/`NET_ADMIN`, so it sees the host's real interfaces rather than a Docker bridge. ## Production Production installs ship as a package built by `deploy/build-package.sh` (Docker images bundled as `images.tar` alongside `deploy/docker-compose.prod.yml`, `deploy/install.sh`, `deploy/upgrade.sh`, and `deploy/.env.example`). On the target server: ```bash tar xzf eonaethis-.tar.gz cd eonaethis ./install.sh ``` `install.sh` loads the bundled images, installs the shipped `docker-compose.yml` (only if one doesn't already exist — it never overwrites an existing install), and creates `.env` from `deploy/.env.example` if missing. It then stops and tells you to edit `.env` before starting the stack; at minimum, change: - `POSTGRES_PASSWORD` - `JWT_SECRET` (generate with `openssl rand -hex 32`) - `ICINGA2_API_PASSWORD` - `SMTP_FROM` - `CADDY_DOMAIN` `CLICKHOUSE_PASSWORD` also has no default in `deploy/docker-compose.prod.yml` and must be set manually — `install.sh`'s own printed reminder doesn't currently list it, but the stack won't come up with ClickHouse authenticated correctly until it's set. Alternatively, run `deploy/setup.sh` for an interactive wizard that prompts for domain/TLS, email, the Anthropic API key, the network interface, and the admin account, and writes `.env` for you — note that it does not currently ask about ClickHouse at all, and the `.env` it writes omits `CLICKHOUSE_DB`/`CLICKHOUSE_USER`/`CLICKHOUSE_PASSWORD` entirely, so add those to the generated `.env` by hand before starting the stack (the production compose file requires `CLICKHOUSE_PASSWORD` with no default). Once `.env` is filled in: ```bash docker compose up -d ``` See [Configuration](/eon-aethis/configuration/) for the full list of environment variables, and [Configuration — First-boot admin account](/eon-aethis/configuration/#first-boot-admin-account) for creating the initial admin user without calling the register endpoint by hand. ### Ports By default Caddy is the only service that binds host ports: | Port | Service | Purpose | |------|---------|---------| | 443 | Caddy | HTTPS UI/API (override with `CADDY_HTTPS_PORT`) | | 80 | Caddy | HTTP, used for ACME challenges (override with `CADDY_HTTP_PORT`) | Override `CADDY_HTTPS_PORT`/`CADDY_HTTP_PORT` when another reverse proxy on the host already owns 443/80. All other services (`backend`, `icinga2`, `postgres`, `victoriametrics`, `redis`, `zeek`, `clickhouse`) are reachable only on the Compose network, not the host, in the production compose file. ### TLS Caddy handles TLS for the platform, in one of three modes set via `CADDY_TLS`: 1. **Self-signed** (default): `CADDY_TLS=internal`. 2. **Let's Encrypt**: leave `CADDY_TLS` empty and set `CADDY_DOMAIN` to a publicly resolvable domain, with port 80 reachable for the ACME challenge. 3. **Your own certificate**: `CADDY_TLS="/etc/caddy/certs/fullchain.pem /etc/caddy/certs/privkey.pem"` and point `CADDY_CERTS_DIR` at the directory containing those files on the host (mounted read-only into the container at `/etc/caddy/certs`). ### Demo traffic generator The production compose file includes a `demo-probe` service behind the `demo` Compose profile, disabled by default. It writes synthetic flow events and must not be enabled on a real production install: ```bash docker compose --profile demo up -d ``` ### Verifying the install ```bash docker compose ps curl -k https://localhost/health ``` The backend's healthcheck hits its own `/health` endpoint and depends on PostgreSQL, Icinga 2, VictoriaMetrics, and ClickHouse all reporting healthy first, so `docker compose ps` showing `backend` as `healthy` implies the whole storage/monitoring layer came up correctly. ## Upgrading ```bash tar xzf eonaethis-.tar.gz cd eonaethis # same directory as the existing install ./upgrade.sh ``` `upgrade.sh` loads the new images, backs up the current `docker-compose.yml` and `.env` to a timestamped `.upgrade-backup-*` directory, adds any new env vars introduced since your last install (with defaults), shows a diff against the shipped compose file for manual merging if your live file has diverged, then runs `docker compose up -d`. Postgres migrations and the ClickHouse schema bootstrap are applied automatically by the backend on startup, and the script waits for `/health` to go green before finishing. For upgrades that also change infrastructure (not just the backend image), use `deploy/major-upgrade.sh` instead — check its output for what it does in your specific package version before running it, since it goes further than the routine `upgrade.sh` flow. --- title: Post-Quantum Cryptography url: https://docs.eoncore.eu/eon-path/concepts/post-quantum/ --- ## Why post-quantum cryptography Large-scale quantum computers, if built, would break the classical public-key cryptography (RSA, ECDH/ECDSA) that most VPNs and TLS connections rely on today. The risk is not only future: an adversary can record encrypted traffic now and decrypt it later once such a computer exists — a strategy known as "harvest now, decrypt later." That makes long-lived secrets and long-lived infrastructure worth protecting with post-quantum algorithms today, even before large-scale quantum computers exist. EON Path uses two NIST-standardized post-quantum algorithms for its VPN routing method: **ML-DSA** (FIPS 204, digital signatures) for authentication and identity, and **ML-KEM** (FIPS 203, key encapsulation) for key exchange. Both are specified at their highest standardized security level in EON Path's authentication protocol: ML-DSA-87 and ML-KEM-1024, corresponding to NIST Level 5. ## Authentication: ML-DSA-87 challenge-response Clients authenticate to the server with an ML-DSA-87 public-key challenge-response protocol, conceptually similar to SSH public-key authentication: 1. The client sends its ML-DSA-87 public key to the server. 2. The server computes a SHA-256 fingerprint of the public key and looks it up through the API to confirm the key is registered and active. 3. The server sends back a 32-byte random challenge nonce. 4. The client signs the nonce, concatenated with a protocol-specific domain separator, using its ML-DSA-87 private key. 5. The server verifies the signature against the public key the client presented in step 1. The domain separator tied to the signed message prevents a signature produced for this protocol from being replayed or reused against a different protocol that also asks a client to sign a nonce. This challenge-response exchange replaces password- or bearer-token-based authentication entirely — there is no shared secret in transit and no token that can be stolen and reused. Earlier versions of the VPN routing method authenticated clients to the API using JSON Web Tokens (JWT); as of March 2026, that has been replaced by the ML-DSA-87 challenge-response protocol described above for the VPN connection itself. ## Key exchange: ML-KEM-1024 After authentication, the server presents its own ML-DSA-87 identity key together with an ephemeral ML-KEM-1024 public key, signed with its identity key so the client can verify it came from the genuine server. The client encapsulates a shared secret against that ephemeral key and returns the resulting ciphertext. Both sides now hold the same 32-byte shared secret, from which the WireGuard pre-shared key is derived with a domain-separated SHA-256 hash. That derivation also binds the pre-shared key to a hash of the full handshake transcript, so tampering with any earlier message in the exchange produces mismatched keys on the two sides and the handshake fails closed. Because the WireGuard tunnel's pre-shared key is derived from a post-quantum key encapsulation, the tunnel's confidentiality does not depend solely on WireGuard's classical X25519 key exchange. ## Server identity pinning Before trusting a server's ML-KEM key, the client verifies the server's ML-DSA-87 identity. EON Path supports two verification methods: a **pinned hash**, where the endpoint configuration stores the expected SHA-256 hash of the server's public key directly, and a **DNS TXT record** lookup, where the client fetches the expected hash from a DNS name configured for that endpoint. Server identity keys are generated automatically the first time the server daemon starts. Identity pinning is mandatory for the routing method: a client will not proceed with a tunnel to a server whose identity it cannot verify, which closes off endpoint impersonation and on-path attacks that rely on presenting a different key than the one the client expects. --- title: Reports url: https://docs.eoncore.eu/eon-kartis/reports/ --- `kartis-scan` turns a consolidated scan document — the JSON produced by `scripts/full-scan.sh` (see [Scanning](/eon-kartis/scanning/)) — into a customer-facing Markdown report. There is no report viewer in Kartis itself; the Markdown is meant to be read directly, converted with another tool, or consumed by Eon Center, Eon Aethis, or Eon Insights. ## Generating a report ``` kartis-scan report [file] [flags] --lang report language (default "nl") -o, --out output file (default: stdout) --anonymize anonymize the input before rendering ``` The input is a file argument, or stdin when no argument (or `-`) is given: ```bash kartis-scan report scan.json # Dutch report to stdout kartis-scan report --lang en scan.json # English report scripts/full-scan.sh qgf.io | kartis-scan report -o report.md ``` ## What's in the rendered document `internal/report` renders, in order: - **Header** — domain, scan window, duration, host/service/finding counts. - **Summary** — an overall risk verdict plus a per-severity tally, and a "top actions" list of up to five items: the highest-severity findings (one action per finding source, to avoid repetition), plus a post-quantum key-exchange recommendation appended whenever no service offers hybrid PQ key exchange. - **Post-quantum readiness** — whether hybrid PQ key exchange (for example `X25519MLKEM768`) is present anywhere, and a per-TLS-service table of key exchange group, weakest certificate link, and PQC classification. - **Findings** — grouped by severity, each with its description, evidence, any associated CVE IDs, and a risk/mitigation block. Mitigation text comes from `internal/report/mitigations.go`, keyed by finding source. - **Appendix** — a table of every discovered service (host, port, protocol, service, product/version), and for each TLS service, its certificate chain: subject, issuer, signature algorithm, key details, PQC classification, expiry, and SANs. - **Methodology** — a fixed disclosure paragraph: the scan is an authorized external perimeter scan, the scanner identifies itself honestly and performs no evasion, and certificate parsing relies on the TLS 1.2 cleartext handshake (a TLS 1.3-only host reports protocol/key-exchange posture without parsed certificates). The document renders in Dutch (`nl`, the default) or English (`en`); all section headers, labels, and the methodology text are localized (`internal/report/i18n.go`). ## Output format `kartis-scan report` only produces GitHub-flavored Markdown — there is no built-in DOCX or PDF export. Converting the Markdown to another format (for example with `pandoc`, or by opening it in a word processor) is a manual step outside the CLI. ## Anonymizing scan data ``` kartis-scan anonymize [file] [flags] -o, --out output file (default: stdout) ``` Reads a consolidated scan JSON document (file argument or stdin) and writes an anonymized copy: - hostnames matching the literal `qgf.io` domain (and the bare `qgf` token, including its dash form used by Microsoft 365 routing hosts) become `acme.com` subdomains - IP addresses become RFC 5737 documentation addresses - certificate identifiers (serial numbers, fingerprints, key IDs, raw DER/PEM) are replaced with random values Every distinct real value maps to one stable anonymized value, so the anonymized document stays internally consistent (the same real hostname always becomes the same fake one, throughout the document). **Limitation:** the domain match is hardcoded to `qgf.io` (`internal/ anonymize/anonymize.go`) — there is no domain parameter. Anonymizing a scan of any other domain will **not** scrub that domain's hostnames; they pass through unchanged into the "anonymized" output, while IPs and certificate fields are still scrubbed. Today this command is effectively limited to producing sample documents from `qgf.io` (the standing own-infra reference target), not to anonymizing arbitrary customer scans. Pipe it into `report` to produce an anonymized, shareable sample document: ```bash kartis-scan anonymize scan.json | kartis-scan report -o sample.md ``` or use `report`'s own `--anonymize` flag to do both in one step: ```bash kartis-scan report --anonymize -o sample.md scan.json ``` --- title: Agents & Probes url: https://docs.eoncore.eu/eon-aethis/agents-and-probes/ --- ## Overview Three standalone Go binaries extend Eon Aethis beyond the central platform. All of them initiate outbound connections to the backend — none requires an inbound port to be opened on the host or network they run on, except the Perimeter Probe, which is deployed the other way around (see below). | Component | Runs on | Purpose | |---|---|---| | Aethis Agent | Individual monitored hosts | Local system metrics, Nagios-plugin checks, web analytics | | Aethis Probe | A network segment (one host with visibility into the segment) | Passive packet capture, NetFlow/IPFIX/sFlow ingestion, device/traffic classification | | Perimeter Probe | Externally, operated separately from a tenant's own infrastructure | On-demand external reconnaissance scans against a tenant's public-facing assets | ## Aethis Agent Installed with a one-liner that downloads the platform-matched binary from the backend itself: ```bash curl -sSL https://YOUR-SERVER/agents/install.sh | bash -s -- --server https://YOUR-SERVER ``` Useful flags: - `--insecure` — skip TLS verification (self-signed backend certs) - `--token TOKEN` — register with a pre-issued token instead of interactive pairing (`--no-pair` is implied) - `--no-pair` — install the binary and systemd unit without registering - `--config PATH` — config file path (default `/etc/eon-aethis/eon-aethis.conf`) - `--no-plugins` — skip installing `monitoring-plugins`/`nagios-plugins` via the host package manager (apt/dnf/yum/apk, whichever is present) - `--with-geoip` — download the DB-IP Lite City database for web-analytics geolocation - `--uninstall` — stop, disable, and remove the agent (leaves the config file behind) The installer detects `linux-amd64`/`linux-arm64`, requires systemd and root, installs the binary to `/usr/local/bin/aethis-agent`, links any discovered Nagios-compatible `check_*` plugins into `/usr/lib/eon-aethis/plugins`, and installs an `eon-aethis-agent.service` systemd unit. Pairing (the default flow when `--token` isn't given) requires approval in the Eon Aethis UI before the config file — and therefore the running service — is created. The generated config (YAML, at `--config`'s path): ```yaml server: url: "https://YOUR-SERVER" token: "" tls_verify: true # false when installed with --insecure checks: interval: "60s" plugins_dir: "/usr/lib/eon-aethis/plugins" metrics: cpu: true memory: true disk: true network: true processes: true ``` Additional fields available in the config schema but not written by the installer: `checks.system_plugins_dir`, `checks.plugins` (a list of `{name, path, args, interval}` for custom checks), `agent.hostname`, `agent.tags` (a map of key/value tags), and `web_analytics` (`enabled`, `access_logs: [{path, format, name}]`, `geoip_db`, `interval`) for parsing a local web server's access logs. `agent.probe_port`/`agent.probe_enabled` are legacy fields kept for backwards compatibility — active network scanning has moved to the Aethis Probe and is disabled here by default. ## Aethis Probe Passively observes a network segment: live packet capture, NetFlow/IPFIX/ sFlow listeners, ARP/DHCP analysis, and traffic classification, buffered locally and pushed to the backend. Install as root: ```bash BACKEND_URL=https://YOUR-SERVER ./install.sh ``` (`install.sh` ships in `probe/dist/` next to the `eon-aethis-probe` binary and its systemd unit; set `BIN_URL` instead of placing the binary alongside the script if you're fetching it remotely.) Set `INSECURE=1` to skip TLS verification for a self-signed backend during testing — this also passes `--insecure` to the pairing step. The installer creates a dedicated `eon-probe` system user, installs the binary to `/usr/local/bin/eon-aethis-probe`, writes `/etc/eon-aethis-probe/probe.yaml` from the template (owned `root:eon-probe`, mode `0640`), installs the `eon-aethis-probe.service` systemd unit, and runs the pairing flow — which must be approved in the backend UI — before enabling the service. Config reference (`/etc/eon-aethis-probe/probe.yaml`): ```yaml backend_url: https://aethis.example.com probe_id: "" # filled in by the pairing flow auth_token: "" # filled in by the pairing flow ca_path: "" # CA cert for a self-signed backend tls_verify: true # set false only for --insecure test setups push_interval: 60s # how often buffered events are pushed buffer_path: /var/lib/eon-aethis-probe/buffer.db buffer_max_events: 100000 buffer_max_bytes: 104857600 # 100 MB interfaces: - name: eth0 mode: host bpf: "" # optional BPF filter control_port: 9202 # local diagnostics API log_level: info ``` Events are buffered in a local SQLite database (`buffer_path`) so a temporary backend or network outage doesn't drop data; the buffer is capped by both event count and byte size, dropping oldest events past either limit. ## Perimeter Probe A multi-tenant reconnaissance service, deployed separately from any single tenant's Eon Aethis instance (typically operated centrally, e.g. one probe serving many customers). It authenticates every call with mutual TLS: an operator-controlled CA signs a server certificate for the probe and a distinct client certificate per tenant, and the probe reads the tenant ID directly from the client certificate's CN on each incoming job request — there is no separate tenant-ID field to trust. Config (`/etc/perimeter-probe/probe.yaml`): ```yaml addr: ":8443" server_cert: /etc/perimeter-probe/server.crt server_key: /etc/perimeter-probe/server.key operator_ca: /etc/perimeter-probe/operator-ca.crt client_cert: /etc/perimeter-probe/client.crt client_key: /etc/perimeter-probe/client.key ``` Run it as a container: ```bash docker run -d --name perimeter-probe \ -v /etc/perimeter-probe:/etc/perimeter-probe:ro \ -p 8443:8443 \ perimeter-probe:latest ``` Certificate CN conventions used by the operator when issuing certs (with `cfssl` or similar): - Operator CA: `CN=eon-aethis-perimeter-ca` - Probe server cert: `CN=perimeter-probe-1` - Probe client cert (for its outbound callbacks): `CN=perimeter-probe-1-client` - Tenant client cert: `CN=tenant-` (e.g. `tenant-acme`) ### Tenant onboarding 1. The probe operator generates a tenant client certificate + key. 2. The tenant creates a Perimeter Probe pairing in their Eon Aethis UI (`/admin/perimeter-probes`), supplying: the probe's base URL (`https://:8443`), the operator CA certificate (PEM), the probe's certificate fingerprint (`openssl x509 -in server.crt -outform DER | openssl dgst -sha256`, formatted as `sha256:`), and the tenant's client certificate + key (PEM). 3. The tenant runs a scan from `/discovery/perimeter` in the Eon Aethis UI. ### Certificate rotation - **Tenant cert:** re-paste the new cert in the admin UI; the old one remains recorded in the database until explicitly cleared. - **Probe server cert:** replace the files on the probe host and restart it; every tenant must then update their pinned fingerprint. - **Operator CA:** high-touch — requires issuing new tenant and probe certificates from the new CA and cycling all of them. --- title: Configuration url: https://docs.eoncore.eu/eon-kartis/configuration/ --- Every Kartis component is configured through a YAML file plus a small number of Docker Compose / `.env` variables. All three long-running commands take `-c/--config `: | Component | Default config path | Config type | |---|---|---| | `kartis-scan` (`serve`, `daemon`, `scan`) | `/etc/kartis/kartis-scan.yaml` | `Config` (`cmd/kartis-scan/config.go`) | | `kartis-discovery` | `/etc/kartis/kartis-discovery.yaml` | `DiscoveryDaemonConfig` | | `kartis-recon` | `/etc/kartis/kartis-recon.yaml` | `ReconDaemonConfig` | | `kartis-scheduler` | `/etc/kartis/kartis-scheduler.yaml` | `config` (`cmd/kartis-scheduler/main.go`) | For local development, `docker-compose.yml` mounts a `.docker.yaml` variant of each file into the container at those same paths. For `kartis-scan.yaml` the only difference is the Postgres/Redis hostnames (`postgres`/`redis` instead of a host IP or `localhost`). For `kartis-discovery.yaml` and `kartis-recon.yaml` the docker variant additionally: env-expands `db.password`/`redis.password` (`${POSTGRES_PASSWORD}`/`${REDIS_PASSWORD}`) instead of a literal value, adds `shodan_api_key`/`securitytrails_api_key` entries (discovery only), and sets a different `ai.model` (`claude-sonnet-4-6` in the docker config vs `claude-sonnet-4-20250514` on the host). Diff `configs/*.yaml` against `configs/*.docker.yaml` for the exact deltas rather than assuming hostnames are the only change. ## `kartis-scan.yaml` ```yaml name: kartis-scan listen: ":8080" database: "postgres://postgres:secret@:5432/kartis?sslmode=disable" redis: host: localhost port: 6379 channel: scan password: "" scan_profile: polite # polite | normal | aggressive — which entry in `profiles` is active profiles: polite: nmap_timing: T2 nmap_scan_type: "-sT" max_rate_pps: 50 inter_probe_delay_ms: 200 concurrent_services_per_ip: 1 concurrent_runs: 5 normal: nmap_timing: T3 nmap_scan_type: "-sT" max_rate_pps: 200 inter_probe_delay_ms: 50 concurrent_services_per_ip: 2 concurrent_runs: 10 aggressive: nmap_timing: T4 nmap_scan_type: "-sS" max_rate_pps: 1000 inter_probe_delay_ms: 0 concurrent_services_per_ip: 4 concurrent_runs: 20 ``` - `listen` defaults to `:8080` and `scan_profile` to `polite` if omitted. - `profiles` tunes nmap pacing (timing template, scan type, max packets/sec, inter-probe delay) and concurrency (services scanned per IP at once, `concurrent_runs` = the daemon's worker-pool size) — this is the same `profile` value that can be overridden per-run via `--profile` or the API's `profile` field, orthogonal to scan depth (`max_level`). - `-sT` (`polite`/`normal`) is a plain TCP connect scan; `-sS` (`aggressive`) is a raw-socket SYN scan and needs `NET_RAW` capability (the dev compose file grants it via `cap_add: ["NET_RAW"]`). ### Portal The customer-facing portal is served by `kartis-scan serve` and is **disabled by default**, so an existing deployment that only serves Eon Center keeps the surface it has. See [Portal](/eon-kartis/portal/) for what it exposes. ```yaml portal: enabled: true trusted_proxies: "127.0.0.1" # who may set X-Forwarded-For / X-Forwarded-Proto insecure: false # drop the __Host- cookie prefix and Secure flag smtp_host: localhost # delivers the login codes smtp_port: 25 mail_from: kartis@example.com ``` | Key | Meaning | |---|---| | `enabled` | Mount `/portal/...` at all. Default `false` | | `trusted_proxies` | Addresses or CIDRs whose `X-Forwarded-For` and `X-Forwarded-Proto` are believed. Empty means believe nobody — right when nothing fronts the process, wrong the moment nginx does | | `insecure` | Drops the `__Host-` cookie prefix and the `Secure` flag, for a test over plain HTTP. The portal is otherwise served behind TLS and the browser would reject the cookie | | `smtp_host` / `smtp_port` / `mail_from` | Delivery of the one-time login codes. Without a host configured the portal falls back to a capturing mailer that writes the code to the log — for a test deployment, and it says so loudly at startup | ## `kartis-scheduler.yaml` The scheduler needs the database and nothing else: it does not scan, does not talk to Redis, and serves no HTTP. ```yaml database: "postgres://postgres:secret@:5432/kartis" interval: "1m" # how often to look for due sessions batch: 50 # most runs placed per tick ``` `interval` only decides how long after a slot a run is placed, since cadences are monthly or biweekly. `batch` caps a backlog so it is worked through over several ticks rather than arriving at the customer all at once. ## `kartis-discovery.yaml` ```yaml name: kartis-discovery redis: host: localhost port: 6379 channel: discovery password: "" db: host: localhost port: 5432 database: kartis username: postgres password: secret rate_limit: whois_per_minute: 10 ctlogs_per_minute: 30 reverse_ip_per_minute: 20 asn_per_minute: 30 dns_recon_per_minute: 60 tech_fp_per_minute: 20 git_recon_per_minute: 30 ai_calls_per_minute: 10 sources: enable_whois: true enable_ctlogs: true enable_reverse_ip: true enable_asn: true enable_dns_recon: true enable_tech_fingerprint: true enable_git_recon: false enable_ai_correlation: true enable_reverse_whois: false # opt-in, needs whoisxml_api_key (paid) whoisxml_api_key: ${WHOISXML_API_KEY} enable_brand_permutation: false # opt-in — permute brand label across common TLDs enable_ai_org_expansion: false # opt-in — LLM-derived reverse-WHOIS pivot terms, needs ai.api_key ai: provider: anthropic # anthropic | openai | ollama (local) api_key: ${ANTHROPIC_API_KEY} model: claude-sonnet-4-20250514 # base_url: http://localhost:11434 # ollama only max_tokens: 4096 temperature: 0.3 ``` Each `enable_*` flag under `sources` turns a discovery source on or off independently; the `run ` recursive-expansion parameters (`--depth`, `--min-confidence`, `--max-related-per-job`, `--max-jobs`) are CLI flags, not config keys (see [Scanning](/eon-kartis/scanning/)). ## `kartis-recon.yaml` ```yaml name: kartis-recon redis: host: localhost port: 6379 channel: enum # MUST be the enum channel — discovery publishes enum jobs here password: "" db: host: localhost port: 5432 database: kartis username: postgres password: secret ai: provider: anthropic api_key: ${ANTHROPIC_API_KEY} model: claude-sonnet-4-20250514 max_tokens: 2048 temperature: 0.4 ``` `kartis-recon` warns at startup if `redis.channel` isn't `enum`, since it will otherwise never receive the jobs `kartis-discovery` publishes. ## Environment variables (`.env` / Docker Compose) `.env` (copy from `.env.example`, or generate it with `./deploy/setup.sh`) feeds variable substitution into `docker-compose.yml` and `deploy/docker-compose.prod.yml`, and is what the `${VAR}` references inside the YAML files above expand from at container startup. | Variable | Purpose | |---|---| | `POSTGRES_PASSWORD` / `POSTGRES_USER` / `POSTGRES_DB` | Postgres credentials — no default in prod compose | | `REDIS_PASSWORD` | Redis auth; empty disables auth (dev / single-host) | | `KARTIS_TAG` | image tag `docker-compose.prod.yml` pulls (default `latest`) | | `KARTIS_PORT` | host-side port mapped to the API's internal `8080` | | `KARTIS_SCAN_LISTEN` | listen address baked into `kartis-scan.yaml` by `setup.sh` | | `KARTIS_SCAN_PROFILE` | default scan profile: `polite` \| `normal` \| `aggressive` | | `KARTIS_SCAN_CONTACT` | contact URL/`mailto:` appended to the scanner's User-Agent and SSH ident comment | | `KARTIS_SCAN_EHLO` | hostname announced in SMTP EHLO/HELO (default `scanner.eon-kartis`) | | `ANTHROPIC_API_KEY` | enables the AI-assisted subdomain source and the `run --judge` review pass | | `SHODAN_API_KEY` | enables the Shodan host/port enrichment source | | `SECURITYTRAILS_API_KEY` | optional discovery enrichment source | | `WHOISXML_API_KEY` | enables reverse-WHOIS (`enable_reverse_whois`) | An empty API key simply disables the corresponding optional source — none are required for the core discovery/scan pipeline to run. ## Bearer tokens `kartis-scan serve` authenticates every `/v1/...` route except `/healthz` and `/readyz` with a DB-backed bearer token (hashes stored in the `auth_tokens` table): ```bash kartis-scan token create --name operator # prints the plaintext token once kartis-scan token list # prefixes + metadata, no plaintext kartis-scan token revoke ``` Store the printed token as a secret in whatever consumes the API (Eon Center, Eon Aethis, Eon Insights, an operator's `~/.kartis-token`, CI); it cannot be retrieved again after creation. ## Deployment lifecycle For a Linux x86 target, `make package TAG=v1.0.0` builds a deploy tarball (`deploy/build-package.sh`); on the target, `./setup.sh` interactively writes `.env` and the runtime YAMLs, `./install.sh` brings the stack up via Docker Compose, and `./upgrade.sh` applies an upgrade package on top of an existing install (backing up compose, `.env`, and YAML under `.upgrade-backup-/`). Locally, `make dev` / `make dev-down` bring the same stack up and down against the repo's own `docker-compose.yml`. --- title: Mobile url: https://docs.eoncore.eu/eon-path/install/mobile/ --- ## Status EON Path has native mobile apps for iOS and Android, built on the same Go core used by the desktop client, so authentication, key exchange, and tunnel behavior are consistent across all clients. The apps are built and feature-complete, with the same ML-DSA-87 authentication, WireGuard tunneling, and server identity pinning described in [Concepts](/eon-path/concepts/). Public store availability is pending — this page will be updated with links once the apps are published. ## iOS **App Store: coming soon.** ## Android **Google Play: coming soon.** ## In the meantime If you need to connect from a desktop or laptop today, see the [Desktop](desktop/) install guide. --- title: Tunneling url: https://docs.eoncore.eu/eon-path/concepts/tunneling/ --- ## WireGuard as the transport Once a client has authenticated to a server (see [Post-Quantum Cryptography](../post-quantum/)), EON Path's routing method brings up a standard **WireGuard** tunnel to carry traffic. WireGuard was chosen because it is a small, well-audited VPN protocol with a compact wire format, low connection latency, and consistently good throughput compared to older VPN protocols such as OpenVPN or IPsec. Packets on the tunnel are encrypted with ChaCha20-Poly1305, and the tunnel itself behaves like any other WireGuard interface: it can be inspected with standard WireGuard tooling, and its keepalive, MTU, and allowed-IP settings follow WireGuard's usual configuration model. ## Keying the tunnel from an authenticated exchange WireGuard's own key exchange is a classical X25519 Diffie-Hellman handshake. EON Path does not replace that handshake, but it strengthens the tunnel's key material by deriving a **pre-shared key (PSK)** from the post-quantum key exchange performed during authentication, and configuring WireGuard to mix that PSK into its own key derivation. Concretely: 1. The client and server complete the ML-DSA-87 challenge-response authentication and ML-KEM-1024 key encapsulation described in [Post-Quantum Cryptography](../post-quantum/). 2. Both sides derive the same shared secret from the ML-KEM-1024 decapsulation and hash it, with a domain separator, into a 256-bit WireGuard PSK. 3. The client and server then exchange their WireGuard (X25519) public keys over the same authenticated channel. 4. The server sends the client its tunnel configuration (addresses, subnet, MTU, keepalive) and both sides configure a WireGuard peer using the exchanged public keys and the post-quantum-derived PSK. 5. The WireGuard tunnel comes up using that peer configuration. Because the PSK step happens over a channel that has already authenticated both parties with post-quantum signatures, an attacker who could eventually break WireGuard's classical X25519 exchange would still need the post-quantum-derived PSK to decrypt tunnel traffic — the tunnel's confidentiality does not rest on a single classical assumption. ## Multi-server clients Desktop and mobile clients can be configured with more than one server endpoint and switch between them, rather than being tied to a single hard-coded server. At the protocol level, EON Path's WireGuard configuration model is built around a list of peer configurations rather than a single fixed peer, which is what makes it possible for a client to hold tunnel configuration for more than one server at a time. ## What this doesn't change EON Path's post-quantum handshake governs how a tunnel gets set up and keyed — it does not change WireGuard's packet format or the way encrypted traffic looks on the wire. This keeps EON Path's routing method compatible with standard WireGuard operational practices (monitoring, `wg show`, keepalive tuning) once a tunnel is established. --- title: Address Translation (SIIT) url: https://docs.eoncore.eu/eon-path/concepts/siit/ --- ## What SIIT does SIIT (Stateless IP/ICMP Translation, [RFC 7915](https://www.rfc-editor.org/rfc/rfc7915)) is a mechanism for translating packets between IPv4 and IPv6 without keeping per-connection state, and is part of the broader 464XLAT approach for giving IPv4 connectivity over an IPv6-only network. EON Path's routing method uses SIIT inside the WireGuard tunnel: the client's TUN device sends and receives plain IPv4 packets, and SIIT translates them to and from IPv6 immediately before and after they cross the tunnel. SIIT embeds an IPv4 address in the low 32 bits of an IPv6 address behind a configured `/96` prefix — for example, an internal address `10.0.0.2` behind the prefix `2001:db8:4464::/96` becomes `2001:db8:4464::a00:2`. Translating a packet means rewriting its IP header (and recalculating checksums, since IPv4 and IPv6 compute them differently) between these two representations; no connection table is required, which is what makes the translation stateless and cheap to run on every packet. ## Where it sits in the data path ``` Application (IPv4) │ ▼ TUN device │ ▼ SIIT: IPv4 → IPv6 │ ▼ WireGuard tunnel (encrypted) │ ▼ SIIT: IPv6 → IPv4 (server side) │ ▼ NAT → public internet ``` On the way back, the server-side SIIT translator converts IPv6 responses back to IPv4 before they re-enter the tunnel toward the client, so the translation is invisible to the application at either end — it always sees plain IPv4. ## When it applies SIIT translation is configured per active tunnel: it applies to a client's single session with a given server, using that session's `/96` prefix and address mapping. This keeps the translation logic simple and stateless — a tunnel either has SIIT enabled with a specific prefix and address pair, or it doesn't need SIIT because both ends already agree on an IPv4 or IPv6 addressing scheme directly. ## Why it exists SIIT solves a specific deployment problem: a server's internal network may be IPv6-only, while the client applications that need to reach it only speak IPv4. Rather than requiring every client application to be updated to support IPv6, or running a stateful NAT64 gateway with connection tracking overhead, EON Path's SIIT translation lets an IPv4-only client transparently reach an IPv6-only server network, and, combined with NAT on the server side, lets that traffic continue on to the public internet using the server's own IPv4 or IPv6 connectivity. This is particularly useful for infrastructure that has migrated internally to IPv6 but still needs to serve IPv4-speaking clients and applications. --- title: Configuration url: https://docs.eoncore.eu/eon-aethis/configuration/ --- ## Central platform (`.env`) All central-platform configuration is read from environment variables, set in the `.env` file next to `docker-compose.yml` (development) or `deploy/docker-compose.prod.yml` (production). Defaults below are the Go backend's built-in defaults — most are declared on the `Config` struct in `go-backend/internal/config/config.go`, but a few aren't: `CLICKHOUSE_URL`, `CLICKHOUSE_READ_TIMEOUT`, and `PROBE_EVENT_RETENTION_DAYS` are read directly via `os.Getenv` in `internal/service/clickhouse/client.go`; `GEOIP_DB_DIR` is read via `os.Getenv` in `cmd/server/main.go`; and `GEOIP_HOST_DIR` isn't read by the Go binary at all — it's a Compose-only variable used to pick the host directory bind-mounted into the container. So `config.go` isn't a single source of truth for all of these. The production `deploy/.env.example` deliberately omits defaults for secrets so an install can't accidentally go live with a known password. ### Required in production | Variable | Description | |---|---| | `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` | Application database credentials | | `JWT_SECRET` | Session token signing secret. The backend refuses to start if unset or left as `change-me-in-production` | | `ICINGA2_API_PASSWORD` | Must match the password baked into the Icinga 2 container's `api-users.conf` | | `CLICKHOUSE_PASSWORD` | ClickHouse credential — do not leave at the dev default in production | | `CADDY_DOMAIN` | Public domain/IP(s) Caddy serves; also the ACME domain in Let's Encrypt mode | ### Backend (Go API) | Variable | Default | Description | |---|---|---| | `DATABASE_URL` | `postgres://eon:eon@localhost:5432/eon_aethis` | PostgreSQL connection string. `postgresql+asyncpg://` prefixes are normalized to `postgres://` automatically | | `REDIS_URL` | `redis://localhost:6379/0` | Redis connection string | | `ICINGA2_API_URL` | `https://localhost:5665` | Icinga 2 REST API base URL | | `ICINGA2_API_USER` | `root` | Icinga 2 API user | | `ICINGA2_API_PASSWORD` | `icinga` | Icinga 2 API password | | `VICTORIAMETRICS_URL` | `http://localhost:8428` | VictoriaMetrics query endpoint | | `CLICKHOUSE_URL` | — | ClickHouse DSN, e.g. `clickhouse://user:pass@clickhouse:9000/db` | | `CLICKHOUSE_READ_TIMEOUT` | `30s` | Inter-packet read timeout for ClickHouse queries (Go duration), read via `os.Getenv` in `internal/service/clickhouse/client.go`. `deploy/docker-compose.prod.yml` and `deploy/upgrade.sh` set/enforce a `300s` floor in production specifically so the one-time `network_flows` backfill migration doesn't time out on upgrade (symptom otherwise: backend crashloops with `read tcp ...: i/o timeout`); a dev deployment that never sets this var gets the 30s in-binary default | | `PROBE_EVENT_RETENTION_DAYS` | `30` | Retention for probe telemetry in ClickHouse | | `ANTHROPIC_API_KEY` | — | Claude API key. Leave unset to disable the AI features | | `JWT_SECRET` | — (required) | Session token signing secret | | `JWT_ALGORITHM` | `HS256` | JWT signing algorithm | | `JWT_EXPIRATION` | `86400` | Token lifetime in seconds | | `PORT` | `8000` | Backend listen port | | `TLS_CERT`, `TLS_KEY` | — | Optional: terminate TLS directly on the backend rather than at Caddy (e.g. agent-only edge sites). Generate dev certs with `cd go-backend && go run ./cmd/gencert --host localhost,127.0.0.1` | | `CORS_ORIGINS` | `http://localhost:5173,https://localhost` | Allowed CORS origins | | `ZEEK_LOG_DIR` | — | Path to Zeek's rotated log directory (mounted read-only from the `zeek-logs` volume, typically `/var/log/zeek/current`) | | `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_FROM` | port `587`, from `noreply@eonaethis.local` | Outbound mail for notifications. In Compose, `SMTP_HOST` defaults to `host.docker.internal` (dev) or `172.17.0.1` (prod) to reach a local MTA on the host | | `GEO_HOME_LAT`, `GEO_HOME_LON` | `52.3702`, `4.8952` | Reference coordinates for geo-based features | | `AETHIS_PUBLIC_BASE_URL` | `https://localhost:1443` | This instance's externally reachable URL, used to build callback URLs sent to the Perimeter Probe | | `BOOTSTRAP_ADMIN_EMAIL`, `BOOTSTRAP_ADMIN_PASSWORD`, `BOOTSTRAP_ADMIN_NAME` | — | See [First-boot admin account](#first-boot-admin-account) below | | `RATE_LIMIT_AUTH_PER_MIN`, `RATE_LIMIT_API_PER_MIN` | `0` (built-in defaults: 30/min auth, 600/min API) | Per-IP, per-minute rate limits. Set explicitly to tighten or disable for testing | | `AETHIS_ENV` | `development` | Set to `production` to make the backend refuse to start with known dev-default credentials (Icinga API password `icinga`, Postgres `eon:eon`, ClickHouse RO password `eon_aethis_ro_dev`) instead of just warning | | `GEOIP_DB_DIR` / `GEOIP_HOST_DIR` | `/var/lib/eon/geoip` / `./data/geoip` | Container path / host path for optional MaxMind GeoLite2 databases (`GeoLite2-ASN.mmdb`, `GeoLite2-Country.mmdb`). If the host path doesn't exist, enrichment falls back to category-only | ### First-boot admin account If `BOOTSTRAP_ADMIN_EMAIL` and `BOOTSTRAP_ADMIN_PASSWORD` are both set and the `users` table is empty, the backend creates a single admin user with those values on startup. On every subsequent boot the check is a no-op (the table is no longer empty), so it's safe to leave the variables set permanently. `deploy/install.sh` also offers an interactive prompt for the same purpose — use whichever fits: env vars for zero-interaction/CI installs, the prompt for manual ones. ### Zeek | Variable | Default | Description | |---|---|---| | `ZEEK_INTERFACE` | `eth0` | Network interface Zeek sniffs | | `ZEEK_PROFILE` | `prod` | `prod` = minimal logging; `dev` = verbose full-protocol capture | | `ZEEK_LOG_RETENTION_HOURS` | `24` (prod) / `72` (dev) | Rotated logs older than this are deleted | | `ZEEK_MAX_DISK_MB` | `20480` | Hard disk ceiling for `/var/log/zeek`; the janitor force-prunes the oldest rotated files once exceeded, as a last-resort guard against disk-fill | ### Caddy / TLS | Variable | Default | Description | |---|---|---| | `CADDY_DOMAIN` | `localhost` | Domain(s)/IP(s) Caddy serves, comma-separated | | `CADDY_DEFAULT_SNI` | value of `CADDY_DOMAIN` | Fallback SNI for connections without a Server Name (e.g. IP-based access) | | `CADDY_TLS` | `internal` | `internal` = self-signed; empty = ACME (Let's Encrypt/ZeroSSL, needs port 80 open); or `" "` for a supplied certificate | | `CADDY_CERTS_DIR` | — | Host directory mounted read-only at `/etc/caddy/certs` when supplying your own certificate | | `CADDY_HTTPS_PORT`, `CADDY_HTTP_PORT` | `443`, `80` | Host ports Caddy binds (production compose only) — change when another reverse proxy already owns 443/80 on the host | ### Postgres / ClickHouse container credentials | Variable | Description | |---|---| | `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` | Passed straight through to the `postgres:16-alpine` image | | `CLICKHOUSE_DB`, `CLICKHOUSE_USER`, `CLICKHOUSE_PASSWORD` | Passed straight through to the `clickhouse/clickhouse-server` image; the backend's `CLICKHOUSE_URL` must reference the same values | ## Aethis Agent (`eon-aethis.conf`, YAML) See [Agents & Probes — Aethis Agent](/eon-aethis/agents-and-probes/#aethis-agent) for the full field reference. Defaults if a field is omitted: `server.tls_verify: true`, `checks.interval: 60s`, `checks.plugins_dir: /usr/lib/eon-aethis/plugins`, `checks.system_plugins_dir: /usr/lib/nagios/plugins`, and all `metrics.*` flags `true`. ## Aethis Probe (`probe.yaml`) See [Agents & Probes — Aethis Probe](/eon-aethis/agents-and-probes/#aethis-probe) for the full field reference: `backend_url`, `probe_id`, `auth_token`, `ca_path`, `tls_verify`, `push_interval`, `buffer_path`, `buffer_max_events`, `buffer_max_bytes`, `interfaces` (list of `{name, mode, bpf}`), `control_port`, `log_level`. ## Perimeter Probe (`probe.yaml`) See [Agents & Probes — Perimeter Probe](/eon-aethis/agents-and-probes/#perimeter-probe) for the full field reference: `addr`, `server_cert`, `server_key`, `operator_ca`, `client_cert`, `client_key`.