-
Notifications
You must be signed in to change notification settings - Fork 0
Network and Security
GamesDownloader ships with a layered set of access controls that live under the admin Settings → Security tab, plus a few network-related settings that decide who can reach the instance and how the app identifies remote clients. This page documents each control, what it protects against, and how the defaults behave.
Design intent. GamesDownloader is built to run LAN-only, with no reverse proxy, and that mode must keep working out of the box. On a trusted home network most of the hardening below is dormant by design. Securing the LAN itself (router, firewall, VLANs, physical access) is the administrator's responsibility. The controls on this page exist mainly to prevent a compromise when the instance is deliberately exposed to the public internet. If you plan to publish the app, read this page together with Reverse Proxy & HTTPS and Security.
- The LAN-first model
- Brute-force protection
- Safe real-IP extraction
- IP allowlist
- Trusted proxies and CORS
- Public base URL
- Registration modes and invite codes
- Two-factor authentication
- Session management
- Security response headers
- Download tokens
- ClamAV antivirus
Everything here is optional and changeable at runtime from Settings → Security (the network block also feeds a few values used across the whole app). None of it needs to be enabled for a private LAN deployment. When you move the instance online, work down the list: enable brute-force protection (on by default), lock down registration, set the public base URL, add an IP allowlist if you have a fixed set of clients, and put HTTPS in front of the app.

Settings → Security: brute-force protection
Login attempts are rate-limited per client IP using a Redis fixed-window counter. After too many failures inside the window, the IP is banned for a configurable duration. The gate is applied to the sensitive authentication endpoints (login, registration, token refresh, and HTTP Basic auth), so it cannot be sidestepped by hammering a different entry point.
Configure it under Settings → Security → Brute-force Protection:
| Setting | Config key | Default | Meaning |
|---|---|---|---|
| Enable protection | bf_enabled |
on | Master toggle. When off, no counting or banning happens. |
| Max attempts | bf_max_attempts |
5 | Failed logins within the window before a ban is issued. |
| Time window | bf_window_seconds |
300 (5 min) | Rolling window in which failures are counted. |
| Ban duration | bf_ban_seconds |
900 (15 min) | How long a banned IP stays blocked. |
| Whitelist | bf_whitelist |
empty | Comma-separated IPs that are never counted or banned. |
Behaviour details:
- Loopback (
127.0.0.1) and any whitelisted IP are always exempt. - A successful login clears the failure counter for that IP.
- The counter is incremented atomically (Redis Lua) so concurrent requests cannot leave a key without a TTL.
- When a ban is issued, an optional email alert (
brute_force) can be sent (see Email & Notifications).
Banned IPs are listed live in the same section with the remaining ban seconds and an Unban button, so an admin who locks themselves out from another machine can clear a ban immediately.
Warning. When verifying brute-force behaviour against a live instance, do not trigger 5 or more failed logins from your own address or you will ban (lock out) yourself for the full ban duration.
Both the brute-force ban and the IP allowlist need the real client IP, but forwarded headers can be forged. GamesDownloader trusts the forwarded headers only when the direct TCP peer is itself trusted, meaning it is a private/LAN address (the normal "app sits behind a LAN reverse proxy" case) or an address you explicitly listed under Trusted proxies.
When the peer is trusted, the real IP is taken from the first header present, in this order:
-
CF-Connecting-IP(Cloudflare) -
X-Real-IP(nginx / HAProxy / Traefik single hop) -
X-Forwarded-For(leftmost entry)
When the peer is not trusted, all of those headers are ignored and the raw TCP connection address is used instead. This means a client connecting directly from an untrusted address cannot rotate a spoofed X-Forwarded-For to dodge a ban or slip past the allowlist.
Docker caveat. If the app port is published directly on the LAN and the container sits behind Docker's userland proxy, every source can appear as the private bridge gateway, so the peer always looks trusted. When you expose the app, restrict the app port at the network layer (firewall, or bind it only to the reverse proxy host) rather than relying on header trust alone.
An optional global gate that rejects any request whose real client IP is not in an allowed set. Configure it under Settings → Security → Network & Access:
-
Enable IP allowlist (
ip_allowlist_enabled, default off). -
Allowed IPs (
ip_allowlist) accepts a comma-separated mix of single addresses and CIDR ranges, for example192.168.0.0/24, 10.0.0.5.
The allowlist uses the same safe real-IP extraction, so it is Cloudflare-aware and cannot be bypassed by header spoofing from an untrusted peer.
Caution. Enabling the allowlist without including your own address will lock everyone out. Add your management subnet first, then enable it.

