Skip to content

luckylittle/zero_footprint_qbittorrent_seedbox

autobrr logo
luckylittle.zero_footprint_qbittorrent_seedbox

Project overview

This Ansible role provisions a standard Red Hat Enterprise Linux 9 (RHEL 9) system as a secure, efficient and lightweight peer-to-peer (P2P) seedbox, running qBittorrent.

The configuration prioritizes security and simplicity, utilizing integrated tools such as SELinux and firewalld. To maintain a minimal system footprint, it is configured for zero-logging operation and no shell history. Among others (mkbrr, tqm, netronome, sizechecker etc.), the role incorporates Autobrr for modern automated downloads and cross-seed for enhanced seeding.

Please be aware that the absence of persistent logs may complicate troubleshooting, though the ephemeral journal should be sufficient for most diagnostics. This project is an enhanced fork of my zero_footprint_rutorrent_seedbox repository, simplified and adapted for qBittorrent (lot of lessons learned!). Contributions via pull requests are welcome.

ansible-lint GitHub Release Ansible Role GitHub last commit

Prerequisites

  • A clean installation of RHEL 9 (CentOS 9 Stream should also work, but is not always tested).
  • Pre-configured, passwordless Ansible access with sudo privileges. The luckylittle/ansible-role-create-user role may be used to establish this access.
  • Access via password should also be in place (mainly due to single-user vsftpd) - e.g. sudo passwd <user>.

⚠️ Important configuration notes ⚠️

  • SSH Key Authentication is mandatory: This role will disable password-based SSH access by setting PasswordAuthentication no. You must configure SSH key-pair authentication BEFORE execution to avoid being completely locked out of the system.
  • Firewall IP whitelisting: To ensure you can access the system after the firewall is enabled, you must add your client IP address(es) to the ipv4_whitelist variable. Failure to do so will result in a system lockout as well.

Architecture

img

Role Variables

Default variables are:

Common (section 1):

  • set_google_dns - if true, it will add Google DNS servers to the primary interface. Defaults to true.
  • set_timezone - change the time zone of the server, defaults to Australia/Sydney.
  • sysctl_tunables - on/off for various tuning options in sysctl.yml. Default is on.

Note: Lot of the tasks rely on remote_user / ansible_user variable (user who logs in to the remote machine via Ansible). For example, it creates directory structure under that user.

qBt (section 2):

  • qbt_port - what port should qBittorrent listen on. Default is 55442.

vsFTPd (section 3):

  • ftp_port - what port should vsftpd listen on. Default is 55443.
  • pasv_port_range - what port range should be used for FTP PASV, by default this is 64000-64321.
  • single_user - when true only one FTP user will be used and it is the same username who runs this playbook. ⚠️ When false, this file is used, update accordingly ⚠️ This is now true by default.

Security (section 5):

  • ipv4_whitelist - what IP addresses should be used in the firewalld zone for access to services. Default whitelisted is arbitrary address X.X.X.X. ⚠️ You need to change it to your own ⚠️

Example: 192.168.0.0/16 10.0.0.0/8 172.16.0.0/12 123.222.11.111

Reboot (section 7):

  • require_reboot - does the machine require reboot after the playbook is finished. It is recommended & default to be true.

Role variables are also tunable, but it is not recommended to change them unless you know what you are doing.

Dependencies

  • Ansible core v2.16.14
  • ansible-galaxy collection install -r requirements.yml

Example Inventory & Playbook

[seedbox]
123.124.125.126
---
- hosts: seedbox
  name: Playbook for zero_footprint_qbittorrent_seedbox role
  roles:
    - "luckylittle.zero_footprint_qbittorrent_seedbox"

Testing

OS Version 0.1.1 Version 0.1.2
9.6 (Plow)

On a brand new Red Hat Enterprise Linux release 9.6 (Plow) on AWS (t3.medium - 2 vCPU, 4GiB RAM), it took 13m 59s. The following versions were installed during the last RHEL9 test:

Package name Package version
autobrr 1.65.0
bash 5.1.8-9.el9.x86_64
cross-seed 6.13.2
curl 7.76.1-31.el9_6.1.x86_64
firewalld 1.3.4-9.el9_5.noarch
libdb-utils 5.3.28-57.el9_6.x86_64
mkbrr 1.15.0
netronome 0.6.0
NetworkManager 1.52.0-5.el9_6.x86_64
nvm 0.40.3
openssh 8.7p1-45.el9.x86_64
qBittorrent 5.1.2
sizechecker 1.4.0
tar 1.34-7.el9.x86_64
tqm 1.16.0
traceroute 2.1.1-1.el9.x86_64
tuned 2.25.1-2.el9_6.noarch
vnstat 2.9-2.el9.x86_64
vsftpd 3.0.5-6.el9.x86_64
wget 1.21.1-8.el9_4.x86_64

The following Terraform can be used to create necessary infrastructure (based on RHEL9.X on AWS):

# Configure the AWS Provider
provider "aws" {
  region = "ap-southeast-2"
}

# Variable
variable "key_name" {
  type        = string
  default     = "ec2-pair"
  description = "AWS Key-pair"
}

