Security hardening (11.0.282+)¶
Homer 11.0.282–11.0.284 closes three coordinator security issues. 11.0.313 hardens the node HTTP /query API and blocks DuckDB external scanners. This page summarizes operator impact and how upgrades stay compatible with existing installs.
Summary¶
| Issue | Risk (before) | Fix | Typical upgrade impact |
|---|---|---|---|
Empty coordinator.jwt.secret |
Protected API routes were unauthenticated | JWT middleware always enforced; empty secret → auto-generated persisted secret | Installs with explicit JWT env/config: no change. Installs with empty secret: API now requires login or Auth-Token |
Default admin sipcapture |
Predictable first-login password | No compose hash; random bcrypt at first boot; JWT must_change_password until a new password is set |
Existing users row with sipcapture hash: login works, then the UI/API require a password change |
POST /api/v4/statistics/query rawquery |
Unvalidated SQL passthrough | Same ValidateRawSQL rules as /api/v4/query |
Legitimate SELECT / WITH / SHOW queries: no change |
Node POST /query (port+1) |
Unauthenticated arbitrary DuckDB SQL / file read | ValidateRawSQL always; Bearer auth when flight_server.auth_token set; auto-token on non-loopback bind |
All-in-one with default 0.0.0.0: token auto-persisted; coordinator local node gets token automatically |
sqlite_scan / external scanners |
Authenticated bypass of SQL denylist | Blocked in ValidateRawSQL |
Legitimate lake SELECTs: no change |
GitHub Security Advisories: GHSA-rqcc-94gv-wjm9, GHSA-6xp5-7rcx-xfgx, GHSA-f46q-3v67-fmm4, GHSA-rm5w-rqr7-2h54, GHSA-4687-q698-mccv, GHSA-263f-5xrw-c34r.
JWT secret (coordinator.jwt.secret)¶
Before: When the signing secret was empty, JWT middleware was not registered and handlers skipped validation — every protected route was open.
After:
- Non-empty secret in config or env — unchanged (recommended for production).
- Empty secret — on coordinator startup Homer generates a random secret, writes
/.homer_jwt_secretbesidecoordinator.settings_db_path(mode0600), and reuses it on restart so sessions stay valid. - Protected routes under
/api/v1,/api/v3, and/api/v4always require authentication (JWT cookie/Bearer, or validAuth-Tokenwhenapi_settings.enable_token_accessis true).
"coordinator": {
"settings_db_path": "/var/lib/homer/homer_settings.duckdb",
"jwt": {
"secret": "use-a-long-random-string-at-least-32-bytes",
"expire_hours": 24
}
}
Environment: HOMER_COORDINATOR_JWT_SECRET.
Docker Compose (examples/docker/docker-compose.yaml) already sets HOMER_COORDINATOR_JWT_SECRET — no action required.
Bootstrap admin password (coordinator.auth)¶
Before: Internal auth normalization injected a fixed SHA-256 hash for cleartext password sipcapture.
After:
| Config | First startup (no users row) |
Existing users row |
|---|---|---|
Explicit admin_password_hash or env |
Uses configured hash | Unchanged if row already has a password |
| Empty hash | Random password generated; logged once at startup | Unchanged if row already has a password |
| Docker examples | Hash omitted; random password in coordinator logs | Existing sipcapture hash in users: login then forced password change |
How to read the Docker password (fresh volume):
docker compose -f examples/docker/docker-compose.yaml logs homer 2>&1 | grep bootstrap_password
# or: docker logs <homer-container> 2>&1 | grep bootstrap_password
The warning looks like generated one-time bootstrap admin password with field
bootstrap_password. It is printed only on first insert of the admin row, not
on later restarts. See examples/docker/README.md.
Legacy SHA-256 rows from migrated homer-app users still authenticate. The well-known sipcapture digest still logs in, but the session is limited to GET/PATCH /api/v4/me and logout until the password is changed. --reset-admin-password still refuses that digest as a new hash.
Wizard: empty admin password field → random password (bcrypt in JSON) shown once after save.
Recovery: homer --config-path … --reset-admin-password still requires explicit admin_password_hash (SHA-256 hex) in config or env.
See AUTH_LDAP_AND_OAUTH.md and COORDINATOR.md.
Statistics SQL (POST /api/v4/statistics/query)¶
Before: rawquery in the request body was passed to FlightSQL without validation.
After: Each rawquery is validated with the same rules as POST /api/v4/query:
- Allowed statement starts:
SELECT,WITH,SHOW,DESCRIBE,EXPLAIN,PRAGMA - No semicolons (multi-statement blocked)
- Blocked DML/DDL keywords and dangerous functions (token-aware: keywords inside string literals / Call-IDs are ignored; real
CALL/DELETEidentifiers remain blocked) - External scanners blocked:
sqlite_scan,postgres_scan,mysql_scan,iceberg_scan,delta_scan,spatial_scan(11.0.313+)
Invalid SQL returns 400 with SQL validation failed. Grafana-style read-only panels using single SELECT statements continue to work.
See LINE_PROTOCOL.md (Statistics API section).
Node HTTP /query (11.0.313+)¶
Before: The node HTTP API on flight_server.port + 1 (default 50052) accepted POST /query with no authentication and no SQL validation, so an unauthenticated client could run arbitrary DuckDB SQL (including read_text('/etc/passwd') and full lake dumps).
After:
- Every
/querybody is validated withValidateRawSQL(same rules as coordinator raw SQL). - When
node.flight_server.auth_tokenis non-empty,/queryand/vacuumrequireAuthorization: Bearer <token>./healthstays open for probes. - If
auth_tokenis empty andflight_server.hostis not loopback (default0.0.0.0), Homer generates a random token, persists.homer_node_auth_tokenbeside the DuckLake catalog (mode0600), and enables Bearer auth. Loopback-only binds (127.0.0.1/::1/localhost) may keep an empty token. - The coordinator sends
coordinator.nodes[].tokenas Bearer on/query. Same-process local nodes with emptytokeninheritnode.flight_server.auth_tokenautomatically.
"node": {
"flight_server": {
"host": "127.0.0.1",
"port": 50051,
"auth_token": "change-me-node-http"
}
},
"coordinator": {
"nodes": [
{
"name": "local",
"host": "127.0.0.1",
"port": 50051,
"token": "change-me-node-http"
}
]
}
Recommendation: bind flight_server.host to 127.0.0.1 when the node is only used by a co-located coordinator, and set an explicit auth_token / nodes[].token in production. Firewall port+1 from untrusted networks.
The live HEP WebSocket /stream on the same HTTP port is still intended for private networks (see HEP_STREAM.md); prefer not exposing it publicly.
Upgrade checklist¶
- Review JWT — If you never set
coordinator.jwt.secret, after upgrade check coordinator logs forjwt_secret_fileor set an explicit secret in config. - Review admin access — If you relied on open API without JWT, enable
Auth-Tokentokens or use UI login. - Docker / explicit env —
HOMER_COORDINATOR_JWT_SECRET+HOMER_COORDINATOR_AUTH_ADMIN_PASSWORD_HASH(as inexamples/docker/) → no credential changes. - Automation — Scripts calling protected endpoints need
Authorization: Bearer …orAuth-Tokenafter upgrade if JWT was previously empty. - Node
/query— After 11.0.313, check logs forauth_token_file/ Bearer requirement; set matchingcoordinator.nodes[].tokenfor remote nodes; prefer loopback bind for local-only nodes.
See also¶
- COORDINATOR.md —
jwt,authparameters - NODE.md —
flight_server.auth_token, HTTP/query - UI_COORDINATOR_AUTH_AND_TOKENS.md — login, cookies, API tokens
- WIZARD.md — generated credentials
- AUTH_LDAP_AND_OAUTH.md —
--reset-admin-password