Operational security notes for Dash AdminLTE. Keep secrets out of Git; prefer environment variables (see .env.example).
Passwords are hashed with Werkzeug scrypt. Plaintext passwords are never stored or written to logs.
Login always performs a real password-hash comparison (including a fixed dummy hash when the username does not exist) to reduce username-enumeration timing differences.
CAPTCHA is generated and validated locally with Pillow. No third-party CAPTCHA service is required.
The Flask session stores an HMAC digest of the answer (and timing metadata), not the plaintext answer. Challenges expire and are single-use after validation. Images use a bundled font so rendering does not depend on OS font paths.
Role checks run server-side (core/02_security.py, page callbacks in app.py). Hiding a sidebar item is never treated as authorization.
Privilege order:
superadmin > admin > user > guest
has_role() compares levels hierarchically. Each page may declare required_role in its sidebar_item; render and navigation callbacks both enforce it.
Disabled or expired accounts cannot authenticate. Active sessions are cleared when those conditions are detected during session refresh.
Each account has an inactivity timeout (session_timeout, seconds). Checks run on protected operations and via a client session-watchdog interval.
The watchdog calls refresh_session(touch=False) so background polls do not reset last_activity. Real user actions use touch=True.
Watchdog interval is per-user (watchdog_interval, default 300 seconds). Global default: DEFAULT_WATCHDOG_INTERVAL. Minimum enforced interval: 30 seconds.
Each user has a theme_id stored in the database. Changing the theme while authenticated updates that field. On subsequent logins the saved theme is applied for that account.
Failed logins are limited in-process by (username, IP) and by IP alone (higher threshold). Multi-worker or multi-instance deployments should replace this with shared storage (for example Redis).
Production secrets must come from the environment (or a local .env that is never committed). Do not commit .env, production SQLite files, passwords, or session secrets.
SECRET_KEY is the server-side signing secret used by Flask/Dash. It is not a user password and must not appear in the UI or full logs.
It protects at least:
- Session cookie integrity — identity after login is carried in a signed cookie. A known or missing secret would allow forging sessions.
- CAPTCHA HMAC — challenge answers are verified via digests derived with this key. If the key differs across processes, digests will not verify consistently.
- Avoiding predictable defaults — a hard-coded fallback in source would put every misconfigured deployment at risk.
Resolved in core/00_config.py (_resolve_secret_key):
| Situation | Behavior |
|---|---|
| Unset, empty, or equal to the insecure placeholder | A random process-local key is generated and a RuntimeWarning is emitted |
| Set to a long random value | Used for the process lifetime; keep it stable across restarts |
This is a safe failure mode (better than a forgeable built-in key). Trade-offs: sessions do not survive restart; multiple workers need the same configured key.
Generate a value:
python -c "import secrets; print(secrets.token_urlsafe(48))"Then set SECRET_KEY in .env, the process environment, or Docker (-e / Compose). See also docker/README.md.
| Variable | Role |
|---|---|
SESSION_COOKIE_SECURE |
Send cookies only over HTTPS when true |
BOOTSTRAP_ADMIN_PASSWORD |
Optional initial superadmin password on first empty database |
DATABASE_PATH |
SQLite path — protect the file and backups separately |
SECRET_KEY does not encrypt the SQLite file.
Security-relevant events can be recorded in the audit_log table for later review.