# Find latest RHEL 9 AMI
data "aws_ami" "rhel9" {
  most_recent = true
  owners      = ["309956199498"] # Red Hat's AWS account ID

  filter {
    name   = "name"
    values = ["RHEL-9*"]
  }

  filter {
    name   = "architecture"
    values = ["x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  filter {
    name   = "root-device-type"
    values = ["ebs"]
  }
}

# Create a security group
resource "aws_security_group" "rhel9_sg" {
  name        = "rhel9_sg"
  description = "Security group for RHEL 9 EC2 seedbox instance"

  tags = {
    Name = "RHEL9-SecurityGroup"
  }
}

resource "aws_vpc_security_group_ingress_rule" "allow_all" {
  security_group_id = aws_security_group.rhel9_sg.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1"
  description       = "Generally a bad practice, but we need to test firewalld functionality"
  tags = {
    Name = "allow_all"
  }
}

resource "aws_vpc_security_group_egress_rule" "allow_all_traffic_ipv4" {
  security_group_id = aws_security_group.rhel9_sg.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1" # semantically equivalent to all ports
}

resource "aws_vpc_security_group_egress_rule" "allow_all_traffic_ipv6" {
  security_group_id = aws_security_group.rhel9_sg.id
  cidr_ipv6         = "::/0"
  ip_protocol       = "-1" # semantically equivalent to all ports
}

# Create an EC2 instance
resource "aws_instance" "rhel_instance" {
  ami                    = data.aws_ami.rhel9.id
  instance_type          = "t3.medium"
  vpc_security_group_ids = [aws_security_group.rhel9_sg.id]
  key_name               = var.key_name # Replace with your key pair name

  root_block_device {
    volume_size = 15
    volume_type = "gp3"
    encrypted   = true
    tags = {
      Name = "RHEL-9-Seedbox"
    }
  }

  ebs_block_device {
    device_name           = "/dev/sdb"
    volume_size           = 15
    volume_type           = "gp3"
    encrypted             = true
    delete_on_termination = true
    tags = {
      Name = "RHEL-9-Seedbox"
    }
  }

  user_data = <<EOF
#!/bin/bash
# Log all output for debugging
exec > >(tee /var/log/user-data.log) 2>&1
echo "Starting user data script at $(date)"
# Wait for the EBS volume to be available
echo "Waiting for EBS volume to be available..."
while [ ! -e /dev/nvme1n1 ]; do
  echo "Waiting for /dev/nvme1n1..."
  sleep 5
done
echo "EBS volume /dev/nvme1n1 is available"
# Create partition on the EBS volume
echo "Creating partition on /dev/nvme1n1..."
(
echo n # Add a new partition
echo p # Primary partition
echo 1 # Partition number
echo   # First sector (Accept default: 1)
echo   # Last sector (Accept default: varies)
echo w # Write changes
) | fdisk /dev/nvme1n1
# Wait a moment for the partition to be recognized
sleep 5
# Format the partition with XFS
echo "Formatting /dev/nvme1n1p1 with XFS..."
mkfs.xfs /dev/nvme1n1p1
# Get the UUID of the new partition
echo "Getting UUID of the partition..."
UUID=$(blkid -s UUID -o value /dev/nvme1n1p1)
echo "UUID: $UUID"
# Add entry to /etc/fstab
echo "Adding entry to /etc/fstab..."
echo "UUID=$UUID /home xfs defaults 0 0" >> /etc/fstab
# Create a temporary mount point to preserve existing home data
echo "Creating temporary mount point..."
mkdir -p /mnt/temp_home
# Mount the new volume temporarily
mount /dev/nvme1n1p1 /mnt/temp_home
# Copy existing /home contents to the new volume (if any)
if [ "$(ls -A /home 2>/dev/null)" ]; then
  echo "Copying existing /home contents to new volume..."
  cp -arv /home/* /mnt/temp_home/
fi
# Unmount the temporary mount
umount /mnt/temp_home
rmdir /mnt/temp_home
# Mount the new volume to /home
echo "Mounting new volume to /home..."
mount -av
# Reload systemd daemon
systemctl daemon-reload
# Verify the mount
echo "Verifying mount..."
df -h /home
mount | grep /home
# Restore default SELinux security contexts
restorecon -Rv /home/
echo "User data script completed successfully at $(date)"
# Optional: Create a marker file to indicate completion
touch /var/log/user-data-complete
EOF

  tags = {
    Name        = "RHEL-9-Seedbox"
    Environment = "Dev"
  }
}

# Output the instance details
output "instance_id" {
  value = aws_instance.rhel_instance.id
}

output "instance_public_ip" {
  value = aws_instance.rhel_instance.public_ip
}

output "instance_dns" {
  value = aws_instance.rhel_instance.public_dns
}

To test:

# Create a testing EC2 machine
cd tests/
terraform init; terraform apply -var=key_name=<NAME_OF_THE_EXISTING_KEY_PAIR_IN_AWS> -auto-approve
# Insert the EC2 machine's public IP to the Ansible inventory
terraform output -raw instance_public_ip > inventory
# Make necessary symlink for testing playbook
mkdir roles/; ln -s ../../ roles/luckylittle.zero_footprint_qbittorrent_seedbox
# Run the test
time ansible-playbook -i inventory -u ec2-user test.yml
# To destroy the EC2 machine afterwards
# terraform destroy -auto-approve

Services Installed

After you successfully apply this role, you should be able to see a similar output and access the following services:

"----------------------------------------------------"
"Autobrr URL:"
"http://123.124.125.126:7474/onboard"
"----------------------------------------------------"
"Autobrr Healthz URL:"
"http://123.124.125.126:7474/api/healthz/liveness"
"----------------------------------------------------"
"qBt WebUI:"
"http://123.124.125.126:8080"
"----------------------------------------------------"
"Netronome URL:",
"http://123.124.125.126:7575",
"----------------------------------------------------"
"vsFTPd URL:"
"ftps://123.124.125.126:55443"
"----------------------------------------------------"

License

MIT

Ansible Galaxy

luckylittle.zero_footprint_qbittorrent_seedbox

Author Information

Lucian Maly <[email protected]>

Last update: Fri 29 Aug 2025 00:02:49 UTC