Skip to content

Server Configuration

The server reads its configuration from a TOML file (default: server.toml).

Terminal window
rvpn-server -c /etc/rvpn/server.toml

[server]
bind_address = "0.0.0.0:443"
tls_cert_file = "/etc/letsencrypt/live/your-domain.com/fullchain.pem"
tls_key_file = "/etc/letsencrypt/live/your-domain.com/privkey.pem"
identity_key_file = "/etc/rvpn/server_identity.key"

[server]
bind_address = "0.0.0.0:443"
tls_cert_file = "/etc/letsencrypt/live/example.com/fullchain.pem"
tls_key_file = "/etc/letsencrypt/live/example.com/privkey.pem"
identity_key_file = "/etc/rvpn/server_identity.key"
websocket_path = "/api/v1/ws"
http_port = 80
[server.network]
nat_enabled = true
dhcp_range = "10.200.0.0/24"
dns_servers = ["1.1.1.1", "8.8.8.8"]
[server.rate_limit]
max_connections_per_ip = 500
max_handshakes_per_minute = 2000
[server.tun]
enabled = true
tun_ip = "10.200.0.1/24"
mtu = 1420
interface_name = "tun0"

Run these commands once when setting up a new server:

Terminal window
cd /etc/rvpn
# Generate the server's long-term identity key
rvpn-server keygen
# Generate the prekey bundle (used by clients to authenticate)
rvpn-server prekey-bundle

This produces:

  • server_identity.key — the server’s Ed25519 identity key pair. Back this up and keep it private.
  • prekey-bundle.json — the public prekey bundle. Distribute this to clients.
  • prekey-bundle.private.json — private signed prekey material. Keep this private.

Prekey rotation is not automated in the current server — regenerate the prekey bundle manually with rvpn-server prekey-bundle when you want to rotate. Clients do not need updated prekey bundles when the server’s one-time prekeys are consumed; they only need a fresh bundle if the server’s identity key changes.

Existing clients pin the server’s identity key on first connect (see Server Identity Pinning). To rotate the identity key without breaking those clients, sign the new bundle with the old identity so the client can verify the chain:

Terminal window
cd /etc/rvpn
# Preserve the old identity long enough to sign the rotation
mv server_identity.key old_identity.key
# Generate the new one
rvpn-server keygen --output server_identity.key
# Publish a v2 bundle signed by old_identity.key
rvpn-server prekey-bundle \
--identity server_identity.key \
--output prekey-bundle.json \
--rotate-from old_identity.key \
--from-version 1

Both --rotate-from and --from-version are required together. Ship the new prekey-bundle.json to clients as usual — already-pinned clients update silently, new clients pin the new key on first use. Archive old_identity.key after publishing so you can chain a future rotation from it if you ever need to.


Let’s Encrypt certificates expire every 90 days. Certbot installs a renewal cron job automatically.

After renewal, restart rVPN to pick up the new certificate:

Terminal window
sudo systemctl restart rvpn-server

To automate this, add a deploy hook:

/etc/letsencrypt/renewal-hooks/deploy/rvpn-reload.sh
#!/bin/bash
systemctl restart rvpn-server
Terminal window
chmod +x /etc/letsencrypt/renewal-hooks/deploy/rvpn-reload.sh

The server automatically returns a hardcoded nginx-style 404 page for any non-WebSocket request. From the outside this looks like an unconfigured web server rather than a VPN endpoint. For a fully custom decoy site, terminate TLS at a reverse proxy and forward only /api/* to rVPN — see Reverse Proxy Setup for Caddy, nginx, and HAProxy recipes with a real decoy website served at the root.


If you’re running behind Caddy, nginx, or HAProxy, omit the cert/key files and bind to a local port:

[server]
bind_address = "127.0.0.1:8443"
websocket_path = "/api/v1/ws"
# No tls_cert_file / tls_key_file — TLS terminated at the proxy

See Reverse Proxy Setup for complete Caddy, nginx, and HAProxy configurations, including decoy site setup and multi-server load balancing.


The server rate-limits incoming connections per client IP address to prevent abuse.

[server.rate_limit]
max_connections_per_ip = 500
max_handshakes_per_minute = 2000
  • max_connections_per_ip — Maximum concurrent connections from a single IP address.
  • max_handshakes_per_minute — Maximum new handshake attempts per IP per minute.

[!WARNING] In legacy (non-multiplexed) SOCKS5 mode, the protocol opens one WebSocket connection per TCP flow. A modern browser page can easily open 20–50 concurrent connections (HTML, CSS, JS, images, API calls). If your rate limits are too low, connections will be silently dropped and pages will fail to load.

For servers serving legacy SOCKS5 clients, set max_connections_per_ip to at least 500 and max_handshakes_per_minute to at least 2000. Adjust upward based on your expected concurrent connections per user.

[!NOTE] Multiplexed SOCKS5 mode (single WebSocket, multiple flows) is the default in modern clients. It eliminates rate limit concerns entirely since all flows share one connection. Set multiplex = true in your client config to use it.

When a connection is rate-limited, the server logs Rate limited: <IP> at debug level and closes the connection without an error response.


See the Server Configuration Reference for a complete list of every available setting.