feat(templates): per-instance stealth via instance_seed in service servers

Every service template now pulls version strings, cluster/node UUIDs, auth
salts, greeting banners, and uptime from the seeded per-instance RNG instead
of hard-coded defaults. Scanners sweeping the fleet now see legitimately
diverging fingerprints per decky while each decky's own responses stay
internally consistent across restarts.

Covers elasticsearch, ftp, http, https, ldap, mongodb, mqtt, mssql, mysql,
postgres, redis, and smtp templates.
This commit is contained in:
2026-04-22 09:24:16 -04:00
parent 51e9e263ca
commit 3fb84ac5d0
12 changed files with 755 additions and 156 deletions

View File

@@ -12,16 +12,44 @@ import json
import os
import random
import struct
import instance_seed as _seed
from syslog_bridge import syslog_line, write_syslog_file, forward_syslog
NODE_NAME = os.environ.get("NODE_NAME", "mqtt-broker")
SERVICE_NAME = "mqtt"
LOG_TARGET = os.environ.get("LOG_TARGET", "")
PORT = int(os.environ.get("PORT", "1883"))
MQTT_ACCEPT_ALL = os.environ.get("MQTT_ACCEPT_ALL", "1") == "1"
MQTT_PERSONA = os.environ.get("MQTT_PERSONA", "water_plant")
# Default to auth-required. A broker that accepts literally anyone with any
# client_id / username was realistic for devices on a flat OT LAN pre-2018,
# but in 2024+ it's a tell. Operators who *want* the anonymous-broker decoy
# still set MQTT_ACCEPT_ALL=1 explicitly.
MQTT_ACCEPT_ALL = os.environ.get("MQTT_ACCEPT_ALL", "0") == "1"
# Optional cred list (user:pass comma-separated). If set, only those combos
# succeed even when ACCEPT_ALL is off — lets operators plant credential bait.
_MQTT_CREDS: set[tuple[str, str]] = set()
for combo in os.environ.get("MQTT_CREDS", "").split(","):
combo = combo.strip()
if ":" in combo:
u, _, p = combo.partition(":")
_MQTT_CREDS.add((u, p))
_PERSONA_CHOICES = ["water_plant", "building_hvac", "solar_farm", "factory_line"]
MQTT_PERSONA = os.environ.get("MQTT_PERSONA") or _seed.pick(_PERSONA_CHOICES)
MQTT_CUSTOM_TOPICS = os.environ.get("MQTT_CUSTOM_TOPICS", "")
# Fleet-diverse broker ID. Real mosquitto versions in the wild right now.
_BROKER_VERSION = os.environ.get("MQTT_BROKER_VERSION") or _seed.pick([
"mosquitto version 1.6.9",
"mosquitto version 2.0.11",
"mosquitto version 2.0.15",
"mosquitto version 2.0.17",
"mosquitto version 2.0.18",
"HiveMQ CE 2024.4",
"EMQX 5.3.2",
])
_CONNACK_ACCEPTED = b"\x20\x02\x00\x00"
_CONNACK_NOT_AUTH = b"\x20\x02\x00\x05"
@@ -133,27 +161,45 @@ def _generate_topics() -> dict:
_log("config_error", severity=4, error=str(e))
if MQTT_PERSONA == "water_plant":
site = _seed.pick(["north", "south", "east", "west", "plant-a", "plant-b"])
topics.update({
"plant/water/tank1/level": f"{random.uniform(60.0, 80.0):.1f}",
"plant/water/tank1/pressure": f"{random.uniform(2.5, 3.0):.2f}",
"plant/water/pump1/status": "RUNNING",
"plant/water/pump1/rpm": f"{int(random.uniform(1400, 1450))}",
"plant/water/pump2/status": "STANDBY",
"plant/water/chlorine/dosing": f"{random.uniform(1.1, 1.3):.1f}",
"plant/water/chlorine/residual": f"{random.uniform(0.7, 0.9):.1f}",
"plant/water/valve/inlet/state": "OPEN",
"plant/water/valve/drain/state": "CLOSED",
"plant/alarm/high_pressure": "0",
"plant/alarm/low_chlorine": "0",
"plant/alarm/pump_fault": "0",
"plant/$SYS/broker/version": "Mosquitto 2.0.15",
"plant/$SYS/broker/uptime": "2847392",
f"{site}/water/tank1/level": f"{random.uniform(60.0, 80.0):.1f}",
f"{site}/water/tank1/pressure": f"{random.uniform(2.5, 3.0):.2f}",
f"{site}/water/pump1/status": "RUNNING",
f"{site}/water/pump1/rpm": f"{int(random.uniform(1400, 1450))}",
f"{site}/water/pump2/status": "STANDBY",
f"{site}/water/chlorine/dosing": f"{random.uniform(1.1, 1.3):.1f}",
f"{site}/water/chlorine/residual": f"{random.uniform(0.7, 0.9):.1f}",
f"{site}/water/valve/inlet/state": "OPEN",
f"{site}/water/valve/drain/state": "CLOSED",
f"{site}/alarm/high_pressure": "0",
f"{site}/alarm/low_chlorine": "0",
f"{site}/alarm/pump_fault": "0",
})
elif not topics:
elif MQTT_PERSONA == "building_hvac":
floor = _seed.rng.randint(1, 12)
for i in range(_seed.rng.randint(4, 10)):
topics[f"bldg/floor{floor}/zone{i}/temp"] = f"{random.uniform(20.0, 24.5):.1f}"
topics[f"bldg/floor{floor}/zone{i}/setpoint"] = f"{random.uniform(21.0, 23.0):.1f}"
topics[f"bldg/floor{floor}/ahu/status"] = _seed.pick(["RUNNING", "RUNNING", "IDLE"])
elif MQTT_PERSONA == "solar_farm":
for arr in range(1, _seed.rng.randint(4, 9)):
topics[f"solar/array{arr}/power_kw"] = f"{random.uniform(40.0, 180.0):.1f}"
topics[f"solar/array{arr}/irradiance"] = f"{random.uniform(500, 950):.0f}"
elif MQTT_PERSONA == "factory_line":
line = _seed.pick(["A", "B", "C"])
for m in range(1, _seed.rng.randint(3, 7)):
topics[f"line{line}/machine{m}/state"] = _seed.pick(["RUN", "RUN", "IDLE", "FAULT"])
topics[f"line{line}/machine{m}/cycle_count"] = str(_seed.rng.randint(1000, 999_999))
if not topics:
topics = {
"device/status": "online",
"device/uptime": "3600"
"device/uptime": str(_seed.uptime_seconds()),
}
# $SYS keys match every real broker.
topics["$SYS/broker/version"] = _BROKER_VERSION
topics["$SYS/broker/uptime"] = f"{_seed.uptime_seconds()} seconds"
topics["$SYS/broker/clients/connected"] = str(_seed.rng.randint(2, 24))
return topics
@@ -211,7 +257,13 @@ class MQTTProtocol(asyncio.Protocol):
if pkt_type == 1: # CONNECT
info = _parse_connect(payload)
_log("auth", **info)
if MQTT_ACCEPT_ALL:
# Decide connection: accept-all > cred list > deny.
cred = (info.get("username", ""), info.get("password", ""))
accepted = (
MQTT_ACCEPT_ALL
or (cred in _MQTT_CREDS if _MQTT_CREDS else False)
)
if accepted:
self._auth = True
self._transport.write(_CONNACK_ACCEPTED)
else: