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
213 lines
7.8 KiB
Markdown
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.
|