Files
ServerKit/.env.example
T
Juan DenisandClaude Opus 5 0bb7e4ab54 fix(install): derive the real client IP on a stock host install
install.sh wrote no TRUST_PROXY_HEADERS line at all, so the code default (False)
applied while every host install serves the panel through its own nginx. Per-IP
rate limits, the login brute-force throttle and every audit-log entry therefore
keyed on 127.0.0.1, which quietly weakened all three -- one abusive client shared
a bucket with everyone else. .env.example claimed true, so the two disagreed and
it looked configured.

Trusting X-Forwarded-For is only safe when nothing can reach gunicorn without
traversing a proxy we control, so the switch follows that condition rather than
assuming it. On a host install it holds: render_service_unit binds gunicorn to
127.0.0.1 and bootstrap_firewall opens SSH + 80/443 + PANEL_PORT and never the
backend port. backend_bind_host() is now the single source of truth for both the
systemd unit and the trust decision so the two cannot drift, and
backend_is_loopback_only() gates the setting.

- stock install: TRUST_PROXY_HEADERS=true, TRUSTED_PROXY_HOPS=1
- SERVERKIT_EXTERNAL_PROXY=1: true with 2 hops, unchanged
- SERVERKIT_BIND_HOST not loopback: false plus a warning. The port is directly
  reachable there, so the header is attacker-chosen, and a forged IP is worse
  than a wrong one because it defeats the throttles that consume it.

Re-runs backfill it into installs that predate this, via _env_default_key, which
fills a gap but never overrides an uncommented value -- flipping someone's
deliberate false back on would be the worst kind of surprise.

.env.example moves to false on purpose: that file is the Docker/manual path and
docker-compose.yml publishes 5000 on all interfaces, so true was the unsafe
default there. Host installs never read it. SECURITY.md claimed "the shipped
nginx deploy sets it", which was untrue until now.

T44 covers all six behaviours, each mutation-checked by line number: disabling
the branch, inverting it to trust an exposed port, and disabling the backfill
each fail exactly one assertion and no others.

Shell suite 121 passed. Backend suite 3542 passed, 48 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 23:52:55 -04:00

130 lines
5.4 KiB
Bash

# ============================================
# ServerKit Configuration
# ============================================
# Copy this file to .env and customize for your environment
# Generate secure keys with: python -c "import secrets; print(secrets.token_hex(32))"
# ============================================
# ----------------------------------------
# REQUIRED: Security Keys
# ----------------------------------------
# Generate unique keys for production!
SECRET_KEY=your-secret-key-here
JWT_SECRET_KEY=your-jwt-secret-key-here
# Fernet key for encrypting secrets at rest.
# Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
SERVERKIT_ENCRYPTION_KEY=your-fernet-key-here
# ----------------------------------------
# Database Configuration
# ----------------------------------------
# SQLite (default - good for small deployments).
# NOTE the path is relative to the backend working directory.
# DATABASE_URL=sqlite:///serverkit.db
# PostgreSQL (recommended for production):
# DATABASE_URL=postgresql://user:password@localhost:5432/serverkit
# MySQL:
# DATABASE_URL=mysql://user:password@localhost:3306/serverkit
# ── Under Docker, use SERVERKIT_DATABASE_URL instead ──
# docker-compose.yml pins DATABASE_URL to /app/instance/serverkit.db, the path
# backed by the persistent volume, and ignores the DATABASE_URL above. That is
# on purpose: a *relative* sqlite path resolves inside the container and is
# destroyed by the next `compose down`. To point Docker at another database,
# set this instead — it wins:
# SERVERKIT_DATABASE_URL=postgresql://user:password@db:5432/serverkit
# ----------------------------------------
# Network Configuration
# ----------------------------------------
# CORS Origins - your domain(s), comma-separated
CORS_ORIGINS=http://localhost,https://your-domain.com
# Public URL that agents should dial back to. Set this whenever the panel
# sits behind a reverse proxy / tunnel / custom domain — the connection
# string the panel issues to agents will embed this URL instead of the
# request's Host header (which would otherwise be the internal IP/port).
# Leave commented out for plain localhost development.
# SERVERKIT_PUBLIC_URL=https://panel.your-domain.com
# Port. Only the Flask *development* server reads this (`python run.py`).
# Under Docker the container always listens on 5000 and you publish it with
# SERVERKIT_HTTP_PORT (see docker-compose.yml); under gunicorn the port comes
# from SERVERKIT_BACKEND_PORT.
# PORT=5000
# ----------------------------------------
# Trusted reverse proxy (client IP)
# ----------------------------------------
# Derive the real client IP from X-Forwarded-For (via Werkzeug ProxyFix) for
# rate limiting, login lockout and audit logs, instead of logging every request
# as the proxy's address.
#
# Turn it on ONLY when a proxy you control is the sole route in. The header is
# whatever the caller typed, so if anything can open a connection to the backend
# port directly, this hands attackers the ability to forge the IP that those
# very throttles key on.
#
# Off by default HERE because this file is the Docker/manual path, and
# docker-compose.yml publishes port 5000 on all interfaces. Set it to true once
# you put a proxy in front AND stop direct access to 5000 — bind it to loopback
# (see docs/INSTALLATION.md → "Running behind your own reverse proxy") or
# firewall it.
#
# Host installs do not use this file: install.sh writes its own .env and enables
# this automatically, because there gunicorn binds loopback behind our nginx.
#
# TRUSTED_PROXY_HOPS = proxies in front of Flask. Your proxy alone = 1; your
# proxy plus ServerKit's nginx = 2; add Cloudflare on top and it goes up again.
TRUST_PROXY_HEADERS=false
TRUSTED_PROXY_HOPS=1
# ----------------------------------------
# Login brute-force throttle (per client IP)
# ----------------------------------------
# On top of the per-user account lockout, block a client IP after too many
# failed logins (defends against password-spraying across many usernames).
# Defaults shown; override only if you need to tune them.
# AUTH_IP_MAX_ATTEMPTS=10
# AUTH_IP_WINDOW_MINUTES=15
# AUTH_IP_BLOCK_MINUTES=15
# ----------------------------------------
# Environment
# ----------------------------------------
# Options: development, production
FLASK_ENV=production
# ----------------------------------------
# GitHub Repository (for agent downloads)
# ----------------------------------------
# This is used to fetch agent releases for the downloads page
SERVERKIT_GITHUB_REPO=jhd3197/ServerKit
# ----------------------------------------
# Theme Registry
# ----------------------------------------
# Curated theme index for the Browse gallery. Unset = the public registry
# (https://serverkit.ai/themes/index.json); set-but-empty = disabled (bundled
# themes only).
# SERVERKIT_THEMES_REGISTRY_URL=https://serverkit.ai/themes/index.json
# Seconds to cache the registry index before re-fetching (default 3600).
# SERVERKIT_THEMES_REGISTRY_TTL=3600
# ----------------------------------------
# Optional: Notification Webhooks
# ----------------------------------------
# These can also be configured via the web UI at Settings > Notifications
# Discord Webhook URL
# DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
# Slack Webhook URL
# SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...
# Telegram Bot
# TELEGRAM_BOT_TOKEN=your-bot-token
# TELEGRAM_CHAT_ID=your-chat-id