Edge nodes are deployed to arbitrary locations by arbitrary people, so the
broker has to be reachable from the internet and secured on its own merits
rather than by a VPN.
Three defects made that impossible. The broker only had a plaintext 1883
listener; every node shared one drb-node password; and the ACL pattern used
%c, the client-supplied client id, so any holder of that shared password
could set client_id to another node and take over its namespace. The comment
claiming this cryptographically prevented cross-node access was wrong and is
gone.
Authentication now uses mosquitto 2.x's built-in dynamic-security plugin on
the stock eclipse-mosquitto image. c2-core administers it over the control
topic, creating each node's client on approval with username=<node_id> and
password=<its node_keys api_key>, attached to a role whose ACL is nodes/%u/#
against the authenticated username. One credential, one revocation point.
An HTTP-callback plugin was implemented first and rejected: that project is
archived upstream, which is not an acceptable dependency on an
internet-facing broker.
Because dynsec state is a second source of truth alongside Firestore,
approve/reissue/delete now write to the broker first and surface a 502
rather than drifting, and c2-core reconciles every approved node into dynsec
on startup.
Adds node self-enrollment (POST /nodes/enroll, GET /nodes/{id}/credentials)
so a new node can obtain its key over HTTPS without an operator handling
secrets by hand. Enrolling an already-approved node_id is refused on the
fleet token alone — otherwise a leaked token plus a guessable id would let
an attacker steal a live node's key before the real node asked for it.
Pickup secrets are stored hashed and returned once, and the endpoint is rate
limited per source IP.
Infrastructure: an 8883 TLS listener fed by Caddy's certificate via a
systemd path unit, a firewall rule for it, and Caddy now 404s /internal/*
so the api vhost cannot proxy internal routes.
Also fixes CORS, which allowed https://app.<domain> while the frontend is
served on the bare domain — every call from the portal would have failed —
and widens the vault gitignore to a glob, since ansible-vault leaves
backup siblings that the exact-name rule left committable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
40 lines
1.4 KiB
Bash
40 lines
1.4 KiB
Bash
# MQTT broker (usually the mosquitto container on this host)
|
|
MQTT_BROKER=mosquitto
|
|
MQTT_PORT=1883
|
|
# Use the c2-core credential — must match MQTT_C2_USER/MQTT_C2_PASS in the
|
|
# top-level .env
|
|
MQTT_USER=drb-c2-core
|
|
MQTT_PASS=change-me-c2
|
|
|
|
# Same value as the top-level .env's MOSQUITTO_DYNSEC_PASSWORD — lets
|
|
# c2-core log in as mosquitto's built-in dynsec "admin" client to
|
|
# administer node MQTT credentials. See app/internal/dynsec.py.
|
|
MQTT_DYNSEC_ADMIN_PASS=change-me-dynsec-admin-min-12-chars
|
|
|
|
# GCP — path to service account JSON inside the container
|
|
GCP_CREDENTIALS_PATH=/app/gcp-key.json
|
|
|
|
# Firestore database name (use "(default)" if you didn't create a named database)
|
|
FIRESTORE_DATABASE=c2-server
|
|
|
|
# GCS bucket for audio storage
|
|
GCS_BUCKET=your-bucket-name
|
|
|
|
# How long (seconds) before a node is marked offline if no checkin received
|
|
NODE_OFFLINE_THRESHOLD=90
|
|
|
|
# Google Maps — for geocoding location strings extracted from transcripts
|
|
# Enable "Geocoding API" in Cloud Console for this key
|
|
GOOGLE_MAPS_API_KEY=
|
|
|
|
# OpenAI — for transcription (Whisper), intelligence extraction, embeddings, and summaries
|
|
OPENAI_API_KEY=
|
|
SUMMARY_INTERVAL_MINUTES=15
|
|
CORRELATION_WINDOW_HOURS=4
|
|
EMBEDDING_SIMILARITY_THRESHOLD=0.82
|
|
|
|
# Fleet-wide token edge nodes present as X-Enrollment-Token on first boot
|
|
# (POST /nodes/enroll). Shared across every node — NOT a per-node secret.
|
|
# Generate with: openssl rand -hex 32
|
|
ENROLLMENT_TOKEN=
|