This holds your food-safety due-diligence records, so it is built to be safe by default rather than safe if you remember to configure it.
| Nothing is readable without signing in | Every page, the CSV exports and the live data API all require a password. There is no "just this one page is open" except where you explicitly turn it on |
| It will not start without a password | Set no password and the app exits with an explanation, rather than starting up wide open |
| Passwords are stored hashed | PBKDF2-SHA256, 600,000 iterations. The installer writes the hash; your actual password isn't in any file |
| Constant-time comparison | Passwords and tokens are compared in a way that doesn't leak how much of a guess was right |
| Lockout after repeated failures | Five wrong attempts, then an increasing lockout up to 15 minutes, per IP address. A shared password can't be guessed remotely |
| CSRF protection on every form | A malicious web page can't make your logged-in browser stop recording or change a limit. Session cookies are HttpOnly + SameSite=Lax, and marked Secure over HTTPS |
| Security headers set by the app | CSP, X-Frame-Options: DENY, nosniff, Referrer-Policy, and HSTS on HTTPS — so it's equally protected behind Plesk, nginx, Traefik or nothing |
| Token-authenticated sync | The on-site device authenticates with a 64-character bearer token, and refuses to send over plain http so the token never crosses the network in clear |
| The record can't be edited | There is no route that changes or deletes a reading. Signed-off records can't be re-signed |
| Two roles on the server | An admin can sign records off; a viewer can only read |
| Locked-down service | systemd runs it with NoNewPrivileges, ProtectSystem, PrivateTmp and write access to its own directory only |
Databases and .env files are 0600 |
Other users on the machine can't read your records or your secrets |
| No external anything | No CDNs, no fonts, no analytics, no telemetry. Nothing about your business leaves your kit unless you set up the optional server yourself |
Be clear-eyed about this.
- One shared password per role. There are no individual staff accounts, so the record shows that someone stopped recording, not who. Sign-off does capture a name, which is where it matters.
- Anyone with the password can stop recording or change future limits. Treat it like a till key. Give staff the viewer password on the server side, and keep the admin password to managers.
- The on-site app is HTTP on your local network. Fine on a private shop network; don't port-forward it to the internet. If you need remote access, use the server half, or a VPN such as Tailscale or WireGuard.
- Physical access beats everything. Someone who can pick up the Pi can read the SD card. Put it somewhere staff-only.
- It is not a tamper-proof legal instrument. Someone with server root can edit the database. It is a very good ordinary business record, not a sealed audit log.
On-site app:
cd ~/chillcheck
.venv/bin/python -c "import security,getpass; print(security.hash_password(getpass.getpass('New password: ')))"Copy the whole pbkdf2:sha256:... line it prints, then:
nano .env # replace the value after APP_PASSWORD_HASH=
sudo systemctl restart chillcheckServer: the same, but server/venv/bin/python, then edit
ADMIN_PASSWORD_HASH (or VIEWER_PASSWORD_HASH) in /opt/chillcheck/server/.env
and sudo systemctl restart chillcheck-server.
Everyone is signed out when the app restarts.
Useful so staff can look without being able to sign records off:
# generate a hash as above, then in server/.env:
VIEWER_PASSWORD_HASH=pbkdf2:sha256:...Leave it blank and the admin password works for both.
If the token might have been exposed:
# On the server
python3 -c "import secrets; print(secrets.token_hex(32))"
sudo nano /opt/chillcheck/server/.env # new INGEST_TOKEN
sudo systemctl restart chillcheck-server
# On the Pi — same value
nano ~/chillcheck/.env # new CLOUD_TOKEN
sudo systemctl restart chillcheckReadings queue on the Pi while the tokens don't match, and back-fill once they do. Nothing is lost.
Delete the password line from .env, restart, and the app will refuse to start
with instructions. Generate a new hash as above. Your records are untouched —
they're in the database, not the config.
The whole record is one file.
On-site: ~/chillcheck/data/chillcheck.db
Server: /opt/chillcheck/server/data/chillcheck.db
Copy it somewhere else regularly. The simplest useful thing:
sudo crontab -e0 3 * * * cp /opt/chillcheck/server/data/chillcheck.db /root/backups/chillcheck-$(date +\%F).dbRunning the server half is itself a backup — two independent copies of every reading, which is most of the point of it.
# Pi
cd ~/chillcheck && git pull && .venv/bin/pip install -r requirements.txt -q && sudo systemctl restart chillcheck
sudo apt update && sudo apt full-upgrade -y
# Server
cd /opt/chillcheck && sudo git pull && sudo systemctl restart chillcheck-serverWorth doing every few months. There's a prompt in CLAUDE-PROMPTS.md that sets up unattended security updates.
Found a security issue? Please open an issue on the repository describing the class of problem, without a working exploit.