Rotating Tor HTTP proxy in Kubernetes

Every proxy article eventually ends with someone asking: "but can I rotate IPs automatically?" The answer running on this server is zhaow-de/rotating-tor-http-proxy — a container that runs N parallel Tor circuits behind a single HAProxy frontend. Each request goes to a different circuit, so you get a different exit IP without doing anything special on the client side.

What runs inside the container

The image bundles:

  • N pairs of Privoxy + Tor (configurable via TOR_INSTANCES)
  • HAProxy in front, round-robining across all Privoxy instances
  • A cron job that sends NEWNYM to each Tor control socket every TOR_REBUILD_INTERVAL seconds

Each Tor process maintains its own circuit independently. When HAProxy routes a request to instance #3, that request exits via instance #3's current exit node. The next request might go to instance #1 with a completely different exit node.

The stats UI at :30444 shows per-backend session counts and traffic, which is what the proxy page pulls to show the "Tor stats" table.

Kubernetes setup

Everything lives in a dedicated namespace tor. The deployment is simple — one pod, Recreate strategy (no benefit to rolling update here), emptyDir for /var/lib/tor so Tor can write its state files:

yamlapiVersion: apps/v1
kind: Deployment
metadata:
  name: tor-proxy
  namespace: tor
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: tor-proxy
  template:
    metadata:
      labels:
        app: tor-proxy
    spec:
      containers:
      - name: tor-proxy
        image: zhaowde/rotating-tor-http-proxy:latest
        env:
        - name: TOR_INSTANCES
          value: "5"
        - name: TOR_REBUILD_INTERVAL
          value: "1800"
        ports:
        - containerPort: 3128   # HAProxy HTTP
        - containerPort: 4444   # HAProxy stats
        volumeMounts:
        - name: tor-data
          mountPath: /var/lib/tor
      volumes:
      - name: tor-data
        emptyDir: {}

Two NodePort services expose it outside the cluster: 30128 for the proxy and 30444 for the HAProxy stats UI.

No auth

There is intentionally no authentication on the Tor proxy. The exit IPs are Tor exit nodes — they're already public, already logged by every major CDN, and rotating every 30 minutes anyway. Adding a password would only be security theater. The NodePort is exposed on the VPS public IP, so technically anyone could use it, but practically nobody knows the port.

If this were a shared infrastructure, I'd add a Proxy-Authorization header check in HAProxy. For a personal setup with 5 circuits and a 1800s rebuild interval, it doesn't matter.

Latency

Tor is slow. A direct curl https://ifconfig.me from the VPS takes ~50ms. Through the Tor proxy it's typically 2–8 seconds depending on which exit node is in the circuit. This is fine for tasks where you need a different IP, not for streaming.

bash# verify rotation — each call should return a different IP
for i in 1 2 3 4 5; do
  curl -s --proxy http://91.184.248.13:30128 https://ifconfig.me
  echo
done

On a good day you'll see 4–5 distinct IPs across 5 requests.

HAProxy stats integration

The stats endpoint at :30444/haproxy?stats;csv;norefresh returns a CSV with one row per backend. The proxy page's /stats/tor endpoint fetches this, parses the CSV, and returns JSON:

json{
  "ok": true,
  "backends": [
    {"name": "tor1", "status": "UP", "scur": 0, "stot": 12, "bin": 4096, "bout": 8192},
    ...
  ],
  "total_stot": 60,
  "total_bin": 20480,
  "total_bout": 40960
}

The frontend polls this every 60 seconds and renders a table showing each circuit's status and cumulative traffic.

Circuit rebuild vs NEWNYM

There are two different things that "rotate" circuits here:

  1. NEWNYM signal (every 10 minutes by default) — tells Tor to build a new circuit for future streams. Fast, but the old circuit may persist for open connections.
  2. Full container restart (TOR_REBUILD_INTERVAL=1800) — kills all Tor processes and restarts them. This guarantees fresh circuits but causes ~60 seconds of downtime while Tor builds initial circuits and joins the network.

The TOR_REBUILD_INTERVAL in the Kubernetes deployment is 1800 (30 minutes). For most use cases NEWNYM is sufficient; the full rebuild is more of a "just in case the circuit is stuck" mechanism.

Setup script

The k0s-setup/21-setup-tor-proxy.sh script applies the manifest, waits for the pod to be Running, then sleeps 60 seconds for Tor to build initial circuits before running a 3-request validation. The sleep is important — the proxy accepts connections immediately but returns errors until Tor is actually connected to the network.

bashssh root@server 'bash -s' < k0s-setup/21-setup-tor-proxy.sh

It prints the exit IP for each of the 3 test requests. If you see 3 different IPs, the rotation is working.


Troubleshooting

Proxy returns connection errors immediately after startup:
Tor needs 30–90 seconds to bootstrap circuits after container start. The liveness probe should be delayed accordingly:

yamllivenessProbe:
  httpGet:
    path: /haproxy?stats;csv;norefresh
    port: 4444
  initialDelaySeconds: 90
  periodSeconds: 30

All 5 requests return the same IP:
HAProxy is probably not round-robining correctly or all circuits share the same exit. Try increasing TOR_INSTANCES to 10 and verify stats at port 30444.

Very slow or 100% failure:
Some countries block Tor. Check if your VPS provider is in a region with restrictions. Use tor --verify-config inside the pod to check for errors.

bashkubectl exec -n tor deploy/tor-proxy -- tor --verify-config
kubectl logs -n tor deploy/tor-proxy | grep -i error

Resource limits

Tor is CPU-light but memory-hungry. Each circuit uses ~50MB RAM. With 5 instances:

yamlresources:
  requests:
    memory: "256Mi"
    cpu: "50m"
  limits:
    memory: "512Mi"
    cpu: "200m"

On a 2GB VPS, 5 instances is a safe number. For 10 instances consider bumping to 1GB limit.


Summary

Component Purpose
N Tor processes Each holds independent circuit
HAProxy Round-robin across all Tor circuits
NEWNYM signal Refresh circuits without restart
TOR_REBUILD_INTERVAL Force full circuit rebuild
NodePort 30128 HTTP proxy access
NodePort 30444 HAProxy stats UI