Skip to content

luckylittle/zero_footprint_qbittorrent_seedbox

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

autobrr logo
luckylittle.zero_footprint_qbittorrent_seedbox

THIS IS CURENTLY UNDER DEVELOPMENT - IT IS A FORK OF MY RTORRENT/RUTORRENT ANSIBLE ROLE WHICH THIS IS BASED ON.

Configures vanilla RHEL8/9 (or CentOS 9) system to be lightweight and bulletproof seedbox running rTorrent and ruTorrent. It aims to be secure (SELinux, firewalld, SSL/TLS, Fail2Ban enabled) and creates absolutely no logs (a.k.a "zero footprint)". It also provides modern autodownloading capabilities with Autobrr and optional cross-seeding. Missing logs will make troubleshooting difficult, but ephemeral journal should be sufficient. Security and simplicity was priroitised over anything else. PRs are most welcome!

GitHub Release Ansible Role GitHub last commit

Requirements

  • It is expected, that you have a brand new RHEL8/9 or CentOS 9 stream system and have passwordless Ansible access sorted out - including working sudo (you can use my other role luckylittle/ansible-role-create-user for passwordless SSH access and sudo).
  • ⚠️ THIS ROLE REQUIRES PASSWORDLESS ACCESS TO YOUR SYSTEM USING SSH KEYPAIR AND NOT THE PASSWORD (e.g. ssh-copy-id) - otherwise you will lock yourself out, because sshd config will change to PasswordAuthentication no! ⚠️
  • ⚠️ Make sure to add your home IP address (or multiple addresses you connect from) to fail2ban_ignore_ipv4, or you risk locking yourself out, as it is also enforced by firewalld! ⚠️

Role Variables

Default variables are:

Common (section 1):

  • set_timezone - change the time zone of the server, defaults to Europe/Prague.
  • set_google_dns - if true, it will add Google DNS servers to the primary interface. Defaults to true.
  • create_new_user - whether you want to also create another user. Defaults to false. Relevant to the new_user variable.
  • autobrr_ver, mkbrr_ver & sizechecker_ver - contains the latest Autobrr, Mkbrr and Sizechecker versions. This gets regularly updated after the tests.
  • sysctl_tunables - on/off for various tuning options in sysctl.yml. Default is on.
  • cross_seed - Optional installation and configuration of the latest cross-seed automation tool. Default is false.

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.

rTorrent (section 2):

  • libtorrent_ver - Version of the libtorrent. It should be identical to rtorrent_ver.
  • rtorrent_ver - Version of the rtorrent. It should be identical to libtorrent_ver.
  • rtorrent_port - what port should rtorrent listen on. Default is 55442.

Note: The ratio defaults should be sufficient (between 400%-500%).

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.

ruTorrent (section 4):

  • rutorrent_ver - Version of the ruTorrent.
  • https_port - what port should rutorrent listen on, by default HTTPS (443).
  • htpasswd - HTTP basic password to log in to ruTorrent interface. Default is r3dh4t. ⚠️ It is recommended to change this to your own ⚠️

Security (section 5):

  • fail2ban_ignore_ipv4 - what IP addresses should be excluded from being banned by Fail2Ban, and the same value is also used in the firewalld limited zone for SSH (only these specified addresses are allowed to SSH to the seedbox). Whitelisted is arbitrary address X.X.X.X and the private IP ranges. ⚠️ You need to change it to your own ⚠️

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
  • community.general v10.6.0 (Install: ansible-galaxy collection install community.general)
  • community.crypto v2.26.1 (Install: ansible-galaxy collection install community.crypto)
  • ansible.posix v2.0.0 (Install: ansible-galaxy collection install ansible.posix)

Example Playbook

echo 'password1' > password.txt

ansible-playbook -i inventory --vault-password-file=password.txt site.yml

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

Testing

OS Version 2.3.0 Version 2.3.1 Version 2.4.0
RHEL9 âś… âś… âś…
CentOS9 âś… Not attempted Not attempted

On a brand new RHEL8.6, 1x vCPU, 4GB RAM playbook took 18m 32s to finish on VirtualBox. On a brand new Red Hat Enterprise Linux release 9.5 (Plow) on AWS (t3.medium), it took 18m 29s. The following versions were installed during the last RHEL9 test:

Package name Package version
fail2ban 1.1.0-6.el9.noarch
libdb-utils 5.3.28-57.el9.x86_64
lighttpd 1.4.67-1.el9.x86_64
php 8.0.30-3.el9_6.x86_64
tmux 3.2a-5.el9.x86_64
vsftpd 3.0.5-6.el9.x86_64

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

terraform apply -var=key_name=<NAME_OF_THE_EXISTING_KEY_PAIR_IN_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_rtorrent_port_tcp" {
  security_group_id = aws_security_group.rhel9_sg.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 55442
  ip_protocol       = "tcp"
  to_port           = 55442
  description       = "Default rtorrent_port (TCP)"
  tags = {
    Name = "allow_rtorrent_port_tcp"
  }
}

