# Server Configuration Reference `server.toml` is written by `konduit-ctl bootstrap` and appended to by `konduit-ctl add-client`. You rarely need to hand-edit it, but when you do, this is every key the server reads. ## Layout ```toml [server] listen_addr = "0.0.0.0" listen_port = 443 public_addr = "vpn.example.com" keepalive_interval = 30 identity_key = "<64 hex chars>" private_key = "<64 hex chars>" [kdf] version = 2 algorithm = "argon2id" m_cost = 262144 t_cost = 3 p_cost = 4 salt_hex = "<64 hex chars>" [network] tun_name = "tun0" server_ip = "10.10.0.1" netmask = "255.255.255.0" mtu = 1300 dns = ["1.1.1.1"] [[peers]] peer_id = "my-laptop" public_key = "<64 hex chars>" ip_address = "10.10.0.10" allowed_routes = ["0.0.0.0/0"] enabled = true ``` ## `[server]` | Key | Type | Default | Notes | |---|---|---|---| | `listen_addr` | string | `0.0.0.0` | Address the VPN listener binds. | | `listen_port` | u16 | `443` | Port the VPN listener binds. Set by the `-l` flag on `bootstrap`. | | `public_addr` | string | — | Hostname or IP written into every client config. Set by the `-l` flag on `bootstrap`. | | `public_port` | u16 | = `listen_port` | Port written into client configs when it differs from `listen_port`. Set by `--public-port`. | | `keepalive_interval` | u64 | `30` | Seconds between keepalives on an idle tunnel. | | `identity_key` | hex or `file://` | — | **The secret.** Ed25519 private identity, 32 bytes hex. See [Key handling](#key-handling). | | `private_key` | hex or `file://` | — | Legacy X25519 key, unused by the current handshake. Left in place for compatibility. | | `fallbacks` | list of IPs | `[]` | Extra server addresses a client may fall back to. | `public_addr` is the value clients dial. Bootstrap with the **hostname** you will actually serve, not a throwaway IP — the string is baked into every client config, and changing it later means re-issuing every client file. Both key fields accept either inline hex or a `file://` URI. A relative `file://./server.key` resolves against the config file's directory; an absolute path is used as-is. Anything that isn't `file://` is treated as literal hex. ## `[kdf]` | Key | Type | Notes | |---|---|---| | `version` | int | KDF format version. `2` is current. | | `algorithm` | string | `argon2id`. | | `m_cost` | int | Memory in KiB. `262144` = 256 MiB. | | `t_cost` | int | Iterations. | | `p_cost` | int | Parallelism. | | `salt_hex` | string | 32 random bytes, hex. **Unique per bootstrap.** | **The server does not read this section at runtime** — it parses and ignores it. It exists so the identity key can be re-derived from your mantra later. Treat it as part of the backup, not as tuning you can adjust: see [Key handling](#key-handling). ## `[network]` | Key | Type | Default | Notes | |---|---|---|---| | `tun_name` | string | `tun0` | Interface name. **Set this explicitly** — see below. | | `server_ip` | IP | `10.10.0.1` | Server's address on the VPN. Its `/24` defines the client pool. | | `netmask` | IP | `255.255.255.0` | VPN subnet mask. | | `mtu` | int | `1300` | TUN MTU. | | `dns` | list of IPs | `[]` | DNS servers pushed to clients. | `tun_name` defaults to `tun0`, and the TUN layer falls back to the first free `tunN` if that name is taken. On a host that also runs WireGuard or OpenVPN the actual name then shifts underneath you, and any firewall rule keyed to it stops matching. Pin it: ```toml [network] tun_name = "konduit0" ``` Your masquerade rule must match whatever subnet `server_ip` implies. The default is `10.10.0.1` with a `/24` mask, so clients land in `10.10.0.0/24`: ```bash sudo iptables -t nat -I POSTROUTING 1 -s 10.10.0.0/24 -o $IFACE -j MASQUERADE ``` If you change `server_ip`, change that rule to match. `add-client` allocates addresses by scanning existing peers in the same `/24`, starting at `.10` and failing once `.254` is passed. ## `[[peers]]` | Key | Type | Default | Notes | |---|---|---|---| | `peer_id` | string | — | Unique label. Becomes `.client.toml`. | | `public_key` | hex | — | Client's Ed25519 public key. | | `ip_address` | IP | — | Assigned inside the VPN subnet. | | `enabled` | bool | `true` | `false` stops the peer being accepted. Takes effect on new connections; see [Reloading](#reloading). | | `allowed_routes` | list of CIDR | `["0.0.0.0/0"]` | What this client is permitted to route. | | `tenant` | string | — | Optional grouping label. Not written by `add-client`. | Routing is server-side policy. A client asking for a route it isn't listed for does not get it, so `allowed_routes` is the knob for splitting access — set `["10.0.0.0/8"]` for a LAN-only peer and `["0.0.0.0/0"]` for full tunnel. ## `[tls]` Optional. When present the server terminates TLS itself instead of expecting a TCP-passthrough proxy in front: ```toml [tls] cert_file = "/etc/konduit/fullchain.pem" key_file = "/etc/konduit/privkey-pkcs8.pem" ``` | Key | Type | Notes | |---|---|---| | `cert_file` | path | PEM certificate, full chain. | | `key_file` | path | Private key — **must be PKCS#8**. | A PKCS#1 key (`BEGIN RSA PRIVATE KEY`) is rejected at startup. Convert it: ```bash sudo openssl pkcs8 -topk8 -inform PEM -outform PEM \ -in privkey.pem -out privkey-pkcs8.pem ``` Omit `[tls]` entirely if a proxy terminates the connection for you. ## Reloading The server exposes an unauthenticated control API on `127.0.0.1:3001`. It is bound to loopback and assumes it stays there. | Method | Path | Effect | |---|---|---| | `GET` | `/status` | Uptime and basic server state. | | `GET` | `/peers` | Currently connected peers. | | `GET` | `/config` | Current configuration. | | `POST` | `/reload` | Re-read `server.toml` from disk. | | `POST` | `/sync` | Replace `server.toml` with the request body, then reload. | `reload` swaps the parsed configuration. It does **not** disconnect anyone: tunnels already up keep running on the old config until they end, so revoking a peer with `enabled = false` stops new connections but leaves an existing session up. Restarting the service is the blunt way to drop everything. `sync` is the one that tears down peers it no longer wants — it disconnects any peer that is disabled or absent from the uploaded config. From the CLI: ```bash ./konduit-ctl reload # POST /reload ./konduit-ctl status # GET /status ./konduit-ctl peers # GET /peers ./konduit-ctl push # upload local server.toml via POST /sync ./konduit-ctl add-client my-laptop --reload # add, then reload in one step ``` `add-client` without `--reload` only edits `server.toml` on disk. The running server keeps its old peer list until something reloads it. > **Never bind the control API to a public address.** `--control-addr > 0.0.0.0:3001` exposes `POST /sync`, which writes an attacker-supplied file > straight to `server.toml` — that file holds the server's private identity key > and every client's public key. Reach it over an SSH tunnel if you need remote > administration. ## Key handling `server.toml` contains the server's **private identity key** in plaintext. Anyone who reads the file can impersonate the server to every client. `salt_hex` in `[kdf]` is 32 random bytes generated at bootstrap time and never derived from anything you chose. Losing `server.toml` means losing the salt, and the mantra alone **cannot** regenerate the key — there is nothing to seed it with. The mantra is one half of the key material; `server.toml` is the other. ```bash # Back up both, or accept that recovery is impossible. sudo install -m 600 server.toml /root/konduit-server.toml.bak ``` Confirm your backup parses before you need it: ```bash ./konduit-server --config /root/konduit-server.toml.bak --validate ``` `konduit-ctl gen-root` prints the public identity and the full KDF parameter set in one block, which is the other thing worth storing off-box.