Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# --- Secrets: MUST stay out of the image ---------------------------------
# The real .env holds live DB/LDAP credentials. It is injected at RUNTIME
# by docker-compose (env_file:), never baked into the image.
.env
*.env

# --- Build context bloat --------------------------------------------------
# build/ contains a pathological build/lib/build/lib/... nest ~6 levels deep.
# Uploading it to the Docker daemon slows every single build.
build/
dist/
*.egg-info/
.venv/
venv/
ENV/
__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.coverage
htmlcov/

# --- Not needed at runtime ------------------------------------------------
# NOTE: tests/ is deliberately NOT excluded. It is the built-in self-check:
# docker compose run --rm web python -m pytest -q
# It uses in-memory SQLite, so it proves the image works with no database.
.git/
.github/
docs/
site/
mkdocs.yml
*.log
.DS_Store
.vscode/
160 changes: 160 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# =============================================================================
# ethopy_control - environment template
#
# Copy this file to .env and fill in the real values:
# cp .env.example .env
#
# .env is gitignored and MUST NOT be committed. The real values are handed over
# separately (password manager / lab admin) - see DEPLOY.md.
# =============================================================================

# --- Flask -------------------------------------------------------------------
# Signs session cookies. Generate a fresh one per deployment with:
# python3 -c "import secrets; print(secrets.token_urlsafe(48))"
# If this changes, everyone is simply logged out. Use 32+ characters.
SECRET_KEY=change-me-generate-a-long-random-string

# production | development | testing.
# docker-compose.yml forces "production"; this value only matters if you run
# the app outside Docker.
FLASK_CONFIG=production

# --- Ports ---------------------------------------------------------------------
# There are TWO ports, and only one of them can ever clash with other software.
#
# HOST_PORT is the one you connect to, and the ONE TO CHANGE if 8000 is already
# taken on this machine (another app, an old gunicorn, a second copy of this
# one). Nothing else needs touching.
HOST_PORT=8000
#
# PORT is the port INSIDE the container. Leave it alone. Containers each get
# their own private network, so 8000 in there can never collide with anything,
# and it is hardcoded in the Dockerfile's gunicorn command and health check.
# docker-compose.yml pins it to 8000 and ignores whatever is set here.
# It is only read when running the app OUTSIDE Docker, via `python main.py`.
PORT=8000

# --- How it is served ---------------------------------------------------------
# Out of the box the app serves PLAIN HTTP, reachable from other machines on the
# local network at http://<this-machine-ip>:<HOST_PORT>
#
# There is no HTTPS and no certificate to manage. That keeps the setup small,
# but it means passwords cross the network in the clear, so this is only
# appropriate on a trusted local network.
#
# To publish it beyond the lab, put a reverse proxy in front of it or reach it
# over a VPN, and then set the two variables below. See DEPLOY.md section 6.

# --- Security -----------------------------------------------------------------
# Login throttling. Only FAILED attempts count, so normal users never consume
# their own quota.
#
# IMPORTANT: these numbers are PER GUNICORN WORKER.
#
# There is no shared counter store, so each of the 4 workers counts on its own
# and the effective allowance is roughly:
#
# configured value x number of workers = what actually gets through
# 4 per 15 minutes x 4 = about 16 per 15 minutes
#
# Set them against your directory's own lockout threshold. If it locks an
# account after very few failures, either lower these numbers or reduce
# --workers in the Dockerfile (fewer workers = smaller multiplier).
#
# PER IP: generous, because several lab members may share one NAT'd address.
LOGIN_RATE_LIMIT_IP=10 per minute; 60 per hour
#
# PER USERNAME: the important one. LDAP locks accounts individually, so an
# attacker rotating IP addresses could still lock one person out. This limit is
# keyed on the account being attempted, so it holds regardless of source.
LOGIN_RATE_LIMIT_USER=4 per 15 minutes; 10 per hour
#
# Optional: point this at a shared store to make the limits exact instead of
# approximate. Needs a Redis service and `pip install "flask-limiter[redis]"`.
# Left unset, counters stay in worker memory as described above.
# RATELIMIT_STORAGE_URI=redis://redis:6379/0

