A review pass over v0.10.0. One real bug, one deployment default worth changing, and two limitations now written down instead of implied.
/health returned 500 instead of 503
The 0.10.0 probe built its detail field from the first line of the exception message. An empty message has no lines, so indexing it raised IndexError — a 500 from the endpoint whose entire job is to answer 503 honestly.
asyncio.timeout raises a bare TimeoutError that stringifies to "", which means this was the common path, not an exotic one: a database too slow to answer inside the probe's own 3s cap took it — precisely the condition the probe exists to report.
Fixed and regression-tested.
Use a read-only database role
Not a code change, but the most important thing in this release.
TeslaMate's own Compose sets POSTGRES_USER=teslamate, which makes that role a superuser. If DATABASE_URL points at it, every write guard still holds — READ ONLY and the keyword filter both block UPDATE and COPY … TO PROGRAM — but a READ ONLY transaction does not restrict superuser-only read functions. Verified against a live instance:
SELECT pg_read_file('/etc/hostname'); -- returns the container's hostname
SELECT pg_ls_dir('/etc'); -- directory listing
SELECT rolname FROM pg_authid; -- role catalogueRe-run as a SELECT-only role, all three are permission denied — and all 30 bundled queries, run_sql, and schema introspection keep working unchanged.
The realistic risk is not a network attacker; the endpoint takes a bearer token. It is prompt injection steering the model that writes the SQL. A non-superuser role removes the capability outright, which no application-layer filter does as reliably.
If you are running this against the teslamate user, switch:
CREATE ROLE teslamate_ro LOGIN PASSWORD '…';
GRANT CONNECT ON DATABASE teslamate TO teslamate_ro;
GRANT USAGE ON SCHEMA public TO teslamate_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO teslamate_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO teslamate_ro;env.example now defaults to teslamate_ro, and SECURITY.md explains the reasoning.
Two limitations, now documented
- The row cap is bypassable.
run_sqlonly wraps a query that has noLIMITof its own, and that check does not distinguish a nested one —SELECT * FROM (SELECT … LIMIT 5000000) xruns uncapped.statement_timeoutstill bounds it in time. Documented rather than "fixed", because tightening the detection risks rejecting legitimate queries. /healthis unauthenticated by design so container health checks can reach it, and it returns a shortdetailwhen the database is unreachable. Treat that as information disclosure if you expose the endpoint publicly.
Housekeeping
The Unraid + Cloudflare walkthrough moved to the wiki, marked as community-contributed and unmaintained; the Community Applications template stays in deploy/unraid/. The static test-count badge is now the dynamic CI badge.
Reviewed and found sound
For the record, these were checked and needed no change: the MCP Apps HTML uses textContent throughout — no innerHTML, no fetch, no external references; bearer comparison is timing-safe; .env is git- and docker-ignored and absent from the published image; the image runs as a non-root user; OpenTelemetry is imported lazily, so it costs nothing unless an endpoint is configured.