Security
Rotate API keys, encrypt the connection to the proxy, limit who can connect and where they can go, and read the logs.
rustle ships with the safe choices turned on. Most of this page is about the few settings that open it up on purpose, and about the habits that keep it closed. Every setting named here is listed, with its default, in Configuration.
Protect the proxy with a key
[auth] api_keys lists the keys the proxy accepts. A client sends one as the proxy password; the user name is ignored. Several keys can be valid at once, and any of them is enough.
With the list empty, there is no authentication: anyone who can reach the port can use your proxy, including to reach other machines through it. rustle check warns about it. Generate keys with rustle init, or with openssl rand -hex 32.
Rotate a key without downtime
Because several keys can be valid at once, a key changes in three steps and no client is ever refused.
- Add the new key next to the old one, then validate and reload:
[auth] api_keys = ["old-key", "new-key"]rustle check rustle reload - Switch every client to the new key.
- Remove the old key from the list and reload again.
A reload applies to new connections straight away. A connection that is already open keeps the keys it was accepted with until it closes, so a revoked key can still work on an idle connection until its idle timeout ends.
Do step 3 only once no client uses the old key. A client that keeps sending a wrong key is banned (see below), and the ban covers its whole IP address.
Encrypt the connection to the proxy
The plain port and the SOCKS5 port send the key in clear text: HTTP Basic credentials are only base64, and SOCKS5 passwords are not encrypted at all. Do not expose either to the Internet. Two good ways to use them:
- Keep them on a trusted network. Bind them to
127.0.0.1, or to a VPN address, and reach them through a VPN or an SSH tunnel. - Use the HTTPS port. It encrypts everything between the client and the proxy, and is the one to expose over the Internet.
What the proxy then sends to the destination is a separate matter: an http:// URL goes to its server in clear text through any port, while the content of a CONNECT tunnel stays end to end when the protocol inside it is TLS.
Turn on the HTTPS port
[tls]
listen = "0.0.0.0:8443"
cert = "/etc/rustle/fullchain.pem"
key = "/etc/rustle/privkey.pem"
Both files are PEM. rustle check loads them and fails if the certificate and the key do not match. The port speaks TLS 1.2 and 1.3, and HTTP/2 or HTTP/1.1 depending on what the client asks for (tls.http2 turns HTTP/2 off). It uses one certificate for all names.
To try it without a certificate authority, a self-signed certificate works if it names the host in subjectAltName, which modern clients require:
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -days 365 \
-subj "/CN=proxy.example.com" -addext "subjectAltName=DNS:proxy.example.com" \
-keyout key.pem -out cert.pem
Clients then need to trust it, for curl with --proxy-cacert cert.pem.
Renew the certificate
rustle reads the certificate at startup and at every reload. It does not watch the files. After renewing, run rustle reload (for example from your certificate tool’s renewal hook). New connections get the new certificate; open ones finish with the old one. If the new certificate and key do not match, the reload fails and the running one stays.
Limit who can connect
The client allowlist
[auth] allow_clients takes IP addresses and CIDR ranges. When it is not empty, every other address is closed as soon as it connects, before a single byte is read, on every listener. The client gets no answer, only a closed connection.
[auth]
allow_clients = ["203.0.113.7", "10.8.0.0/24"]
It looks at the address the connection comes from. Behind a NAT or another proxy, that is the address of the one in front of you.
A firewall in front
The allowlist is not a firewall: the TCP handshake still completes before rustle closes the connection. Let the firewall do the first filtering and keep both. What rustle listens on, by default:
- the plain port,
0.0.0.0:8080; - the health endpoint, on
127.0.0.1only, which needs no rule; - the HTTPS port and the SOCKS5 port, only if you set their address.
Open only the ports you use. With ufw, to let one address reach the HTTPS port (standard ufw syntax, nothing specific to rustle):
sudo ufw allow from 203.0.113.7 to any port 8443 proto tcp
SOCKS5 UDP (UDP ASSOCIATE) opens UDP sockets on ports chosen by the system for each association, so a firewall that only allows the TCP port of the SOCKS5 listener breaks UDP through the proxy while TCP keeps working. Allow it only if you need UDP.
Bans
A client that sends a wrong key too many times in a short time is banned. max_failures, failure_window and ban_duration in [auth] set the numbers; max_failures = 0 turns bans off. What to know:
- A request that carries no credentials (a browser’s first attempt, before it asks you for the key) does not count; only a wrong key does.
- The ban is by IP address and covers every listener. A banned client is closed at accept, silently, even with the right key. Clients behind one NAT share a ban.
- There is no command to list or clear bans, and they are kept in memory only. A restart clears them. So does a reload that changes
max_failures,failure_windoworban_duration; a reload that leaves them alone keeps the bans.
Limit where clients can go
By default, rustle refuses to connect to private and local addresses ([filter] block_private): loopback, private ranges, link-local (which includes the cloud metadata address 169.254.169.254), the CGNAT range, multicast and the IPv6 equivalents. Without this, a client could use the proxy to reach services inside your network. A refused request gets a 403, or a “not allowed” answer over SOCKS5.
The check is made on the address the name resolves to, not on the name: a host name that points to 127.0.0.1 is refused too, and every address a name resolves to is checked, which also stops DNS rebinding.
- Allowed ports.
allowed_portslimits the destination ports, for example[80, 443]. It applies to plain requests,CONNECT, SOCKS5 and SOCKS5 UDP. Empty means every port. - Extra denied ranges.
denyblocks more CIDR ranges. - Reaching your own network. The only way to allow private destinations is to turn
block_privateoff, for everything at once. Do it only on a proxy that your trusted clients alone can reach, and keep the sensitive ranges closed withdeny:[filter] block_private = false deny = ["169.254.0.0/16", "127.0.0.0/8"]
All of these apply on a reload.
Keep the other doors closed
- Health endpoint.
server.admin_listenanswersGET /healthwithok, without authentication and ignoringallow_clients. It listens on loopback by default; leave it there, or set it to""to turn it off. - Control socket.
rustle reloadtalks to the proxy through a Unix socket readable by its owner only. - Limits.
[limits]and[timeouts]cap connections, request head sizes and idle time, which is what holds slow clients (slowloris) in check.
Read the logs
The access log tells you who was refused and why. At the errors level it records wrong keys, bans, refusals by the destination filter, and a one-line summary per second of connections closed at accept by the allowlist or a ban. At all it records every request and every tunnel.
By default no access log is written: log.output is file and log.file is empty. Set a path, or use log.output = "stdout":
[log]
access = "errors"
output = "file"
file = "/var/log/rustle/access.log"
A line looks like client=203.0.113.9 via=http1 kind=connect target=example.com:443 status=403 reason=port. To rotate, rename the file and send SIGHUP (systemctl reload rustle does it); rustle reload does not reopen the file. These [log] settings need a restart to change.