Securely expose your home network services to the internet through a VPS using Rathole (encrypted TCP tunnel) and Caddy (reverse proxy with automatic HTTPS).
Problem: You want to access home services (Home Assistant, Jellyfin, etc.) from anywhere, but:
- Your ISP uses CGNAT or blocks incoming connections
- You don't want to expose your home IP address
- You need reliable HTTPS with valid certificates
Solution: This setup creates an encrypted tunnel from your home network to a cheap VPS, which handles all incoming traffic.
flowchart TB
subgraph Internet
USER["User: https://app.example.com"]
end
subgraph VPS["VPS (Public Server)"]
CADDY["Caddy (Docker)<br/>Terminates HTTPS :443/:80<br/>Routes to Rathole ports"]
RATHOLE_SERVER["Rathole Server (systemd)<br/>Listens on :2333<br/>Binds services to localhost"]
CADDY --> RATHOLE_SERVER
end
subgraph HomeNetwork["Home Network"]
RATHOLE_CLIENT["Rathole Client (systemd)<br/>Connects outbound to VPS<br/>Forwards to local services"]
subgraph Services["Local Services"]
WEBAPP["Web App<br/>192.168.1.100:3000"]
HA["Home Assistant<br/>192.168.1.50:8123"]
JELLYFIN["Jellyfin<br/>192.168.1.60:8096"]
end
RATHOLE_CLIENT --> WEBAPP
RATHOLE_CLIENT --> HA
RATHOLE_CLIENT --> JELLYFIN
end
USER --> CADDY
RATHOLE_SERVER <--->|"Encrypted Tunnel :2333<br/>Noise Protocol"| RATHOLE_CLIENT
Automate config management with these CLI tools:
| App | Description | Install |
|---|---|---|
| RCM-Go (Recommended) | Single binary with interactive TUI | go install github.com/AhmedAburady/rcm-go/cmd/rcm@latest |
| RCM (Python) | Original Python version | uv tool install "rcm @ git+ssh://git@github.com/AhmedAburady/rcm.git" |
Both apps use your Caddyfile as the source of truth - edit one file and run rcm sync to deploy configs to both machines automatically.
flowchart TD
subgraph repo["rathole-tunnel/"]
subgraph server_dir["server/"]
ST[server.toml]
SS[rathole-server.service]
end
subgraph client_dir["client/"]
CT[client.toml]
CS[rathole-client.service]
end
subgraph caddy_dir["caddy/"]
DC[compose.yml]
CF[Caddyfile]
CERTS[certs]
end
README[README.md]
end
- VPS: Any Linux server with a public IP (e.g., Hetzner, DigitalOcean, Vultr)
- Home machine: Any always-on Linux device on your home network
- Domain: Point your domain's DNS to your VPS IP
- Docker: Required on VPS for Caddy (or install Caddy directly)
Run this on both VPS and home machine:
wget https://github.com/rathole-org/rathole/releases/download/v0.5.0/rathole-x86_64-unknown-linux-gnu.zip
unzip rathole-x86_64-unknown-linux-gnu.zip
sudo mv rathole /usr/local/bin/
sudo chmod +x /usr/local/bin/rathole
rathole --versionRun once on either machine:
# Generate keypair
rathole --genkey
# Output:
# Private Key: xxxxx (use on server)
# Public Key: yyyyy (use on client)
# Generate token
openssl rand -base64 32Save these values - you'll need them for both configs.
# Clone this repo
git clone https://github.com/YOUR_USERNAME/rathole-tunnel.git
cd rathole-tunnel
# Edit server config with your keys
nano server/server.toml
# Replace: YOUR_TOKEN_HERE, YOUR_PRIVATE_KEY_HERE
# Configure your services (bind_addr ports)
# Install config into /etc/rathole
sudo mkdir -p /etc/rathole
sudo cp server/server.toml /etc/rathole/server.toml
sudo chmod 644 /etc/rathole/server.toml
# Install and start the systemd service
sudo cp server/rathole-server.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now rathole-server
# Verify it's running
sudo systemctl status rathole-servercd caddy
# Edit Caddyfile with your domains
nano Caddyfile
# If using Cloudflare origin certs, add them to certs/
mkdir -p certs
# Copy your cert.pem and key.pem files here
# Start Caddy
docker compose up -d
# Check logs
docker compose logs -fsudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow from YOUR_HOME_IP to any port 2333
sudo ufw enable# Clone this repo
git clone https://github.com/YOUR_USERNAME/rathole-tunnel.git
cd rathole-tunnel
# Edit client config
nano client/client.toml
# Replace: YOUR_VPS_IP, YOUR_TOKEN_HERE, YOUR_SERVER_PUBLIC_KEY_HERE
# Configure your services (local_addr = your service IPs)
# Install config into /etc/rathole
sudo mkdir -p /etc/rathole
sudo cp client/client.toml /etc/rathole/client.toml
sudo chmod 644 /etc/rathole/client.toml
# Install and start the systemd service
sudo cp client/rathole-client.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now rathole-client
# Verify connection
sudo journalctl -u rathole-client -f
# Should show: "Control channel established"Use RCM-Go or RCM (Python) to automate the entire process. Just edit your Caddyfile and run one command:
rcm syncRCM parses your Caddyfile, generates both server.toml and client.toml, deploys them via SSH, and restarts all services automatically.
-
Server (
server/server.toml):[server.services.myapp] bind_addr = "127.0.0.1:5003"
-
Client (
client/client.toml):[client.services.myapp] local_addr = "192.168.1.X:PORT"
Service names must match exactly!
-
Caddy (
caddy/Caddyfile):myapp.example.com { reverse_proxy 127.0.0.1:5003 }
-
Re-deploy configs and restart services:
Configs are copied into
/etc/rathole/(not symlinked), so re-copy after editing.# VPS sudo cp server/server.toml /etc/rathole/server.toml sudo chmod 644 /etc/rathole/server.toml sudo systemctl restart rathole-server cd ~/rathole-tunnel/caddy && docker compose restart # Home sudo cp client/client.toml /etc/rathole/client.toml sudo chmod 644 /etc/rathole/client.toml sudo systemctl restart rathole-client
Best for: DNS pointing directly to VPS (Cloudflare "DNS only" / grey cloud)
app.example.com {
reverse_proxy 127.0.0.1:5000
}Caddy automatically obtains and renews certificates.
Best for: Traffic proxied through Cloudflare (orange cloud)
- Generate origin cert in Cloudflare dashboard (SSL/TLS > Origin Server)
- Save as
caddy/certs/domain-cert.pemandcaddy/certs/domain-key.pem - Configure Caddyfile:
app.example.com { tls /certs/domain-cert.pem /certs/domain-key.pem reverse_proxy 127.0.0.1:5000 }
Add to configuration.yaml:
http:
use_x_forwarded_for: true
trusted_proxies:
- 127.0.0.1
- 192.168.1.0/24 # Your home network range# === VPS ===
sudo systemctl status rathole-server
sudo systemctl restart rathole-server
sudo journalctl -u rathole-server -f
cd ~/rathole-tunnel/caddy
docker compose logs -f
docker compose restart
docker compose pull && docker compose up -d # Update Caddy
# === Home ===
sudo systemctl status rathole-client
sudo systemctl restart rathole-client
sudo journalctl -u rathole-client -f| Problem | Solution |
|---|---|
| Client won't connect | Check VPS firewall allows port 2333 from your home IP |
| "Token mismatch" | Ensure default_token is identical in both configs |
| "Invalid public key" | Verify remote_public_key in client matches server's public key |
| 502 Bad Gateway | Service not running, wrong local_addr, or service name mismatch |
| Certificate errors (Let's Encrypt) | Ensure DNS points to VPS, port 80 is open |
| Certificate errors (Cloudflare) | Check cert files exist and paths are correct |
- Encryption: All tunnel traffic uses Noise Protocol (ChaCha20-Poly1305 + X25519)
- Firewall: Restrict port 2333 to your home IP only
- Secrets: Never commit real tokens/keys - the example configs use placeholders
- Cloudflare certs: Valid for 15 years, no auto-renewal needed
- Let's Encrypt: Caddy handles renewal automatically
Both rathole-server.service and rathole-client.service ship with a sandbox profile that drops privileges and shrinks the kernel attack surface:
| Directive | Effect |
|---|---|
DynamicUser=yes |
Runs under a transient, unprivileged user (no static account, no home) |
NoNewPrivileges=yes |
Process can never gain new privileges (e.g. via setuid) |
ProtectSystem=strict |
Entire filesystem mounted read-only except a few runtime paths |
PrivateTmp=yes |
Private /tmp and /var/tmp, isolated from other processes |
ProtectKernelTunables=yes |
Blocks writes to /proc/sys, /sys, etc. |
ProtectKernelModules=yes |
Denies loading/unloading kernel modules |
ProtectControlGroups=yes |
/sys/fs/cgroup is read-only |
RestrictAddressFamilies=AF_INET AF_INET6 |
Only IPv4/IPv6 sockets allowed |
RestrictNamespaces=yes |
Blocks creation of new namespaces |
LockPersonality=yes |
Prevents changing execution domain |
CapabilityBoundingSet= |
Drops all Linux capabilities |
SystemCallFilter=@system-service |
Allows only the standard service syscall set |
SystemCallErrorNumber=EPERM |
Denied syscalls return EPERM instead of killing the process |
After editing a unit, reload and restart:
sudo systemctl daemon-reload
sudo systemctl restart rathole-server # or rathole-clientInspect the applied sandbox at any time with systemd-analyze security rathole-server.
The server.toml / client.toml configs live in /etc/rathole/ (root-owned). Because the services run as a DynamicUser, the files are world-readable (644) so the transient user can read them at startup.
⚠️ Note:644means any local user can read these files, which contain your tunnel token and (on the server) the private key. On a single-admin VPS this is acceptable. If untrusted users have shell access, tighten to640with a dedicated group and setGroup=/SupplementaryGroups=on the unit (incompatible withDynamicUser), or use systemdLoadCredential=to inject secrets instead.
MIT