Twelve builds

A seedbox on a 50 TB storage node, with permissions that hold

qBittorrent on an S-50 with an NVMe staging tier, a hardened unit, group permissions that survive every new download, and a proxy that keeps the interface off the public internet.

What this builds

A storage node running qBittorrent as an unprivileged service: incomplete data written to the NVMe tier, completed data moved to the fifty-terabyte bulk tier, everything owned by a shared group so that a second account can read it over SFTP without any chmod archaeology, and a web interface reachable only through an authenticated proxy.

The obvious approach is to install the package, click through the interface and get on with your day. That works for a fortnight, until the first file arrives with the wrong group and your sync user cannot read it, and by then there are forty thousand files with the wrong group.

Before you start

  • An S-50: fifty terabytes of bulk behind a one-terabyte NVMe tier, on a ten-gigabit port. Traffic is unmetered under fair use, and the acceptable use policy is short and specific about what may cross it. Distribution rights are your problem, not a footnote.
  • Debian 13, root, and a hostname such as box.example.com.

1. Users, groups, layout

apt update && apt install -y qbittorrent-nox nginx apache2-utils acl
groupadd -g 3000 media
useradd -r -g media -u 3000 -d /srv/qbt -m -s /usr/sbin/nologin qbt
useradd -m -G media sync

Two accounts, one group. The daemon runs as qbt and never logs in; sync is the account you pull files with and can write nothing. Now the directories, and the two flags that make this work permanently:

install -d -o qbt -g media -m 2775 /srv/nvme/incomplete
install -d -o qbt -g media -m 2775 /srv/bulk/complete
setfacl -d -m g:media:rwx /srv/bulk/complete
setfacl -d -m g:media:rwx /srv/nvme/incomplete

Mode 2775 sets the setgid bit, so every file created inside inherits the media group instead of the creating user’s primary group. The default ACL then grants that group read and write on new entries regardless of the process umask. Together they mean the permission question is answered once rather than after every download.

On an S-50 the bulk tier and the NVMe are separate block devices. Confirm which is which before you commit to the paths:

lsblk -o NAME,SIZE,ROTA,MOUNTPOINT

2. The daemon

Start it once so it writes a configuration file, then stop it and edit that file. Fighting the interface for settings that live in a text file is a waste of an afternoon.

sudo -u qbt qbittorrent-nox --webui-port=8080 &
sleep 5 && pkill -u qbt qbittorrent-nox

Edit /srv/qbt/.config/qBittorrent/qBittorrent.conf so the [BitTorrent] and [Preferences] sections contain:

[BitTorrent]
Session\DefaultSavePath=/srv/bulk/complete
Session\TempPath=/srv/nvme/incomplete
Session\TempPathEnabled=true
Session\Port=51413
Session\DiskCacheSize=2048
Session\MaxConnections=800
Session\MaxUploads=40
Session\GlobalMaxSeedingMinutes=-1
Session\Preallocation=true

[Preferences]
WebUI\Address=127.0.0.1
WebUI\Port=8080
WebUI\LocalHostAuth=false
WebUI\CSRFProtection=true
Downloads\UseIncompleteExtension=true

Staging on NVMe is the whole trick. Torrent writes are small and scattered, which is the worst possible pattern for the bulk tier; the NVMe absorbs them, and the completed file moves across as one sequential copy. Preallocation stops fragmentation on the way in.

3. A unit that cannot wander

/etc/systemd/system/qbittorrent.service:

[Unit]
Description=qBittorrent
After=network-online.target
Wants=network-online.target

[Service]
User=qbt
Group=media
UMask=0002
ExecStart=/usr/bin/qbittorrent-nox
Restart=on-failure
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
ReadWritePaths=/srv/qbt /srv/nvme/incomplete /srv/bulk/complete
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

UMask=0002 and Group=media are what keep new files group-writable. Without the first, setgid gives you the right group on files nobody in that group can write, which is a subtle and irritating half-success.

systemctl daemon-reload && systemctl enable --now qbittorrent

4. Kernel and firewall

Ten gigabits with several hundred peers needs bigger socket buffers and a connection table that will not fill:

cat > /etc/sysctl.d/70-seedbox.conf <<EOF
net.core.rmem_max = 33554432
net.core.wmem_max = 33554432
net.ipv4.tcp_rmem = 4096 87380 33554432
net.ipv4.tcp_wmem = 4096 65536 33554432
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
net.netfilter.nf_conntrack_max = 524288
EOF
sysctl --system

Firewall: the torrent port open on both families, SSH and HTTPS open, the web interface bound to loopback and therefore needing nothing.

table inet filter {
  chain input {
    type filter hook input priority filter; policy drop;
    ct state established,related accept
    iif lo accept
    ip6 nexthdr icmpv6 accept
    ip protocol icmp accept
    tcp dport { 22, 443 } accept
    tcp dport 51413 accept
    udp dport 51413 accept
  }
}

5. The interface, behind a proxy

certbot certonly --standalone -d box.example.com
htpasswd -c /etc/nginx/qbt.htpasswd you
server {
  listen 443 ssl;
  server_name box.example.com;
  ssl_certificate     /etc/letsencrypt/live/box.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/box.example.com/privkey.pem;
  client_max_body_size 100m;

  location / {
    auth_basic "closed";
    auth_basic_user_file /etc/nginx/qbt.htpasswd;
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header X-Forwarded-Host $http_host;
    proxy_http_version 1.1;
  }
}

Verify it

Use something with a large, well-seeded swarm and an unambiguous licence. A current Debian installer image is the traditional choice and saturates a ten-gigabit port more convincingly than anything else you can legally test with.

Add the torrent through the interface, then watch the staging tier fill:

watch -n2 "du -sh /srv/nvme/incomplete /srv/bulk/complete"

While it runs, confirm the port is genuinely reachable rather than merely open in your own firewall, since an unreachable port produces a seedbox that downloads acceptably and seeds to nobody:

ss -tn state established sport = :51413 | wc -l

Incoming connections on that port mean peers found you. Zero, with plenty of outgoing connections, means only you are dialling out.

When the download completes, check the three things this build exists for:

ls -l /srv/nvme/incomplete
ls -ln /srv/bulk/complete
sudo -u sync cat /srv/bulk/complete/*.iso > /dev/null && echo "sync can read"

The staging directory is empty, the completed files show group 3000 with group read and write, and the sync account can read them without any manual permission fixing. That third command is the one that proves the setgid and ACL work paid off.

Finally, prove the interface is not exposed:

curl -m5 http://box.example.com:8080/ ; echo "exit $?"
curl -sI https://box.example.com/ | head -1

The first should fail on connection, the second should return 401. Anything else and the address binding in step two did not take.

Afterwards

Set a global seeding ratio or a share-time limit unless you intend to seed everything forever, because fifty terabytes fills faster than anyone expects. If a complaint ever reaches us about traffic from your instance, the process is described on the abuse page: it is forwarded to you, and we do not go looking through your disk.

Ready when you are

Pick a city. Pick a size. Pay in coin.

No forms about who you are, no wait for a human to approve you, no phone call to verify anything. The invoice clears and the credentials land in your inbox.