# Set to true ONLY once the app is served over HTTPS. When true the browser
# refuses to send the session cookie over plain HTTP, so turning it on too
# early makes login silently impossible.
#
# Leave false for the default local-network setup. Turn it on together with
# TRUST_PROXY_HEADERS when a reverse proxy terminates HTTPS in front.
SESSION_COOKIE_SECURE=false

# How long a login lasts before the user must sign in again.
SESSION_LIFETIME_HOURS=12

# Set to true ONLY when a reverse proxy (nginx, Caddy, a Cloudflare Tunnel, or
# an appliance that already does this) sits in front of the app. It makes rate limiting see the real client
# IP instead of the proxy's.
#
# Enabling it while the app is directly reachable lets any client forge that IP
# and skip the login rate limit entirely, so also firewall port 8000 to the
# proxy's address. See DEPLOY.md section 6.
TRUST_PROXY_HEADERS=false

# --- Database (REQUIRED) ------------------------------------------------------
# The existing lab MySQL server. This app does NOT create or host a database -
# it connects to the shared one the Raspberry Pi setups also write to.
#
# The DB user needs read/write on THREE schemas:
# - lab_experiments (this is DB_NAME below)
# - lab_behavior (hardcoded in real_time_plot/get_activity.py)
# - lab_interface (hardcoded in real_time_plot/get_activity.py)
# The tables `#control` and `#task` must already exist there.
#
# Get the real hostname from the lab database admin, or copy it from the .env
# on a machine that is already running the app.
DB_HOST=db.example.org
DB_PORT=3306
DB_NAME=lab_experiments
DB_USER=change-me
DB_PASSWORD=change-me

# --- SSH (REQUIRED, even if unused) -------------------------------------------
# Used ONLY by the "reboot" button, which SSHes into a setup's IP (from the
# `ip` column of the #control table) and runs `sudo reboot`.
#
# These two variables must be present or the app will not start at all -
# utils/config.py validates them at import time. If you do not use the reboot
# feature, leave the placeholders; the button will return a clear
# "SSH credentials not configured" error instead of crashing the app.
#
# For Raspberry Pi setups this is typically the `pi` user. The Pi must allow
# `sudo reboot` without a password prompt.
SSH_USERNAME=change-me
SSH_PASSWORD=change-me

# --- Authentication -----------------------------------------------------------
# The lab authenticates against an LDAP directory. Local auth is the fallback
# if LDAP is ever unavailable (it needs a `users` table and an admin account).
USE_LOCAL_AUTH=false
USE_LDAP_AUTH=true

# --- LDAP (required only when USE_LDAP_AUTH=true) -----------------------------
# Placeholders. The lab's real directory host and DN layout come from the
# handed-over .env or from the lab admin - they are deliberately not committed
# to this repository.
LDAP_HOST=ldap.example.org
LDAP_PORT=389
LDAP_USE_SSL=false
LDAP_BASE_DN=dc=example,dc=org
LDAP_USER_DN=ou=users
LDAP_GROUP_DN=ou=groups
# Leave both blank for anonymous bind (this is what the lab currently uses).
LDAP_BIND_USER_DN=
LDAP_BIND_USER_PASSWORD=
# NOTE: no spaces around "=" - Docker's env-file parser is stricter than
# python-dotenv and would read the key as "LDAP_SEARCH_FOR_GROUPS ".
LDAP_SEARCH_FOR_GROUPS=true

# --- Admin (only used when USE_LOCAL_AUTH=true) -------------------------------
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me

# --- DO NOT SET ---------------------------------------------------------------
# FLASK_ENV=development
# This is an authentication BYPASS (app.py:65-75) - any username and password
# combination is accepted. It is a different variable from FLASK_CONFIG and is
# easy to set out of habit. Never set it on a machine reachable by others.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,6 @@ ENV/
.mypy_cache/

# IDE settings
.vscode/
.vscode/
# Lab-specific notes: handed over directly, never committed (public repo)
private/
Loading
Loading