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

213 lines
7.8 KiB
Markdown

# 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 `<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](#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.