- Go to https://console.hetzner.cloud
- Create a new project → Add Server:
- Location: Helsinki (hel1)
- Image: Ubuntu 24.04 LTS
- Type: CX23 (2 vCPU, 4GB RAM, 40GB SSD) ~€4.35/mo
- Networking: Enable IPv4 + IPv6
- SSH Key: Add your public key during creation
- Note your server's:
- IPv4 address →
<SERVER_IP> - IPv6 address →
<SERVER_IPV6> - (No root password needed — Hetzner uses SSH key from creation)
- IPv4 address →
ssh root@<SERVER_IP>
# Or with explicit key:
ssh -i ~/.ssh/your_key root@<SERVER_IP># Update system
apt update && apt upgrade -y
# Install essentials
apt install -y curl wget git ufw fail2ban unattended-upgrades
# Install Docker
curl -fsSL https://get.docker.com | sh
systemctl enable docker
systemctl start docker
# Install Docker Compose (v2 plugin)
apt install -y docker-compose-plugin
# Verify
docker --version
docker compose version# On the server: disable password auth
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshdufw --force reset
ufw default deny incoming
ufw default allow outgoing
# Core
ufw allow 22/tcp # SSH
ufw allow 80/tcp # HTTP redirect (sing-box) + SS2022 fallback
ufw allow 443/tcp # HAProxy → Reality, XHTTP, CDN-WS, XrayHTTP
# Hysteria2
ufw allow 8443/udp # Hy2 standard (routed through HAProxy on 443 for TCP)
# Finalmask (xray-core sidecar)
ufw allow 10053/udp # Finalmask XDNS (mKCP headerType=dns)
ufw allow 10054/udp # Finalmask XICMP (mKCP headerType=utp)
# Hy2 Salamander + UDP Hop
ufw allow 20000:50000/udp # Salamander obfuscated + port hopping range
# ShadowTLS v3 (Helsinki + Scaleway only)
ufw allow 10443/tcp # ShadowTLS v3 wrapping SS2022
# NaiveProxy (Helsinki + Oracle + Scaleway)
ufw allow 2087/tcp # Caddy forwardproxy (Chrome TLS stack)
# Cloak (Scaleway only)
ufw allow 2053/tcp # Cloak TLS camouflage
# DNS tunnel
ufw allow 53/udp # DNS (for dnstt/slipstream)
ufw allow 53/tcp # DNS (for dnstt/slipstream)
ufw --force enable
ufw status numberedNote: Not all ports are needed on every server. ShadowTLS (10443) is HEL+SCW only, Cloak (2053) is SCW only, NaiveProxy (2087) is HEL+ORC+SCW. When deploying a new server, only open ports for protocols you actually deploy.
# Already installed above, just enable
systemctl enable fail2ban
systemctl start fail2ban
# Verify
fail2ban-client status# Configure unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
# Select YES when promptedcd /opt
git clone https://github.com/aleskxyz/reality-ezpz.git
cd reality-ezpz
# Run interactive setup — configures VLESS Reality + WARP outbound
bash reality-ezpz.shDuring setup, configure:
| Setting | Recommended Value | Notes |
|---|---|---|
| SNI | www.google.com |
Site to impersonate. Pick a major site Iran won't block |
| Transport | tcp |
Most reliable |
| WARP | ON |
Routes outbound through Cloudflare — hides VPS IP from destinations |
| UUID | Auto-generated | Save as <UUID> — shared across VLESS + Hysteria2 |
| Reality keypair | Auto-generated | Save private key as <REALITY_PRIVATE_KEY>, public key as <REALITY_PUBLIC_KEY> |
| Reality short-id | Auto-generated | Save as <REALITY_SHORT_ID> |
After setup, save the generated values:
# View generated config
cat /opt/reality-ezpz/config
# Show user config + VLESS URI
bash reality-ezpz.sh --show-user=RealityEZPZNote: WARP is integrated as a WireGuard endpoint inside sing-box (
"final": "warp"routing). No separate warp-yg installation needed.
reality-ezpz doesn't support Hysteria2 natively alongside Reality TLS. We manually inject it.
mkdir -p /opt/reality-ezpz/certs
openssl ecparam -genkey -name prime256v1 | \
openssl ec -out /opt/reality-ezpz/certs/hy2.key
openssl req -new -x509 -days 3650 \
-key /opt/reality-ezpz/certs/hy2.key \
-out /opt/reality-ezpz/certs/hy2.crt \
-subj '/CN=www.google.com'
# Get cert hash for client config — save as <HY2_CERT_SHA256>
openssl x509 -in /opt/reality-ezpz/certs/hy2.crt -noout -fingerprint -sha256 \
| sed 's/.*=//;s/://g' | tr 'A-F' 'a-f'Add this inbound to the "inbounds" array in /opt/reality-ezpz/engine.conf:
{
"type": "hysteria2",
"tag": "hy2-in",
"listen": "::",
"listen_port": 8444,
"users": [
{
"name": "RealityEZPZ",
"password": "<UUID>"
}
],
"tls": {
"enabled": true,
"certificate_path": "/etc/sing-box/hy2.crt",
"key_path": "/etc/sing-box/hy2.key"
}
}Also add "hy2-in" to every "inbound" array in the "route" → "rules" section (copy the existing "in" rules).
Add to ports:
- "8443:8444/udp" # Hysteria2 (host 8443 → container 8444)Add to volumes:
- ./certs/hy2.crt:/etc/sing-box/hy2.crt:ro
- ./certs/hy2.key:/etc/sing-box/hy2.key:rocd /opt/reality-ezpz
docker compose down && docker compose up -d
# Verify Hysteria2 is listening
ss -ulnp | grep 8443WARNING: Running
bash reality-ezpz.shwill OVERWRITEengine.confand remove the Hysteria2 injection. Always back up first:cp engine.conf engine.conf.bak cp docker-compose.yml docker-compose.yml.bak
See dnstt-setup.md for full instructions.
# Disable systemd-resolved (needs port 53)
systemctl stop systemd-resolved && systemctl disable systemd-resolved
cat > /etc/resolv.conf << 'EOF'
nameserver 8.8.8.8
nameserver 8.8.4.4
EOF
# Download and install dnstm
curl -Lo /usr/local/bin/dnstm \
https://github.com/net2share/dnstm/releases/download/v0.6.7/dnstm-linux-amd64
chmod +x /usr/local/bin/dnstm
dnstm install --mode multi
# Add tunnels
dnstm tunnel add --name slip-socks --type slipstream --domain t.<DOMAIN> --port 5310 --backend socks
dnstm tunnel add --name dnstt-socks --type dnstt --domain t2.<DOMAIN> --port 5311 --backend socks
dnstm tunnel add --name slip-ssh --type slipstream --domain s2.<DOMAIN> --port 5312 --backend ssh
# Start DNS router
dnstm router start
# Save crypto material for clients
dnstm tunnel list # → save <DNSTT_PUBKEY>, <SLIP_SOCKS_FP>, <SLIP_SSH_FP># Create restricted tunnel user (installed by dnstm)
sshtun-user create <SSH_TUNNEL_USER> --insecure-password "<SSH_TUNNEL_PASS>"
# Apply SSH hardening
sshtun-user configure
# Verify
id <SSH_TUNNEL_USER> # → should show sshtunnel-password group
getent passwd <SSH_TUNNEL_USER> # → should show /usr/sbin/nologin shellPre-install dns-tun-lb for instant scaling to multiple servers later.
# Install Go
curl -Lo /tmp/go.tar.gz https://go.dev/dl/go1.24.1.linux-amd64.tar.gz
rm -rf /usr/local/go && tar -C /usr/local -xzf /tmp/go.tar.gz
export PATH=$PATH:/usr/local/go/bin
# Build
mkdir -p /opt/dns-tun-lb/src
cd /opt/dns-tun-lb/src
git clone https://github.com/aleskxyz/dns-tun-lb.git .
go build -o /usr/local/bin/dns-tun-lb .
# Create standby config
cat > /opt/dns-tun-lb/lb.yaml << 'EOF'
global:
listen_address: "127.0.0.1:5354"
default_dns_behavior:
mode: "forward"
forward_resolver: "9.9.9.9:53"
protocols:
dnstt:
pools:
- name: "dnstt-socks"
domain_suffix: "t2.<DOMAIN>"
backends:
- id: "local-dnstt"
address: "127.0.0.1:5311"
logging:
level: "info"
EOF
# Create systemd service
cat > /etc/systemd/system/dns-tun-lb.service << 'EOF'
[Unit]
Description=DNS Tunnel Load Balancer
After=network.target
[Service]
ExecStart=/usr/local/bin/dns-tun-lb -config /opt/dns-tun-lb/lb.yaml
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable dns-tun-lb
systemctl start dns-tun-lb
# Create activation script
cat > /opt/dns-tun-lb/activate-lb.sh << 'SCRIPT'
#!/bin/bash
case "$1" in
activate)
sed -i 's/127.0.0.1:5354/0.0.0.0:53/' /opt/dns-tun-lb/lb.yaml
systemctl stop dnstm-dnsrouter
systemctl restart dns-tun-lb
echo "dns-tun-lb activated on port 53"
;;
deactivate)
sed -i 's/0.0.0.0:53/127.0.0.1:5354/' /opt/dns-tun-lb/lb.yaml
systemctl restart dns-tun-lb
systemctl start dnstm-dnsrouter
echo "dns-tun-lb deactivated to standby"
;;
status)
echo "=== dns-tun-lb ==="
systemctl is-active dns-tun-lb
grep listen_address /opt/dns-tun-lb/lb.yaml
echo "=== dnstm-dnsrouter ==="
systemctl is-active dnstm-dnsrouter
;;
*)
echo "Usage: $0 {activate|deactivate|status}"
;;
esac
SCRIPT
chmod +x /opt/dns-tun-lb/activate-lb.shmkdir -p /opt/vpn-backups
cat > /opt/vpn-backups/backup-vpn-v2.sh << 'SCRIPT'
#!/bin/bash
BACKUP_DIR="/opt/vpn-backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/vpn-backup-$TIMESTAMP.tar.gz"
mkdir -p "$BACKUP_DIR"
echo "[$(date)] Starting VPN backup..."
tar -czf "$BACKUP_FILE" \
/opt/reality-ezpz/config \
/opt/reality-ezpz/engine.conf \
/opt/reality-ezpz/docker-compose.yml \
/opt/reality-ezpz/certs/ \
/etc/dnstm/ \
/opt/dns-tun-lb/lb.yaml \
/root/.ssh/authorized_keys \
2>/dev/null
echo "[$(date)] Backup complete: $BACKUP_FILE ($(du -h "$BACKUP_FILE" | cut -f1))"
# Keep last 7 backups
ls -t "$BACKUP_DIR"/vpn-backup-*.tar.gz 2>/dev/null | tail -n +8 | xargs rm -f 2>/dev/null
echo "[$(date)] Total backups: $(ls "$BACKUP_DIR"/vpn-backup-*.tar.gz 2>/dev/null | wc -l)"
SCRIPT
chmod +x /opt/vpn-backups/backup-vpn-v2.sh
# Schedule daily at 2 AM UTC
(crontab -l 2>/dev/null; echo "0 2 * * * /opt/vpn-backups/backup-vpn-v2.sh >> /var/log/vpn-backup.log 2>&1") | crontab -
# Run first backup now
bash /opt/vpn-backups/backup-vpn-v2.sh# All services running
systemctl is-active reality-ezpz-engine-1 || docker compose -f /opt/reality-ezpz/docker-compose.yml ps
systemctl is-active dnstm-dnsrouter dnstm-tunnel-slip-socks dnstm-tunnel-dnstt-socks \
dnstm-tunnel-slip-ssh dnstm-microsocks dns-tun-lb
# All ports listening (core + v4.0 protocols)
ss -tulnp | grep -E '(443|8443|53|10053|10054|10443|2087|2053|5310|5311|5312|39190|5354)'
# DNS tunnels
dnstm tunnel list
dnstm router status
# Firewall
ufw status numbered
# Test from LOCAL machine
curl -I https://<SERVER_IP>:443 # Should get HTTP 301/302
curl -x socks5://127.0.0.1:39190 ifconfig.me # (from server) Should show server IP
# Security checks
grep PasswordAuthentication /etc/ssh/sshd_config # Should be 'no'
systemctl is-active fail2ban # Should be 'active'
systemctl is-active unattended-upgrades # Should be 'active'