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
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:3001exposesPOST /sync, which writes an attacker-supplied file straight toserver.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.