Files
konduit-public/docs/server-config.md
E. Kaparulin c61664ca70 docs: correct the self-hosted server path and document server.toml
Audit of the public repo against the actual server and konduit-ctl source.
The happy path was documented; the failure modes were not.

- server.toml had no reference at all. Add docs/server-config.md covering
  every key, [tls], the control API, and key handling.
- The mantra could not recover the identity key on its own:
  derive_from_passphrase needs the [kdf] salt, which is 32 random bytes
  generated once at bootstrap. bootstrap says so in the file it generates
  (core/src/config.rs:403); the quickstart claimed the opposite. Fixed, with
  a backup step added before it is needed rather than after.
- konduit-server never appeared in a download step, yet
  konduit-server.service expects /opt/konduit/konduit-server - a path no
  instruction created. Added the install section and moved setcap after the
  copy, since capabilities do not survive every cross-filesystem copy and a
  binary that silently lost cap_net_bind_service fails at bind time.
- Hot config reload was a headline feature with no documented mechanism. It
  is a control API on 127.0.0.1:3001, and add-client only edits the file on
  disk, so a new peer is not live until something reloads it. Documented,
  including that /reload does not disconnect revoked peers but /sync does.
- [tls] native termination was entirely undocumented; the key must be PKCS#8.
- Dropped a README claim about a konduit-admin-ui unit that does not exist,
  and fixed a systemd copy path that only resolved if this repo had been
  cloned to /opt/konduit.

#k-ops/#1/#211
2026-10-10 22:43:47 +03:00

7.8 KiB

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

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

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

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

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 <peer_id>.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.
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:

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

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:

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

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

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