Authenticate the node dashboard, and the broker connection per node
Two unauthenticated surfaces closed on the edge node. Dashboard and API: the local dashboard and every /api/* route were open to anything on the node's LAN. Adds a login page plus session-cookie auth for the browser, and cookie-or-Basic for the API so scripted callers stay possible. Passwords are hashed with stdlib scrypt (no new dependency, this runs on a Pi) and compared in constant time; the salt and session-signing secret persist in credentials.json. Startup warns while the default password is still in place. No non-browser callers of the node API exist today (C2 talks to nodes over MQTT and nodes call C2 outbound), so nothing breaks. Adds python-multipart, which FastAPI's Form() needs for the login POST and which was missing from requirements entirely. MQTT: nodes authenticated with a shared drb-node password, and the broker ACL keyed off %c — the client-supplied client id — so any holder of that one password could claim another node's topic namespace. Nodes now connect as username=<node_id>, password=<their C2-issued api_key>, which mosquitto's dynamic-security plugin checks, with the ACL keyed off the authenticated %u. TLS is gated on MQTT_TLS and uses default CA verification. The old key_request MQTT path stays in place behind TODO(mqtt-cutover) markers as the fallback until the cutover is proven; a node with no api_key on disk logs a clear repeated refusal rather than spinning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a61a7b2c31
commit
87633ab50d
@@ -266,8 +266,11 @@ async def on_config_push(payload: dict):
|
||||
async def lifespan(app: FastAPI):
|
||||
logger.info(f"Edge node starting — ID: {settings.node_id}")
|
||||
|
||||
# Load persisted credentials (API key provisioned by C2 after approval)
|
||||
# Load persisted credentials (API key provisioned by C2 after approval;
|
||||
# also generates/loads the local dashboard's auth salt + session secret)
|
||||
credentials.load()
|
||||
from app.internal import auth
|
||||
auth.warn_if_default_password()
|
||||
|
||||
# Wire callbacks
|
||||
metadata_watcher.on_call_start = on_call_start
|
||||
|
||||
Reference in New Issue
Block a user