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
This commit is contained in:
212
docs/server-config.md
Normal file
212
docs/server-config.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user