There is a kind of technical contribution almost nobody sees, yet it sustains free internet access for
thousands of people every day: donating a slice of a server to run a Tor bridge. Unlike an ordinary
relay, a bridge is an entry point that is not publicly listed, existing specifically to let users under
censorship reach the Tor network without their local ISP noticing.
This article documents the full process: the technical grounding of what a bridge is and why it matters,
the criteria for choosing a VPS, deploying the stack on Docker (obfs4 bridge, monitoring and alerting) and
hardening the host. It closes with the main failure point found in practice, the interaction between
Docker and UFW, which silently invalidates much of a VPS firewall configuration.
Concepts
Node types in the Tor network
The Tor network is sustained by volunteers operating three types of node, with distinct purposes. The
middle relay forwards already-encrypted traffic between other nodes, being public and openly listed
in the consensus. The exit relay is the last hop, where traffic leaves the network toward the open
internet using the operator's IP, the most needed type, but also the one most exposed to abuse complaints
and legal implications. The bridge, in turn, is an entry point deliberately absent from the public
consensus.
The distinction is crucial: ordinary relays appear on a public list that any government can download
and block wholesale in a single operation. A bridge is not on that list. A censor cannot block what it
cannot enumerate, and that is precisely where its function lies.
obfs4 and Pluggable Transports
Keeping the bridge address secret is not enough: Tor traffic has a recognizable signature in its TLS
handshake, which lets Deep Packet Inspection (DPI) systems identify and block the connection even
without knowing the destination IP. Pluggable Transports solve this by wrapping the traffic in an
obfuscation layer.
obfs4 is the standard transport for bridges. It performs a handshake authenticated by a key
that only a client holding the bridge line knows, turning the entire flow into seemingly random noise -
no fixed headers, no recognizable packet size pattern, and no response to probes from anyone without the
key. That last property is what provides resistance to active probing: a censor connecting to the
obfs4 port without the correct key receives nothing that confirms a Tor bridge is there.
Distribution and bridge life cycle
A newly created bridge is not handed directly to users. It is registered with the
Bridge Authority and distributed by Rdsys, the Tor Project's distribution system, which
releases it through controlled channels, BridgeDB over HTTPS, e-mail and Telegram, or bundled with Tor
Browser. Distribution is fragmented on purpose: no single channel hands out the complete set of bridges,
precisely to prevent a censor from enumerating them all at once.
Bridges are not permanent. Countries with large-scale active probing capability manage to identify and
block bridges over time, even with obfuscation. This creates a continuous replacement dynamic: the network
depends on a steady flow of new bridges, still absent from block lists, replacing those being burned. The
value of a new bridge lies exactly at that point.
Operational relevance
The numbers support the argument. Russia and Iran are consistently the countries with the highest
volume of bridge connections, tens of thousands of daily users combined, with spikes multiplying by
dozens of times during periods of heavier repression. In Iran, direct connections to Tor have been blocked
for years; for virtually every Iranian user, a bridge is not an alternative, it is the only entry path
that exists. The network operates with roughly 1,800 to 2,200 active bridges at any given moment, a modest
figure against the demand.
Why not run it on a home network
The temptation to save money and run the bridge on a home computer exists, but it runs into technical
limitations that a VPS eliminates immediately.
CGNAT
A large share of residential connections, especially mobile and part of cable connections, operates
behind Carrier-Grade NAT, meaning the subscriber does not even hold a public IP of their own.
Without a public IP, there is no way for the Bridge Authority to reach the ORPort to validate the bridge's
reachability. This is an architectural blocker, not a configuration issue.
Dynamic IP
Even without CGNAT, residential ISPs rotate the subscriber's IP periodically. Each change breaks the
bridge's continuity, forcing it to rebuild reputation from scratch, and Rdsys distribution mechanisms
favor bridges with stable uptime and a consistent address.
Shared bandwidth
The bridge competes for upload with all household usage: streaming, video calls, games. On a VPS,
bandwidth is dedicated and predictable.
Terms of service
Many residential ISPs contractually prohibit running servers, even when they do not actively enforce it.
Residential IP exposure
Although the bridge does not expose the operator to end users, that is precisely the point of the design -
it remains exposed to any internet scanning system, including those of countries performing active
probing. It is preferable that this address belongs to a disposable VPS rather than to the operator's home.
Choosing the VPS
The Tor Project's official requirements for a bridge are modest: a minimum of 1 Mbit/s of bandwidth (with
an ideal range between 10 and 16 Mbit/s), a monthly traffic minimum of 100 GB, 2 TB or more recommended -
and no relevant CPU or RAM demands. That makes low-cost entry plans viable.
Hetzner (CX22)
2 vCPU, 4 GB RAM, 40 GB NVMe and 20 TB of monthly traffic, in the USD 4.50 to 5 range per month. It offers
the best ratio of included bandwidth to price among traditional providers.
OVHcloud (entry line)
2 GB RAM with unmetered bandwidth, around USD 4 to 4.50 per month. The absence of a traffic cap removes
any concern about usage growth.
Contabo (Cloud VPS)
4 vCPU, 8 GB RAM and 100 GB SSD for roughly USD 5 per month on long-term promotional contracts. Generous
resources, with historically more irregular network performance.
BuyVM
Unmetered bandwidth at 1 Gbps for around USD 3.50 per month. A smaller provider, but traditionally
tolerant of privacy infrastructure.
The deciding factor is neither CPU nor RAM, but high or unmetered bandwidth. A healthy, well
distributed bridge tends to receive growing traffic as it gains reputation on the network, so a capped
plan tends to become a bottleneck sooner than expected. It is also prudent to check the provider's policy
regarding Tor nodes: bridges and middle relays are widely accepted, while exit relays are usually
prohibited.
Actual resource usage
After months of operation, the measured consumption of the full stack, obfs4 bridge, Uptime Kuma monitoring and the daily summary script, stayed considerably below what any entry plan offers:
RAM 150-400 MB total, against 4-8 GB available
Disk 1-2 GB for the whole stack, growing slowly through logs
CPU average well below one core, with rare spikes under heavy traffic
In practice, even the cheapest plan on the list above keeps a wide resource margin, which allows the same VPS to take on other roles, bug bounty and continuous reconnaissance tooling, for instance, with no noticeable conflict with the bridge.
Stack architecture
The stack runs entirely on Docker and consists of three services, on top of a previously hardened host:
Service layer: obfs4-bridge, built from the official Tor Project image
Observability layer: uptime-kuma, monitoring the port and alerting via Telegram
Reporting layer: tor-bridge-report.sh, querying the Tor Metrics Onionoo API
Host layer: UFW, ufw-docker, Fail2Ban, unattended-upgrades and kernel hardening via sysctl
Hands-on
Host preparation
With the instance provisioned, update the system and install the base dependencies.
apt update && apt upgrade -y
apt install -y ufw fail2ban unattended-upgrades curl jq
Next, install Docker Engine from the official script and enable the service.
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
Kernel hardening
Before exposing any service, apply network protections at the kernel level. Create the file
/etc/sysctl.d/99-hardening.conf with the content below.
# Network protections
net.ipv4.conf.all.accept_redirects = 0 # Blocks ICMP redirects
net.ipv4.conf.all.send_redirects = 0 # Does not send redirects
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.rp_filter = 1 # Prevents IP spoofing
net.ipv4.tcp_syncookies = 1 # Mitigates SYN flood
net.ipv4.conf.all.log_martians = 1
# Kernel protections
kernel.kptr_restrict = 2 # Hides kernel pointers
kernel.dmesg_restrict = 1
sysctl --system
Firewall and the Docker problem
Configure UFW with a restrictive policy, allowing only SSH. The bridge ports must not be
opened here, the reason follows immediately.
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw --force enable
The critical point: Docker bypasses UFW by default. The reason lies in the iptables evaluation
order: traffic to a published port is DNATed in the PREROUTING chain (nat table) and
delivered to the container by traversing the FORWARD chain, never the INPUT chain, which is
exactly where UFW applies its rules. When publishing a port in a docker-compose.yml, Docker also
inserts its own rules into the DOCKER chain, evaluated within FORWARD before any UFW policy. The result is
that a container with a published port stays externally reachable even with UFW set to deny everything -
and, conversely, attempts to open the port through UFW alone do not have the expected effect. It is a
silent divergence between what ufw status reports and what the kernel actually enforces.
The fix is the community project ufw-docker, which reorganizes the chains so that UFW governs
container-bound traffic again.
wget -O /usr/local/bin/ufw-docker \
https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
chmod +x /usr/local/bin/ufw-docker
ufw-docker install && systemctl restart ufw
From this point on, rules are applied per container, not per generic port. The bridge-specific
rules are added after the services are up.
* Validation must be performed from outside the host, with independent tools (nmap or online port
checkers). Trusting only what runs locally is exactly what masks this class of failure.
obfs4 bridge configuration
The official Tor Project image bundles tor and obfs4proxy already configured as a
bridge. Create the project directory and the environment file.
mkdir -p /opt/tor-bridge && cd /opt/tor-bridge
# .env
OR_PORT=443
PT_PORT=8443
EMAIL=your-email@example.com
NICKNAME=YourBridge
The port choice is not arbitrary. The ORPort is used for communication with the Tor network and for the reachability check performed by the Bridge Authority; PT_PORT is the obfs4 port censored clients connect to. Common ports such as 443 tend to traverse restrictive firewalls more easily than arbitrary high ports. EMAIL is used by the Tor Project to notify the operator about problems with the bridge, and NICKNAME is the node's public identifier.
Next, define the docker-compose.yml with the stack's services.
services:
obfs4-bridge:
image: thetorproject/obfs4-bridge:latest
container_name: obfs4-bridge
restart: unless-stopped
env_file: .env
ports:
- "443:443"
- "8443:8443"
volumes:
- tor-data:/var/lib/tor
uptime-kuma:
image: louislam/uptime-kuma:1
container_name: uptime-kuma
restart: unless-stopped
ports:
- "127.0.0.1:3001:3001"
volumes:
- kuma-data:/app/data
volumes:
tor-data:
kuma-data:
The tor-data volume is the most important element of the configuration: it holds the bridge's identity keys. Losing that volume means losing the fingerprint, all reputation accumulated on the network and the continuity of Rdsys distribution, the bridge is treated as a brand new node again. Uptime Kuma, in turn, is published only on 127.0.0.1, deliberately unreachable from outside and accessible only through an SSH tunnel.
Bring the stack up and follow the initialization.
docker compose up -d
docker logs -f obfs4-bridge
Now apply the per-container firewall rules. Without this step, the ORPort is not reachable from outside and the bridge never completes validation.
ufw-docker allow obfs4-bridge 443/tcp
ufw-docker allow obfs4-bridge 8443/tcp
Bridge validation
Definitive confirmation comes from tor's own logs. The message below indicates that the Bridge
Authority managed to reach the ORPort from the internet.
docker logs obfs4-bridge | grep -i "self-testing"
[notice] Self-testing indicates your ORPort 203.0.113.10:443 is reachable
from the outside. Excellent. Publishing server descriptor.
With the bridge published, retrieve the bridge line, the string a censored client uses to connect manually to the node.
docker exec obfs4-bridge get-bridge-line
obfs4 203.0.113.10:8443 A1B2C3D4E5F60718293A4B5C6D7E8F9012345678 \
cert=Bvg/itxeL4TWKLP6N1MaQzSOC6tBjZmJ3EXAMPLEcert iat-mode=0
* The bridge line must not be published publicly. Distribution is Rdsys's responsibility, which hands it out in fragmented fashion to real users. Posting it in a repository, forum or social network is the fastest path to having it collected and added to block lists.
Querying Onionoo
The public Onionoo API exposes node metadata maintained by Tor Metrics. For bridges, the query
is not made with the plain fingerprint but with its SHA-1 hash, one more layer of protection
against enumeration. Extract the fingerprint from the volume and derive the hash.
docker exec obfs4-bridge cat /var/lib/tor/fingerprint
echo -n "<FINGERPRINT>" | xxd -r -p | sha1sum | cut -d' ' -f1 | tr 'a-z' 'A-Z'
With the hash in hand, query the bridge's state.
curl -s "https://onionoo.torproject.org/details?lookup=<HASHED_FINGERPRINT>" | jq
* Newly created bridges take a few hours to appear on Onionoo and a few days to be effectively distributed by Rdsys. No results in the first hours is expected behavior, not a symptom of error.
Monitoring and alerting
Since Uptime Kuma is published only on loopback, access to the interface goes through an SSH tunnel,
avoiding exposing the dashboard to the internet.
ssh -N -L 3001:localhost:3001 root@<PUBLIC_IP>
# Then, locally:
# http://localhost:3001
In the dashboard, configure a TCP Port monitor pointing at PT_PORT (8443) and a second one at the ORPort (443), with a 60-second interval. Then add a Telegram notification with the bot token and chat ID. Any port going down now triggers an immediate alert, and, for a bridge, prolonged downtime means losing position in Rdsys distribution.
Automated daily summary
Complementing Uptime Kuma's reactive alerting, a daily report provides the bridge's state from the
network's own perspective: assigned flags, observed bandwidth and the date of the last published
descriptor. Create the script /opt/tor-bridge/tor-bridge-report.sh.
#!/usr/bin/env bash
set -euo pipefail
HASHED_FP="<HASHED_FINGERPRINT>"
BOT_TOKEN="<TELEGRAM_BOT_TOKEN>"
CHAT_ID="<TELEGRAM_CHAT_ID>"
DATA=$(curl -s "https://onionoo.torproject.org/details?lookup=${HASHED_FP}")
RUNNING=$(echo "$DATA" | jq -r '.bridges[0].running')
NICKNAME=$(echo "$DATA" | jq -r '.bridges[0].nickname')
FLAGS=$(echo "$DATA" | jq -r '.bridges[0].flags | join(", ")')
SEEN=$(echo "$DATA" | jq -r '.bridges[0].last_seen')
[ "$RUNNING" = "true" ] && STATUS="online" || STATUS="OFFLINE"
MSG="Bridge ${NICKNAME}
Status: ${STATUS}
Flags: ${FLAGS}
Last descriptor: ${SEEN}"
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}" \
--data-urlencode "text=${MSG}" > /dev/null
Make the script executable and schedule daily execution via cron.
chmod +x /opt/tor-bridge/tor-bridge-report.sh
# crontab -e
0 9 * * * /opt/tor-bridge/tor-bridge-report.sh
* The script holds Telegram bot credentials. Restrict permissions with chmod 700 and keep it out of any versioned repository.
Fail2Ban and automatic updates
Finally, complete host hardening by enabling Fail2Ban for SSH and automatic security updates. A
bridge is a long-running, low-maintenance service, exactly the machine profile that tends to accumulate
outdated packages.
systemctl enable --now fail2ban
dpkg-reconfigure --priority=low unattended-upgrades
Final considerations
The total cost of maintaining a bridge lands in the USD 4 to 6 per month range, and the technical
knowledge required does not go beyond that of anyone already comfortable with Linux and Docker.
Operationally, the only discipline genuinely required is preserving the container's data volume and
keeping uptime stable.
The most transferable lesson from the process, however, goes beyond the Tor context: the divergence
between what ufw status reports and what the kernel actually enforces under Docker affects any
containerized service published on a VPS. Validating the exposure surface from outside the host, with
independent tools, should be a standard step in any deployment, not merely the outcome of a debugging
session.