Skip to content

Security Overview ​

Trackarr is built with security as a foundational principle, not an afterthought. This page provides an overview of the security architecture.

Security Layers ​

LayerProtection
AuthenticationZero-Knowledge proofs, PoW anti-abuse, session encryption, CSRF protection
DatabaseSCRAM-SHA-256 auth, optional TLS (DB_SSL / database.sslMode — off by default on the internal network), parameter binding, connection pool limits
RedisPassword auth, command restrictions, memory limits
NetworkRate limiting, auto IP bans, attack pattern detection
PrivacySHA-256 hashed IPs, no raw IP persistence, minimal logging

Rate Limits ​

Trackarr implements distributed rate limiting to prevent abuse:

EndpointLimitAction on Abuse
Public API100/min100+ req/10s → auto-block
Mutations10/minProgressive penalties
Password sign-in10/5min per address429, nothing more
Sign-in, per account10 failures/15min per username429 for that username, except on its known browsers
Sign-in, per known browser10 failures/15minThat browser is no longer known
Other auth (register, 2FA, password change)5/5minIP blacklisted after violations
Tracker200/minDistributed sliding window

Every counter above but the per-account one is keyed on the client IP — its /64 for IPv6, the smallest block a subscriber is handed, since one host can pick a fresh address in it for every request. That requires TRUST_PROXY=true behind a reverse proxy — see Configuration. The one exception is the web container's own server-side rendering when nothing sits in front of it: those calls carry no forwarded-client header and arrive from a private address, so they are excluded from the 100 req/10s abuse counter and from the temporary blacklist it feeds. Without that exclusion the whole site shares a single counter — measured at four API calls per page view, which means about twenty-five page views in ten seconds would blacklist the instance for everyone. Behind the shipped Caddy the server-side calls inherit the visitor's X-Forwarded-For, so they are counted on the visitor's own address and the exclusion does not apply. Nothing else is relaxed for them: user-agent filtering, path and parameter validation, IP bans, per-route limits and authentication all still apply.

IP Privacy ​

User IP addresses are never stored in plaintext. Instead:

  1. IPs are hashed using SHA-256 with a secret salt
  2. Only the hash is stored for rate limiting and abuse detection
  3. Hashes cannot be reversed to obtain the original IP
  4. Logs are minimal and do not contain identifying information

Attack Detection ​

The system automatically detects and blocks:

  • SQL injection attempts
  • XSS (Cross-Site Scripting) attacks
  • Path traversal attempts
  • Brute force authentication attempts
  • Automated scraping and enumeration

Learn More ​


Login history ​

Every attempt to open a session is recorded — successful or not — with the method that was used, the browser, and the address it came from.

Members read their own at Settings → Security → Login history, including the client column: the member is the one person who knows which devices they own, so "Chrome on Windows" against an iPhone-and-Mac life is the strongest signal they have that it was not them. Staff read any member's from that member's profile page.

Failures are the interesting half ​

Password sign-in is held back two ways. Per address, ten attempts in five minutes, after which that address waits — and nothing more: sign-in used to share the strict auth bucket, whose sixth failure blacklisted the whole address, so on a VPN exit or a carrier NAT one stranger mistyping a password shut every member behind it out of the site. Per account, ten failures in fifteen minutes hold that username out of password sign-in for the rest of the window, from every address — the answer a guess spread over many addresses used to meet nowhere. Failures are counted for every username, existing or not, so a lockout never tells whether an account exists. Its open sessions are untouched.

That lockout spares the member's own browsers. A browser that completes a sign-in — the password, and the second factor if the account has one — or a registration receives a known-device cookie (trackarr_kd, kept six months). On that browser the account's count does not apply: the browser answers to a count of its own, and ten failures there revoke its cookie for good, after which it is a browser like any other. Someone failing on purpose can still shut an account to new devices for the window, but no longer to the ones its member already uses — where before, one failure every ninety seconds kept the account shut everywhere, the right password included. Guessing gains nothing: a guesser never holds a valid cookie.

The cookie skips nothing — not the password, not the second factor, not the per-address limit — which sets it apart from the opt-in Trust this browser cookie of two-factor authentication. It is stateless, an HMAC over the account, its session epoch and its stored verifier, so Sign out everywhere, a ban or a password change voids every one at once; Redis keeps only a failure count per device and the ids revoked. A successful sign-in from a browser that is not known clears the account's count; one from a known browser leaves it, since those failures came from somewhere else.

Every refusal of credentials takes at least 300 ms, whatever its cause. A username with no account and a wrong password already got the same status and message, but not the same work — the second reads the account and hashes — and the few milliseconds between them told which usernames exist.

This table is what makes an attempt visible afterwards — a run of refusals against one account, from addresses that differ, is a shape worth recognising.

What the address column can and cannot tell you ​

Neither view shows a hash. The address is stored hashed, through the same daily-rotating salt as every other IP in this system, and is presented as an ordinal — #1, #2 — numbered within its own day.

Numbered within the day and not across the table, because the alternative was actively misleading: a single counter over the whole response rendered one home address on five different days as #1 #2 #3 #4 #5, so the ordinary case — one place, many days — looked exactly like five different places, on the one screen whose entire purpose is spotting somebody else. Per day, the same place is #1 every day, and a second number inside one day means a second place that day.

The daily salt is why cross-day comparison is not offered at all: two hashes a week apart may well be the same address. That is enough for the question staff actually ask — "is this account being used from several places right now", the shape of account sharing on an invite-only tracker — and not enough for a long-range history.

An ordinal rather than the hash itself for a second reason: the same hash handed to two readers is a confirmation oracle for a day. Somebody who suspects an account is shared with a person they can reach could sign in from that address, read their own hash, and compare. The ordinal keeps the only property either view claims and gives up the only one neither needs.

The moderator view flags a day with more than two distinct addresses, and names the day and the number of sign-ins it covers — at a threshold of two, a phone plus a laptop tripped a warning-coloured panel on the median member, which teaches the reader to ignore it.

Retention ​

login_event_retention_days — default 90, 0 keeps them indefinitely. Shorter than the audit log's year because this is a high-volume table and the questions it answers are about the recent past. The period is published on the public /privacy page.

Released under the MIT License.