This post covers how I secure my torrent box so it only runs torrents when the VPN is active. No firewall killswitch, no Docker, no pretending it’s more complicated than it is.
The goal is simple:
- The VPN must be up before qBittorrent starts
- If the VPN drops, qBittorrent must stop
- qBittorrent should bind to the VPN interface/IP
- PIA port forwarding should be applied automatically
- Downloads should land on the drive with the most free space (for now)
This is a service-level enforcement model using systemd:
qbittorrent-vpn.servicerequirespia-vpn.service- If the PIA manual connection drops, the VPN service ends
- When the VPN service ends, systemd stops qBittorrent immediately
What I’m Using
- Linux-based torrent box
- Private Internet Access (PIA)
- OpenVPN via PIA’s official manual-connections scripts
- systemd for boot + dependency control
- qBittorrent-nox
PIA manual connections repo:
https://github.com/pia-foss/manual-connections
The Security Model (What This Does and Does Not Do)
What this does: prevents torrenting without the VPN by controlling whether the torrent process is allowed to exist.
- qBittorrent starts only after VPN is up and port-forwarding is assigned
- qBittorrent is configured to bind to the VPN interface/IP
- If the VPN drops and the service exits, qBittorrent is stopped
What this does not do: it is not a firewall-based killswitch. If you start qBittorrent manually outside systemd, you are bypassing the safety rails.
systemd: Start PIA VPN on Boot
This unit starts the PIA VPN after the network is online and enables PIA port forwarding.
[Unit]
Description=Start PIA VPN on boot
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStartPre=/bin/sleep 10
ExecStartPre=/bin/bash -c '/usr/bin/curl -s ifconfig.me > /home/<user>/original_ip.txt'
WorkingDirectory=/home/<user>/Documents/manual-connections
Environment="VPN_PROTOCOL=openvpn"
Environment="DISABLE_IPV6=yes"
Environment="DIP_TOKEN=no"
Environment="AUTOCONNECT=true"
Environment="PIA_PF=true"
Environment="PIA_DNS=true"
EnvironmentFile=/etc/pia.env
ExecStart=/home/<user>/Documents/manual-connections/run_setup.sh
Restart=no
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Suggested screenshot: systemctl status pia-vpn.service and journalctl -u pia-vpn.service showing the forwarded port line.
systemd: Start qBittorrent Only After VPN Is Active
This unit makes qBittorrent dependent on the VPN. If the VPN service ends, qBittorrent is stopped by systemd.
[Unit]
Description=Start qBittorrent after VPN port is assigned
After=network-online.target pia-vpn.service
Requires=pia-vpn.service
[Service]
Type=simple
User=<user>
ExecStart=/home/<user>/Documents/start_qbittorrent_after_vpn.sh
Restart=no
[Install]
WantedBy=multi-user.target
The Script: Start qBittorrent After Port Forwarding, Bind to VPN, Pick Best Drive
This script waits for PIA to assign a forwarded port, then:
- extracts the forwarded port from the VPN service logs
- detects the VPN IP on
tun06(stable on my box) - updates qBittorrent’s config to use the forwarded port
- binds qBittorrent to the VPN interface/IP
- chooses the mounted drive with the most free space and sets download paths (This is because I manually manage my drives, in the future I am updating my NAS)
- starts
qbittorrent-nox
Here is the start_qbittorrent_after_vpn.sh script
#!/bin/bash
CONFIG_PATH="/home/<user>/.config/qBittorrent/qBittorrent.conf"
FORWARD_LINE="Forwarded port"
VPN_INTERFACE="tun06"
DRIVE_CANDIDATES=(/mnt/seagate /mnt/mybook /mnt/passport /mnt/hddthree)
# Function to find the drive with the most available space
get_best_drive() {
best_drive=""
best_space=0
for dir in "${DRIVE_CANDIDATES[@]}"; do
if mountpoint -q "$dir"; then
space=$(df --output=avail "$dir" | tail -1)
if [[ "$space" -gt "$best_space" ]]; then
best_space=$space
best_drive="$dir"
fi
fi
done
echo "$best_drive"
}
# Wait for VPN forwarded port to appear in logs
journalctl -u pia-vpn -f | while read -r line; do
if [[ "$line" == *"$FORWARD_LINE"* ]]; then
PORT=$(echo "$line" | awk '{print $NF}')
echo "Detected forwarded port: $PORT"
echo "$PORT" > /home/<user>/vpn_port.txt
VPN_IP=$(ip -4 addr show "$VPN_INTERFACE" | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
if [[ -z "$VPN_IP" ]]; then
echo "Failed to get IP address for $VPN_INTERFACE"
exit 1
fi
echo "Detected VPN IP: $VPN_IP"
BEST_DRIVE=$(get_best_drive)
if [[ -z "$BEST_DRIVE" ]]; then
echo "No suitable drive found. Aborting."
exit 1
fi
echo "Using drive with most space: $BEST_DRIVE"
# Make sure the folders exist
mkdir -p "${BEST_DRIVE}/Complete"
mkdir -p "${BEST_DRIVE}/Incomplete"
# Escape slashes for sed replacements
SAVE_PATH_ESCAPED=$(echo "${BEST_DRIVE}/Complete" | sed 's/\//\\\//g')
TEMP_PATH_ESCAPED=$(echo "${BEST_DRIVE}/Incomplete" | sed 's/\//\\\//g')
# Backup the config
cp "$CONFIG_PATH" "${CONFIG_PATH}.bak"
# Update Session\Port
if grep -q "^Session\\\\Port=" "$CONFIG_PATH"; then
sed -i "s|^Session\\\\Port=.*|Session\\\\Port=$PORT|" "$CONFIG_PATH"
else
echo "Session\\Port=$PORT" >> "$CONFIG_PATH"
fi
# Update Preferences\Connection\InterfaceAddress
if grep -q "^Preferences\\\\Connection\\\\InterfaceAddress=" "$CONFIG_PATH"; then
sed -i "s|^Preferences\\\\Connection\\\\InterfaceAddress=.*|Preferences\\\\Connection\\\\InterfaceAddress=$VPN_IP|" "$CONFIG_PATH"
else
echo "Preferences\\Connection\\InterfaceAddress=$VPN_IP" >> "$CONFIG_PATH"
fi
# Update Session\DefaultSavePath
if grep -q "^Session\\\\DefaultSavePath=" "$CONFIG_PATH"; then
sed -i "s|^Session\\\\DefaultSavePath=.*|Session\\\\DefaultSavePath=$SAVE_PATH_ESCAPED|" "$CONFIG_PATH"
else
echo "Session\\DefaultSavePath=${BEST_DRIVE}/Complete" >> "$CONFIG_PATH"
fi
# Update Session\TempPath
if grep -q "^Session\\\\TempPath=" "$CONFIG_PATH"; then
sed -i "s|^Session\\\\TempPath=.*|Session\\\\TempPath=$TEMP_PATH_ESCAPED|" "$CONFIG_PATH"
else
echo "Session\\TempPath=${BEST_DRIVE}/Incomplete" >> "$CONFIG_PATH"
fi
# Update Downloads\SavePath (older qBittorrent versions)
if grep -q "^Downloads\\\\SavePath=" "$CONFIG_PATH"; then
sed -i "s|^Downloads\\\\SavePath=.*|Downloads\\\\SavePath=${SAVE_PATH_ESCAPED}\/|" "$CONFIG_PATH"
else
echo "Downloads\\SavePath=${BEST_DRIVE}/Complete/" >> "$CONFIG_PATH"
fi
# Update Downloads\TempPath
if grep -q "^Downloads\\\\TempPath=" "$CONFIG_PATH"; then
sed -i "s|^Downloads\\\\TempPath=.*|Downloads\\\\TempPath=${TEMP_PATH_ESCAPED}\/|" "$CONFIG_PATH"
else
echo "Downloads\\TempPath=${BEST_DRIVE}/Incomplete/" >> "$CONFIG_PATH"
fi
# Ensure TempPathEnabled is set
if grep -q "^Downloads\\\\TempPathEnabled=" "$CONFIG_PATH"; then
sed -i "s|^Downloads\\\\TempPathEnabled=.*|Downloads\\\\TempPathEnabled=true|" "$CONFIG_PATH"
else
echo "Downloads\\TempPathEnabled=true" >> "$CONFIG_PATH"
fi
# Update Connection\PortRangeMin (older versions)
if grep -q "^Connection\\\\PortRangeMin=" "$CONFIG_PATH"; then
sed -i "s|^Connection\\\\PortRangeMin=.*|Connection\\\\PortRangeMin=$PORT|" "$CONFIG_PATH"
else
echo "Connection\\PortRangeMin=$PORT" >> "$CONFIG_PATH"
fi
# Update Connection\InterfaceAddress (older versions)
if grep -q "^Connection\\\\InterfaceAddress=" "$CONFIG_PATH"; then
sed -i "s|^Connection\\\\InterfaceAddress=.*|Connection\\\\InterfaceAddress=$VPN_IP|" "$CONFIG_PATH"
else
echo "Connection\\InterfaceAddress=$VPN_IP" >> "$CONFIG_PATH"
fi
echo -e "\n--- qBittorrent config updated ---"
grep -E "Session\\\\(Port|DefaultSavePath|TempPath)|Preferences\\\\Connection\\\\InterfaceAddress" "$CONFIG_PATH"
# Start qBittorrent
qbittorrent-nox --webui-port=8080
break
fi
done
What Worked
- Dependency enforcement: if the VPN dies, the torrent client dies
- VPN binding: qBittorrent is configured to use the VPN interface/IP
- Port forwarding automation: no manual port fiddling after reboot
- Drive selection automation: dumps to the drive with the most space (temporary but useful)
What Didn’t (Or Will Change Later)
- This is not a firewall killswitch: it’s process control via systemd
- Hard-coded interface: stable on my box (
tun06) but could vary for others - Drive scanning is temporary: this will change as storage gets reworked
Wrap-Up
This setup does exactly what I want: qBittorrent only starts after PIA is connected and port forwarding is assigned, and it stops immediately if the VPN service ends.
There are cleaner solutions using Docker and Gluetun, and I plan to explore those later. For now, this systemd + script approach is simple, transparent, and easy to debug.
Next article suggestion: Securing Your Indexers
Question for you
Are you doing process-control like this, or do you prefer a firewall killswitch? If you’ve got a cleaner pattern, I want to see it.
1 thought on “Securing the Torrent Box”