feat(orchestrator): MVP synthetic life-injection worker (SSH only)

Adds a new decnet orchestrate worker whose job is to keep the honeypot
ecosystem from looking suspiciously static — a frozen LAN with no
inter-host traffic and no filesystem aging is its own honeypot tell.

MVP scope:
- New OrchestratorEvent table + repo methods (purpose-built sibling
  to Log so synthetic events stay separable from attacker-driven ones).
- New orchestrator.{activity,file}.<decky_id> bus topics +
  system.orchestrator.health heartbeat.
- SSH-only driver. Traffic action runs python3 inside src container
  to TCP-connect dst:22 and read the SSH banner — real on-the-wire
  SSH-protocol traffic without shipping creds. File action drops or
  refreshes a small file via docker exec on the destination.
- Random scheduler (50/50 traffic/file when >=2 SSH-capable deckies
  are running). Diurnal shaping, role-aware pairing, and session-aware
  backoff are explicit non-goals for MVP.
- CLI registration, systemd unit (SupplementaryGroups=docker),
  worker-registry entry so the dashboard shows orchestrator health.
- 11 tests: scheduler policy, driver argv shape + injection-safety,
  end-to-end one-tick integration with FakeBus + SQLite.
This commit is contained in:
2026-04-26 19:43:20 -04:00
parent cc2deb73f7
commit 4c37ece39e
21 changed files with 972 additions and 1 deletions

View File

@@ -10,6 +10,8 @@ Token structure (NATS-style, dot-separated):
topology.{topology_id}.status
decky.{decky_id}.state
decky.{decky_id}.traffic
orchestrator.activity.{decky_id}
orchestrator.file.{decky_id}
attacker.observed
attacker.scored
attacker.session.started
@@ -46,6 +48,7 @@ IDENTITY = "identity"
CAMPAIGN = "campaign"
SYSTEM = "system"
CREDENTIAL = "credential"
ORCHESTRATOR = "orchestrator"
# ─── Leaf event-type constants (the last segment of each topic) ──────────────
@@ -160,6 +163,16 @@ CAMPAIGN_UNMERGED = "unmerged"
CREDENTIAL_CAPTURED = "captured"
CREDENTIAL_REUSE_DETECTED = "reuse.detected"
# Orchestrator event types (second token under ``orchestrator``). The
# orchestrator worker publishes one of these per synthetic action it
# drives against a decky — cheap inter-decky traffic and filesystem
# mutations whose role is to keep the honeypot from looking suspiciously
# static. Always nested with the destination decky uuid as the third
# token, so consumers can subscribe to a single decky's life-injection
# stream via ``orchestrator.*.<decky_uuid>``.
ORCHESTRATOR_ACTIVITY = "activity"
ORCHESTRATOR_FILE = "file"
# System event types.
SYSTEM_LOG = "log"
SYSTEM_BUS_HEALTH = "bus.health"
@@ -280,6 +293,18 @@ def identity(event_type: str) -> str:
return f"{IDENTITY}.{event_type}"
def orchestrator(event_type: str, decky_id: str) -> str:
"""Build ``orchestrator.<event_type>.<decky_id>``.
*event_type* should be one of :data:`ORCHESTRATOR_ACTIVITY` or
:data:`ORCHESTRATOR_FILE`. The destination decky is always the
third token so per-decky subscribers can use
``orchestrator.*.<decky_uuid>``.
"""
_reject_tokens(event_type, decky_id)
return f"{ORCHESTRATOR}.{event_type}.{decky_id}"
def system_health(worker: str) -> str:
"""Build ``system.<worker>.health``.