resource "aws_vpc_security_group_ingress_rule" "allow_rtorrent_port_udp" {
  security_group_id = aws_security_group.rhel9_sg.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 55442
  ip_protocol       = "udp"
  to_port           = 55442
  description       = "Default rtorrent_port (UDP)"
  tags = {
    Name = "allow_rtorrent_port_udp"
  }
}

resource "aws_vpc_security_group_ingress_rule" "allow_pasv_port_range" {
  security_group_id = aws_security_group.rhel9_sg.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 64000
  ip_protocol       = "tcp"
  to_port           = 64321
  description       = "Default pasv_port_range (TCP)"
  tags = {
    Name = "allow_pasv_port_range"
  }
}

resource "aws_vpc_security_group_ingress_rule" "allow_ftp_port" {
  security_group_id = aws_security_group.rhel9_sg.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 55443
  ip_protocol       = "tcp"
  to_port           = 55443
  description       = "Default ftp_port (TCP)"
  tags = {
    Name = "allow_ftp_port"
  }
}

resource "aws_vpc_security_group_ingress_rule" "allow_tls_ipv4" {
  security_group_id = aws_security_group.rhel9_sg.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 443
  ip_protocol       = "tcp"
  to_port           = 443
  description       = "Default ruTorrent port (IPv4)"
  tags = {
    Name = "allow_tls_ipv4"
  }
}

resource "aws_vpc_security_group_ingress_rule" "allow_autobrr_port" {
  security_group_id = aws_security_group.rhel9_sg.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 7474
  ip_protocol       = "tcp"
  to_port           = 7474
  description       = "Default Autobrr port (TCP)"
  tags = {
    Name = "allow_autobrr_port"
  }
}

resource "aws_vpc_security_group_ingress_rule" "allow_ssh_port" {
  security_group_id = aws_security_group.rhel9_sg.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 22
  ip_protocol       = "tcp"
  to_port           = 22
  description       = "Default SSH port (TCP)"
  tags = {
    Name = "allow_ssh_port"
  }
}

resource "aws_vpc_security_group_ingress_rule" "allow_tls_ipv6" {
  security_group_id = aws_security_group.rhel9_sg.id
  cidr_ipv6         = "::/0"
  from_port         = 443
  ip_protocol       = "tcp"
  to_port           = 443
  description       = "Default ruTorrent port (IPv6)"
  tags = {
    Name = "allow_tls_ipv6"
  }
}

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
}

Then you can just add instance_public_ip to the inventory and run this Ansible role against the EC2 machine like: time ansible-playbook -i inventory -u ec2-user test.yml --ask-vault-pass within the tests folder (cd tests; ln -s ../../zero_footprint_qbittorrent_seedbox .).

Services Installed

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

Service URL
autobrr https://<IP_ADDR>:<https_port>/autobrr/
autobrr healthz https://<IP_ADDR>:<https_port>/autobrr/api/healthz/liveness
ftp ftps://<IP_ADDR>:<ftp_port>
rtorrent rpc https://<IP_ADDR>:<https_port>/plugins/httprpc/action.php
rutorrent https://<IP_ADDR>:<https_port>
ssh ssh://<IP_ADDR>:22
cross-seed (optional) http://<127.0.0.1>:2468 (optional)

License

MIT

Ansible Galaxy

luckylittle.zero_footprint_qbittorrent_seedbox

Author Information

Lucian Maly <lmaly@redhat.com>

Last update: Tue 15 Jul 2025 01:33:49 UTC