This document describes the security practices followed across all repositories in the Torrust organization.
It is intended to be transparent about how we manage security for our open-source BitTorrent tools, and to serve as a reference for contributors, researchers, and users.
- Scope
- Security Model
- Development Environment
- CI/CD Pipeline
- Production Infrastructure
- Vulnerability Disclosure & Triage
- AI Tools in Security Workflows
These practices apply to the following repositories (and any future additions):
| Repository | Language | Description |
|---|---|---|
| torrust-tracker | Rust | BitTorrent tracker |
| torrust-index | Rust | Torrent index backend |
| torrust-tracker-deployer | Rust / Ansible / OpenTofu | Deployment automation |
| torrust-tracker-demo | Shell | Demo server CI and config |
| torrust-demo | Shell | Live demo CI and config |
| torrust-website | Svelte | Public website |
| bittorrent-primitives | Rust | Core BitTorrent types |
| torrust-linting | Rust | Shared linting utilities |
Security concerns in Torrust span three environments and multiple layers within each.
| Environment | Description |
|---|---|
| Development | Local developer machines, editor tooling, local secrets (SSH keys, GPG keys, API tokens, Docker Hub credentials) |
| CI | GitHub Actions runners, workflow permissions, secrets injected at runtime, container builds, SARIF uploads |
| Production | Remote servers, firewall, OS hardening, automatic security updates, deployed containers, databases, TLS, monitoring |
Within each environment, issues arise at different layers:
| Layer | Examples |
|---|---|
| Base OS | Kernel, system libraries (glibc, OpenSSL), automatic updates, firewall (UFW) |
| Tools & Dependencies | Rust crates (RustSec advisories), container base images (Trivy CVEs), third-party services |
| Application | Secret handling, input validation, DoS vectors, error messages leaking internals |
| Access & Credentials | SSH keys, GPG signing keys, API tokens, registry credentials (Docker Hub, crates.io), production server access |
The sections below describe the specific practices applied at each layer and environment. A full index of security-labelled GitHub issues mapped to this model is maintained in SECURITY_ISSUES.md.
Not all security surfaces carry equal risk. The following priority framework guides where scanning and remediation effort is focused first:
| Priority | Surface | Rationale |
|---|---|---|
| 1 — Critical | Internet-facing production components (public endpoints, databases, runtime services) | Run continuously and exposed to users; a vulnerability directly affects availability or data |
| 2 — Important | User workflow security (deployment secrets, SSH keys, AI agent access to credentials) | Mistakes here expose production access to attackers or unintended third parties |
| 3 — Standard | Internal tooling and build-time components (CLI tools, IaC tooling, package build steps) | Short-lived, not internet-exposed; priority increases if ever promoted to a production service |
| 4 — Low | Test-only and CI-only components (test servers, mock environments, sandbox simulations) | Never run in production; re-scan only if promoted to a higher environment |
Security practices for local developer machines and contributor workflows.
Rule: Secrets must never be committed to version control. Each project must provide a .env.example documenting required variables; actual .env files must be git-ignored.
Current practice: .env.example files are maintained in all projects. GitHub's native secret scanning is enabled on all repositories and alerts maintainers when known secret patterns (API keys, tokens, credentials) are detected in commits. At the IDE level, maintainers using VS Code are encouraged to install the DevSkim plugin, which flags hardcoded passwords, tokens, and credentials inline as you type.
Rule: Maintainers must sign all commits with a GPG key. Branch protection on key repositories enforces that all commits merged into protected branches are verified.
Current practice: GPG signing is configured per-user. Branch protection rules in key repositories require signed commits.
Rule: When publishing Docker images manually (not through CI), registry credentials must be stored encrypted — not in base64. The default Docker credential store uses base64 encoding, which provides no real protection if the config file is exposed.
Current practice: In CI, Docker Hub credentials are stored as GitHub Actions secrets and injected at runtime — they never touch local disk. For the rare cases where manual publishing is needed, maintainers should use pass + docker-credential-pass to store registry credentials encrypted on disk.
Planned: A full setup guide is tracked in torrust-tracker#1458.
Rule: When a contributor stops actively contributing, their organisation access must be reduced promptly. Credentials associated with their access must be reviewed and rotated as needed.
Current practice: The team is small. Contributors who stop actively contributing are converted from organisation member to external collaborator. This removes their access to private repositories and org-level settings while preserving the ability to collaborate on specific repositories if needed. API tokens follow a naming convention that encodes the creation date (see API Tokens & Platform Access), making it straightforward to identify and revoke stale credentials.
Rule: Contributors are encouraged to run cargo audit locally before opening a pull request to catch known RustSec advisories before CI does.
Current practice: Dependabot opens daily PRs for outdated Cargo dependencies. Local cargo audit is recommended but not strictly enforced at the PR gate.
Rule: Cargo.lock must always be committed for all projects, including libraries. This ensures reproducible builds and makes dependency changes fully visible in code review.
Current practice: Cargo.lock is committed in all repositories.
Rule: Cloud-based AI coding agents must not be given access to files or terminal sessions containing production secrets. Deployment credentials must be handled in a context the AI agent cannot see.
Background: Cloud AI agents (GitHub Copilot, Cursor, Windsurf, etc.) process code and terminal context on remote servers. Any secret the AI agent can see is transmitted to the AI provider's infrastructure. Infrastructure tools like the deployer necessarily handle sensitive data — cloud provider API tokens, database credentials, SSH private keys. If you use a cloud AI agent while working with deployment configuration, those secrets are exposed to the AI provider. There is no technical solution that allows a cloud AI to help with secret-containing configurations while keeping those secrets from the AI provider. These goals are mutually exclusive.
Mitigation options:
| Option | Approach | Trade-off |
|---|---|---|
| Local AI models | Run inference locally (Ollama, llama.cpp, LM Studio, Continue.dev with local backend) | Generally less capable than cloud models |
| Vault-based secrets | Store secrets in a dedicated secrets manager; configuration files contain only vault:path/to/secret references resolved at runtime, outside the AI's view |
Requires additional infrastructure; secrets never appear in files the AI reads |
| Separate contexts | Keep separate terminal sessions — one for AI-assisted coding (no secrets), one for secret-containing operations | Discipline-based; no technical enforcement |
Full guidance for deployer users: docs/security/user-security/ai-agents-and-secrets.md.
Rule: All active repositories must configure automated dependency update PRs for both CI workflow actions and language-level dependencies. New dependency PRs must go through normal code review before merging.
Current implementation: .github/dependabot.yaml is configured in all repos, covering github-actions and cargo ecosystems, targeting the develop branch on a daily schedule. Projects with container images or Node.js dependencies add those ecosystems to the same file. Example configuration from torrust-tracker:
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
target-branch: "develop"
labels:
- "Continuous Integration"
- "Dependencies"
- package-ecosystem: cargo
directory: /
schedule:
interval: daily
target-branch: "develop"
labels:
- "Build | Project System"
- "Dependencies"For projects that include container images or Node.js dependencies, additional ecosystems are added to the same file.
Rule: Only explicitly approved GitHub Actions may be used in any repository workflow. Adding a new action is a deliberate, reviewed decision at the organisation level — individual repository maintainers cannot introduce new actions unilaterally.
Current allowed actions:
| Action | Notes |
|---|---|
EndBug/label-sync@da00f2c11fdb78e4fae44adac2fdd713778ea3e8 |
Label synchronisation (pinned to commit SHA) |
actions-rust-lang/setup-rust-toolchain@v1 |
Rust toolchain setup |
alekitto/grcov@v0.2 |
Code coverage (grcov) |
dtolnay/rust-toolchain@nightly |
Rust nightly toolchain |
dtolnay/rust-toolchain@stable |
Rust stable toolchain |
endbug/export-label-config@v1 |
Export label configuration |
peter-evans/create-pull-request@v5 |
Automated PR creation |
rustsec/audit-check@v2.0.0 |
cargo audit in CI (pinned to version) |
smorimoto/tune-github-hosted-runner-network@v1 |
Runner network tuning |
stefanzweifel/git-auto-commit-action@v4 |
Auto-commit changes in CI |
swatinem/rust-cache@v2 |
Rust build caching |
taiki-e/install-action@v2 |
Tool installer for CI |
Note: Where possible, actions are pinned to a specific version tag or commit SHA rather than a mutable
@latestreference, to prevent unexpected upstream changes from affecting workflows.
Rule: All CI and security configuration files must require explicit maintainer approval before they can be merged. Changes to CI pipelines, security tooling, or branch protection settings must not be mergeable by contributors alone.
Current implementation: Every repository defines .github/CODEOWNERS with at minimum:
/.github/**/* @torrust/maintainers
This covers all CI workflows, Dependabot config, and the CODEOWNERS file itself. Individual repositories may add additional rules for security-sensitive paths (e.g., cryptographic code, authentication modules).
Rule: GitHub API tokens must be fine-grained (not classic), scoped to the minimum permissions required, set to expire within 90 days, and named descriptively to make auditing and revocation straightforward. Classic tokens with broad scopes and no expiry are prohibited.
Example naming convention: torrust-tracker-deployer-ci-2026-03 — encodes the target repository, purpose, and creation date at a glance.
Rule: The following GitHub security features must be enabled on all active repositories.
| Feature | Purpose |
|---|---|
| Organisation MFA enforcement | All organisation members must have 2FA enabled; GitHub enforces this at the org level, blocking access for any account without 2FA |
| Branch protection rules | Require PR review and passing CI before merging to main / develop |
| First-time contributor approval | CI workflows do not run on PRs from first-time contributors until a maintainer approves, preventing CI abuse |
| Secret scanning | Detects accidentally committed secrets (API keys, tokens, credentials) |
| Private vulnerability reporting | Allows security researchers to report issues privately without opening a public issue |
| Code scanning (CodeQL) | Static analysis for common vulnerability patterns |
Rule: Direct pushes to main and develop are prohibited. All changes must go through a pull request, receive at least one maintainer approval, and pass the required CI check suite.
Rule: Reviewers must acknowledge the exact commit hash they reviewed by posting ACK <commit-hash> on the PR. A general "looks good" is not sufficient. If new commits are pushed after an ACK, reviewers must re-ACK the updated hash.
Rule: All merge commits into protected branches must be GPG-signed by the maintainer performing the merge, providing a cryptographic record of who merged and when.
Current implementation: Merges are performed using a local script derived from Bitcoin Core's github-merge.py. The script: fetches the PR branch, constructs a standardised merge commit message (Merge #<PR>: <title>), presents the full diff for final manual inspection, optionally runs a configured test command, and GPG-signs the commit before pushing.
Planned: The script is currently written in Python. A rewrite in Rust is planned to align with the organisation's primary language.
Rule: Every CI run must include static analysis covering Rust code safety, shell script safety, and CI workflow correctness. This must run on every PR without exception.
Current implementation: The Torrust Linter (torrust-linting) is installed from source and runs linter all on every PR:
cargo install --locked --git https://github.com/torrust/torrust-linting --bin linter| Linter | Tool | Security relevance |
|---|---|---|
clippy |
cargo clippy |
Catches Rust-specific bugs and unsafe patterns |
shellcheck |
ShellCheck | Detects bugs and vulnerabilities in shell scripts (.sh, .bash); skips .git/ and .terraform/ directories |
rustfmt |
cargo fmt |
Enforces uniform formatting (reduces diff noise in reviews) |
markdown |
markdownlint | Consistent documentation quality |
cspell |
cspell | Spell checking |
toml |
toml linter | Validates TOML config files |
yaml |
yaml linter | Validates YAML files including CI workflows |
hadolint |
hadolint | Lints Containerfiles for unsafe patterns: missing pipefail, unpinned apt-get package versions, leftover apt lists, unnecessary layers |
Planned:
hadolintis not yet integrated into the Torrust Linter. Adding it to the container workflow is tracked in torrust-tracker#1460.
ShellCheck and Clippy are the most directly security-relevant: ShellCheck catches injection risks and unsafe shell patterns; Clippy flags Rust code that may indicate bugs or safety issues.
Rule: All Rust projects must run cargo audit against the RustSec advisory database on a daily schedule, independent of code changes. New advisories are published continuously and a project can become vulnerable without any code change.
Current implementation: cargo-security-audit.yml uses rustsec/audit-check@v2.0.0. It creates GitHub check runs and can open issues automatically for new advisories. Triggers: push to main/develop when Cargo.toml or Cargo.lock changes, every PR touching dependency files, daily at 06:00 UTC, and manual dispatch.
Rollout planned: Currently live in
torrust-tracker-deployer. Rollout totorrust-tracker,torrust-index,torrust-website, andbittorrent-primitivesis planned.
Rule: All Docker images — both project-built and third-party — must be scanned for HIGH and CRITICAL CVEs on a daily schedule. Findings that cannot be immediately fixed must be explicitly documented as accepted risks with a re-evaluation trigger, not silently ignored.
Current implementation: docker-security-scan.yml uses Trivy. The workflow runs on push (when docker/, src/, or workflow files change), on every PR, and daily. Two categories of images are scanned:
- Project-built images — built from
Containerfiles in the repo. Each image is built locally first so Trivy scans exactly what the repo produces. Results are uploaded to GitHub Security as SARIF. - Third-party images — the list is extracted dynamically from the deployer CLI at scan time, so it always reflects the currently configured versions without manual maintenance.
Scan behaviour: only HIGH and CRITICAL findings are reported; the scan step always exits with code 0 so findings do not block CI directly — enforcement is via the GitHub Security UI; results are uploaded as SARIF artefacts (retained 30 days) and to GitHub Code Scanning with per-image categories.
Rollout planned: Currently live in
torrust-tracker-deployer. Rollout totorrust-trackerandtorrust-indexis planned.
Accepted risk documentation: When a CVE cannot be remediated because no upstream fix is available or the vulnerable component is not reachable in our deployment context, the finding is explicitly documented rather than silently ignored. Each accepted risk entry records: the CVE identifier and affected package, why no fix is available (e.g., Debian <no-dsa>, will_not_fix upstream status, component not used at runtime), and a re-evaluation trigger. Accepted risks are tracked in per-image scan history files and linked to the corresponding GitHub issue.
Scan history: Scan results are persisted as Markdown files in the repository under docs/security/ alongside the code they cover. Each file records the image version, scanner output summary, and remediation actions. This creates a durable audit trail independent of ephemeral CI artefacts. Current production image scan status: docs/security/production/scans/.
Rule: Security-critical Rust codebases must run CodeQL Advanced analysis on a weekly schedule in addition to on every PR, because some vulnerability classes only emerge through deeper analysis that would be too slow to run on every commit.
Current implementation: codeql.yml scans two languages: rust (source code) and actions (GitHub Actions workflow files for misconfigurations and injection risks). Triggers: push and PRs to main, weekly on Fridays at 23:24 UTC. Results appear in the GitHub Security tab.
Rollout planned: Currently live in
torrust-tracker-deployer. Rollout totorrust-tracker,torrust-index, andtorrust-websiteis planned.
Rule: CI secrets must be scoped to specific environments and must not be accessible from fork PRs.
Current implementation: All secrets (Docker Hub credentials, API tokens, Codecov token) are stored as GitHub Actions secrets, never hardcoded in workflow files. They are scoped to specific environments (e.g., dockerhub-torrust, coverage). Pull requests from forks do not have access to repository secrets by default.
Rule: Expensive CI workflows must not run on documentation-only changes. The policy must be explicit and consistently applied across all repos so contributors receive predictable feedback.
Current implementation: Heavy workflows (testing.yaml, container.yaml, os-compatibility.yaml) skip execution when every changed file matches documentation paths (**/*.md, project-words.txt). Mixed PRs always run the full suite.
The following checks run automatically on every pull request. torrust-tracker is the canonical example; other active repos follow the same pattern.
Docs lint (all PRs): Markdown lint and spell check always run, even on documentation-only PRs.
Testing (all PRs touching code): Two jobs in parallel.
Unit job (matrix: nightly + stable toolchains):
| Step | Toolchain | Notes |
|---|---|---|
cargo fmt --check |
nightly only | Enforces uniform code formatting |
linter all |
both | Torrust Linter — clippy, shellcheck, and custom rules |
cargo test --doc |
both | All documentation examples compile and pass |
cargo test --tests --benches --examples --workspace --all-targets --all-features |
both | Full unit and integration test suite |
Docker E2E job: Builds the tracker Docker image; runs internal E2E tests and qBittorrent E2E tests against SQLite3, MySQL, and PostgreSQL.
Container build (PRs targeting develop or main): Builds for debug and release targets to verify the Containerfile is valid. Images are only published to Docker Hub on push to main, develop, or releases/** — never from a PR.
Production environments are managed via torrust-tracker-deployer using Ansible and OpenTofu. The deployer repository maintains its own structured security documentation under docs/security/. This section summarises the key practices.
Rule: Every provisioned server must have automatic OS security updates enabled and a firewall configured from first boot. Manual patching alone is insufficient.
Current implementation: Ansible installs and enables unattended-upgrades on every server, ensuring OS-level CVE patches are applied without manual intervention. UFW is configured to allow only the required ports (SSH, HTTP, HTTPS). Because Docker bypasses UFW's INPUT rules by default, a Docker + UFW co-existence strategy is applied so container-published ports are also governed by firewall rules.
Rule: Secrets must be injected at runtime, not baked into images or committed to version control. File permissions on configuration files must be set to the application user, not root.
Current implementation:
- Environment variables for secrets (database passwords, API tokens, registry credentials) are injected via
.envfiles at runtime and are never in images or version control. .envanddocker-compose.ymlfiles deployed by Ansible are owned byansible_user, notroot:root. (A previous bug where Ansible deployed these as root was fixed in #313.)
Rule: Internal services must not be reachable from the host network interface or the public internet. Only the application's public endpoints (HTTP/HTTPS) should be exposed.
Current implementation: Database services (MySQL) and internal components use Docker's expose: directive for intra-container communication, not ports:. This means database ports (e.g., 3306) are not reachable from outside the Docker network. HTTPS is enforced for all public endpoints via Caddy with Let's Encrypt certificates.
Rule: SSH access to production servers must use key-based authentication only. Password authentication must be disabled. The application must run as a non-root user.
Current implementation on Hetzner Cloud: Two independent mechanisms inject the same SSH public key at provisioning time: OpenTofu's hcloud_ssh_key (applied at server creation, grants root access for emergency debugging if cloud-init fails) and cloud-init's ssh_authorized_keys (applied on first boot, grants access as the torrust application user). See docs/security/user-security/ssh-root-access-hetzner.md for the rationale.
For stricter deployments, root SSH access can be removed once the deployment is verified:
# Remove root's authorized keys (torrust user and sudo access remain intact)
ssh torrust@<server-ip> "sudo rm /root/.ssh/authorized_keys"Rule: Production-deployed container images must be pinned to specific version tags (never latest), scanned for HIGH and CRITICAL CVEs before deployment and on a recurring schedule, and updated promptly when patched upstream versions are released.
Current implementation: Image versions are pinned in templates/docker-compose/docker-compose.yml.tera. Official vendor images are preferred (prom/prometheus, grafana/grafana, mysql). Trivy scans run daily via CI. See Container Image Scanning for the full scanning and accepted-risk process.
Rule: Container images must use the smallest available base image to minimise the attack surface. Every additional package included in the base image is a potential vulnerability vector.
Current implementation: Project images currently use docker.io/library/rust:trixie as the build base. Migration to the slimmer rust:slim-trixie variant is under investigation (torrust-tracker#1463); compatibility with the build toolchain must be verified before switching.
Planned: Docker image signing with cosign is planned (torrust-tracker#1461). The approach: generate a cosign key pair, store the private key as a GitHub Actions secret, and sign each image immediately after it is published in the release workflow. Crate publishing to crates.io relies on crates.io's own ownership and token controls; signed release artefacts are not yet produced.
Rule: Production environments must have centralised logging and metrics to support incident detection and investigation.
Current implementation: Prometheus and Grafana are deployed alongside the tracker stack. Logs are retained for incident investigation. Anomalous conditions are visible in dashboards.
Rule: Security vulnerabilities must not be reported as public GitHub issues. Reporters must use a private channel to allow remediation before public disclosure.
Current implementation: GitHub Private Vulnerability Reporting is enabled on all repositories (available under Security → Report a vulnerability). Alternatively, maintainers can be contacted directly — see SECURITY.md in each repository.
When a vulnerability report is received:
- Acknowledge — within a few days where possible.
- Assess — confirm the issue, evaluate severity (CVSS v3.1 or equivalent), and determine affected versions.
- Remediate — develop and test a fix in a private branch if necessary.
- Release — publish a patched release and update changelogs.
- Disclose — publish a security advisory on GitHub after the fix is available, crediting the reporter where appropriate.
We broadly follow the CVSS v3.1 scoring guide:
| Severity | CVSS Score |
|---|---|
| Critical | 9.0 – 10.0 |
| High | 7.0 – 8.9 |
| Medium | 4.0 – 6.9 |
| Low | 0.1 – 3.9 |
Critical and High findings are prioritised for immediate attention. Medium and Low findings are tracked and addressed in the normal release cycle.
Torrust currently operates demo environments only — there is no persistent user data at risk and environments can be reset at any time. If a demo environment is compromised, the response is:
- Open a public GitHub issue documenting the incident immediately.
- Reset and redeploy the affected demo environment.
- Investigate root cause and address the underlying vulnerability before redeploying.
This lightweight approach is appropriate for demo-only deployments. A formal incident response plan will be introduced if and when persistent production environments with user data are deployed.
Rule: AI tools complement but do not replace human review. Any AI-generated security finding or report must be reviewed and independently verified by a maintainer before any action is taken.
Current practices:
- Code review — AI assistants (e.g., GitHub Copilot) are used during development to catch potential issues early.
- Security report generation — AI tools are occasionally used to generate structured security assessments of specific components or configurations. These are treated as a starting point only.
- Triage assistance — when a security issue is reported, AI tools help quickly summarise, classify, and look up related advisories.
We receive security reports generated (fully or partially) by AI tools. Our approach:
- Read carefully — AI-generated reports are often verbose or include generic findings. Read the full report before acting.
- Verify independently — every reported vulnerability is reproduced or confirmed against the actual codebase before being treated as a real issue.
- Assess severity and context — evaluate whether the reported issue is actually exploitable in our specific deployment context.
- De-duplicate — AI tools sometimes surface the same advisory multiple times with slightly different framing. Consolidate before triaging.
- Acknowledge the reporter — acknowledge all reports, AI-assisted or not, and communicate the assessment clearly.
AI-generated reports are most useful when they include specific file/line references, a proof-of-concept, or a clear explanation of the attack vector. Generic or overly broad reports require significantly more investigation time.
If you see something missing or incorrect in this document, please open an issue or pull request in this repository.