Network & Access: trusted proxies, IP allowlist, CORS, and public base URL
Two more fields in the same Network & Access block control how the app treats reverse proxies and cross-origin browser requests:
| Setting | Config key | Format | Notes |
|---|---|---|---|
| Trusted proxies | trusted_proxies |
comma-separated IPs / CIDRs | Peers allowed to supply the forwarded real-IP headers above. Applies immediately, no restart. |
| CORS origins | cors_origins |
comma-separated origins, or *
|
Browser origins allowed to call the API. Takes effect after a container restart. |
Trusted proxies and the IP allowlist are read per request, so changing them takes effect at once. CORS origins are applied at startup, so the app reminds you that a container restart is needed after changing them.
public_base_url (for example https://games.example.com) is the address users actually reach the server at. It is used to build absolute links in outgoing email, most importantly password-reset links.
Setting it is an anti-poisoning measure: without a configured base URL the app falls back to the request's Host / X-Forwarded-Host headers, which an attacker could spoof to make a valid reset token point at their own domain. With public_base_url set, reset links always use the trusted address. Leave it empty to derive the URL from the request (the historical behaviour), or set it whenever the instance is reachable over the internet.
Self-service account creation is governed by a single mode (registration_mode) under Settings → Security → Network & Access:
| Mode | Behaviour |
|---|---|
| Open | Anyone can register a new account. |
| Disabled | Registration is closed. This is the safe default when nothing has been configured. |
| Invite-only | New accounts require a valid invite code. |
Invite codes are created and managed under Settings → Users. Each code supports a maximum number of uses, an optional expiry (in hours; omit for a code that never expires), and a free-text note, so you can hand out time-limited, single-use invitations. See Users & Permissions for role assignment and per-user controls.

Two-factor authentication setup
Users can enable TOTP two-factor authentication (RFC 6238) on their own account from Profile. The flow is standard authenticator-app enrolment:
- A base32 secret is generated and shown as a scannable QR code (issuer name
GamesDownloader), compatible with Google Authenticator, Authy, 1Password, and similar apps. - The candidate secret is held in a short-lived Redis buffer (10 minutes) and is only written to the user record once the user proves setup by entering a valid 6-digit code, so a half-finished enrolment never persists a live secret.
- Enrolment also produces one-time recovery codes. They are shown once and stored only as bcrypt hashes, so a database leak does not yield working backup keys. Each recovery code can be spent exactly once.
- Code verification accepts a one-step clock-drift window (the previous and next 30-second step).
Authentication uses JWTs with refresh tokens and supports instant revocation:
- Session lifetime ("stay logged in" duration) is chosen with the chips under Settings → Security → Session Lifetime.
- Every active session is visible in the admin Users panel, where an admin can revoke a single session or revoke all sessions for a user at once.
- Revocation is enforced through a Redis JTI blocklist, with a database fallback so revoked tokens stay rejected even if Redis restarts.
A middleware adds defensive HTTP headers to every response automatically. There is nothing to configure; the notable point for self-hosters is that the headers are careful not to break a plain-HTTP LAN deployment.
| Header | Value | Purpose |
|---|---|---|
X-Content-Type-Options |
nosniff |
Stops MIME-type sniffing. |
X-Frame-Options |
SAMEORIGIN |
Blocks cross-origin framing (clickjacking). |
Referrer-Policy |
strict-origin-when-cross-origin |
No path/query leakage to other origins. |
X-XSS-Protection |
1; mode=block |
Legacy reflected-XSS filter for old browsers. |
Content-Security-Policy |
see below | Locks down what the page may load and execute. |
Strict-Transport-Security |
max-age=31536000 |
Sent only when the connection is HTTPS. |
Key points:
- The CSP locks
script-srcto'self'(no'unsafe-inline'/'unsafe-eval'), so an injected<script>from a game title, description, username, or scraper field cannot run.style-srckeeps'unsafe-inline'on purpose because theme plugins inject inline styles and are installed knowingly by the admin.img-srcdrops plainhttp:(all scraper media is served locally),connect-srcis'self'only, andframe-srcallows only YouTube for trailer embeds. - The
/playerand/emulatorjsroutes are exempt from the strict CSP and instead sendCross-Origin-Opener-Policy/Cross-Origin-Embedder-Policy, because EmulatorJS needs unrestricted JS andSharedArrayBuffer. See ROMs & Emulation. -
HSTS is sent only when the request is genuinely HTTPS, detected via
X-Forwarded-Proto. A plain-HTTP self-hosted deployment never receives it, so browsers will not be pinned to HTTPS for a LAN address that has no certificate.
Download links can be gated behind short-lived, per-download tokens so that a raw file URL cannot be shared or reused indefinitely. This is configured on its own page; see Downloads & Torrents for the token settings and how they interact with the security headers above.
ClamAV runs in the same container and is managed from Settings → Security → ClamAV Antivirus. It provides:
- A daemon status card (running state, engine version, virus-database version and date) with a refresh button.
- Action on infected file: Report only, Quarantine (recommended), or Delete permanently.
- Automatic scans on upload and after a GOG download, plus a master enable toggle.
-
Virus-definition updates via
freshclam, either on demand or on a configurable auto-update interval (hours). - Manual scans of selected folders with live progress, a scan history table, and a quarantine view where infected files can be restored to their original location or deleted.
Note. Manual and folder scans require the ClamAV daemon to be running; the UI disables the scan button and warns you when it is not.
Next: Reverse Proxy & HTTPS - Security
Getting Started
Configuration
Features
- Dashboard
- Library
- Collections
- Game Requests
- GOG Integration
- ROMs & Emulation
- Downloads & Torrents
- Users & Permissions
- Themes
Plugin Development
Reference