From 51e9e263cad4c04de197af3a2ef5368a48a1da07 Mon Sep 17 00:00:00 2001 From: anti Date: Wed, 22 Apr 2026 09:24:04 -0400 Subject: [PATCH] feat(templates): add instance_seed stealth helper and wire into template builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each decky now gets a deterministic-per-instance seeded RNG derived from NODE_NAME, so cluster UUIDs, version strings, uptime, and credentials diverge across the fleet while staying stable within one container. The canonical helper lives at decnet/templates/instance_seed.py; the deployer copies it into every active template build context alongside syslog_bridge.py. Dockerfiles COPY it to /opt/ so server.py can import it. Connection-time jitter intentionally stays unseeded — two hits to the same decky must not replay the same latency curve. --- decnet/engine/deployer.py | 11 +- decnet/templates/elasticsearch/Dockerfile | 1 + .../templates/elasticsearch/instance_seed.py | 120 ++++++++++++++++++ decnet/templates/ftp/Dockerfile | 1 + decnet/templates/ftp/instance_seed.py | 120 ++++++++++++++++++ decnet/templates/http/Dockerfile | 1 + decnet/templates/http/instance_seed.py | 120 ++++++++++++++++++ decnet/templates/https/Dockerfile | 1 + decnet/templates/https/instance_seed.py | 120 ++++++++++++++++++ decnet/templates/instance_seed.py | 120 ++++++++++++++++++ decnet/templates/ldap/Dockerfile | 1 + decnet/templates/ldap/instance_seed.py | 120 ++++++++++++++++++ decnet/templates/mongodb/Dockerfile | 1 + decnet/templates/mongodb/instance_seed.py | 120 ++++++++++++++++++ decnet/templates/mqtt/Dockerfile | 1 + decnet/templates/mqtt/instance_seed.py | 120 ++++++++++++++++++ decnet/templates/mssql/Dockerfile | 1 + decnet/templates/mssql/instance_seed.py | 120 ++++++++++++++++++ decnet/templates/mysql/Dockerfile | 1 + decnet/templates/mysql/instance_seed.py | 120 ++++++++++++++++++ decnet/templates/postgres/Dockerfile | 1 + decnet/templates/postgres/instance_seed.py | 120 ++++++++++++++++++ decnet/templates/redis/Dockerfile | 1 + decnet/templates/redis/instance_seed.py | 120 ++++++++++++++++++ decnet/templates/smtp/Dockerfile | 1 + decnet/templates/smtp/instance_seed.py | 120 ++++++++++++++++++ 26 files changed, 1579 insertions(+), 4 deletions(-) create mode 100644 decnet/templates/elasticsearch/instance_seed.py create mode 100644 decnet/templates/ftp/instance_seed.py create mode 100644 decnet/templates/http/instance_seed.py create mode 100644 decnet/templates/https/instance_seed.py create mode 100644 decnet/templates/instance_seed.py create mode 100644 decnet/templates/ldap/instance_seed.py create mode 100644 decnet/templates/mongodb/instance_seed.py create mode 100644 decnet/templates/mqtt/instance_seed.py create mode 100644 decnet/templates/mssql/instance_seed.py create mode 100644 decnet/templates/mysql/instance_seed.py create mode 100644 decnet/templates/postgres/instance_seed.py create mode 100644 decnet/templates/redis/instance_seed.py create mode 100644 decnet/templates/smtp/instance_seed.py diff --git a/decnet/engine/deployer.py b/decnet/engine/deployer.py index 620383ff..6fec4119 100644 --- a/decnet/engine/deployer.py +++ b/decnet/engine/deployer.py @@ -49,13 +49,15 @@ log = get_logger("engine") console = Console() COMPOSE_FILE = Path("decnet-compose.yml") _CANONICAL_LOGGING = Path(__file__).parent.parent / "templates" / "syslog_bridge.py" +_CANONICAL_INSTANCE_SEED = Path(__file__).parent.parent / "templates" / "instance_seed.py" _CANONICAL_SESSREC_DIR = Path(__file__).parent.parent / "templates" / "_shared" / "sessrec" _SESSREC_SERVICES = {"ssh", "telnet"} def _sync_logging_helper(config: DecnetConfig) -> None: - """Copy the canonical syslog_bridge.py into every active template build context.""" + """Copy canonical shared helpers into every active template build context.""" from decnet.services.registry import get_service + shared_files = [_CANONICAL_LOGGING, _CANONICAL_INSTANCE_SEED] seen: set[Path] = set() for decky in config.deckies: for svc_name in decky.services: @@ -66,9 +68,10 @@ def _sync_logging_helper(config: DecnetConfig) -> None: if ctx is None or ctx in seen: continue seen.add(ctx) - dest = ctx / "syslog_bridge.py" - if not dest.exists() or dest.read_bytes() != _CANONICAL_LOGGING.read_bytes(): - shutil.copy2(_CANONICAL_LOGGING, dest) + for src in shared_files: + dest = ctx / src.name + if not dest.exists() or dest.read_bytes() != src.read_bytes(): + shutil.copy2(src, dest) def _sync_sessrec_sources(config: DecnetConfig) -> None: diff --git a/decnet/templates/elasticsearch/Dockerfile b/decnet/templates/elasticsearch/Dockerfile index 5dca7b87..7087b291 100644 --- a/decnet/templates/elasticsearch/Dockerfile +++ b/decnet/templates/elasticsearch/Dockerfile @@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* COPY syslog_bridge.py /opt/syslog_bridge.py +COPY instance_seed.py /opt/instance_seed.py COPY server.py /opt/server.py COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/decnet/templates/elasticsearch/instance_seed.py b/decnet/templates/elasticsearch/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/elasticsearch/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/ftp/Dockerfile b/decnet/templates/ftp/Dockerfile index 378b3c86..d8f52dd9 100644 --- a/decnet/templates/ftp/Dockerfile +++ b/decnet/templates/ftp/Dockerfile @@ -9,6 +9,7 @@ ENV PIP_BREAK_SYSTEM_PACKAGES=1 RUN pip3 install --no-cache-dir twisted jinja2 COPY syslog_bridge.py /opt/syslog_bridge.py +COPY instance_seed.py /opt/instance_seed.py COPY server.py /opt/server.py COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/decnet/templates/ftp/instance_seed.py b/decnet/templates/ftp/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/ftp/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/http/Dockerfile b/decnet/templates/http/Dockerfile index a8f2876f..19ff037f 100644 --- a/decnet/templates/http/Dockerfile +++ b/decnet/templates/http/Dockerfile @@ -9,6 +9,7 @@ ENV PIP_BREAK_SYSTEM_PACKAGES=1 RUN pip3 install --no-cache-dir flask jinja2 COPY syslog_bridge.py /opt/syslog_bridge.py +COPY instance_seed.py /opt/instance_seed.py COPY server.py /opt/server.py COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/decnet/templates/http/instance_seed.py b/decnet/templates/http/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/http/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/https/Dockerfile b/decnet/templates/https/Dockerfile index 7dbd9152..2371e64d 100644 --- a/decnet/templates/https/Dockerfile +++ b/decnet/templates/https/Dockerfile @@ -9,6 +9,7 @@ ENV PIP_BREAK_SYSTEM_PACKAGES=1 RUN pip3 install --no-cache-dir flask jinja2 COPY syslog_bridge.py /opt/syslog_bridge.py +COPY instance_seed.py /opt/instance_seed.py COPY server.py /opt/server.py COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/decnet/templates/https/instance_seed.py b/decnet/templates/https/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/https/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/instance_seed.py b/decnet/templates/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/ldap/Dockerfile b/decnet/templates/ldap/Dockerfile index 64e1a508..53636300 100644 --- a/decnet/templates/ldap/Dockerfile +++ b/decnet/templates/ldap/Dockerfile @@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* COPY syslog_bridge.py /opt/syslog_bridge.py +COPY instance_seed.py /opt/instance_seed.py COPY server.py /opt/server.py COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/decnet/templates/ldap/instance_seed.py b/decnet/templates/ldap/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/ldap/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/mongodb/Dockerfile b/decnet/templates/mongodb/Dockerfile index d7bc9531..b0f472db 100644 --- a/decnet/templates/mongodb/Dockerfile +++ b/decnet/templates/mongodb/Dockerfile @@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* COPY syslog_bridge.py /opt/syslog_bridge.py +COPY instance_seed.py /opt/instance_seed.py COPY server.py /opt/server.py COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/decnet/templates/mongodb/instance_seed.py b/decnet/templates/mongodb/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/mongodb/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/mqtt/Dockerfile b/decnet/templates/mqtt/Dockerfile index 562ed429..11f9c6ee 100644 --- a/decnet/templates/mqtt/Dockerfile +++ b/decnet/templates/mqtt/Dockerfile @@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* COPY syslog_bridge.py /opt/syslog_bridge.py +COPY instance_seed.py /opt/instance_seed.py COPY server.py /opt/server.py COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/decnet/templates/mqtt/instance_seed.py b/decnet/templates/mqtt/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/mqtt/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/mssql/Dockerfile b/decnet/templates/mssql/Dockerfile index 2f341564..fd2c972c 100644 --- a/decnet/templates/mssql/Dockerfile +++ b/decnet/templates/mssql/Dockerfile @@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* COPY syslog_bridge.py /opt/syslog_bridge.py +COPY instance_seed.py /opt/instance_seed.py COPY server.py /opt/server.py COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/decnet/templates/mssql/instance_seed.py b/decnet/templates/mssql/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/mssql/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/mysql/Dockerfile b/decnet/templates/mysql/Dockerfile index 926e74b1..f3bb4f2a 100644 --- a/decnet/templates/mysql/Dockerfile +++ b/decnet/templates/mysql/Dockerfile @@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* COPY syslog_bridge.py /opt/syslog_bridge.py +COPY instance_seed.py /opt/instance_seed.py COPY server.py /opt/server.py COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/decnet/templates/mysql/instance_seed.py b/decnet/templates/mysql/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/mysql/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/postgres/Dockerfile b/decnet/templates/postgres/Dockerfile index 6eab4e17..d564c9a1 100644 --- a/decnet/templates/postgres/Dockerfile +++ b/decnet/templates/postgres/Dockerfile @@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* COPY syslog_bridge.py /opt/syslog_bridge.py +COPY instance_seed.py /opt/instance_seed.py COPY server.py /opt/server.py COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/decnet/templates/postgres/instance_seed.py b/decnet/templates/postgres/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/postgres/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/redis/Dockerfile b/decnet/templates/redis/Dockerfile index b3f85de0..b644ccc5 100644 --- a/decnet/templates/redis/Dockerfile +++ b/decnet/templates/redis/Dockerfile @@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* COPY syslog_bridge.py /opt/syslog_bridge.py +COPY instance_seed.py /opt/instance_seed.py COPY server.py /opt/server.py COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/decnet/templates/redis/instance_seed.py b/decnet/templates/redis/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/redis/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/smtp/Dockerfile b/decnet/templates/smtp/Dockerfile index c7bf5c83..68d28efa 100644 --- a/decnet/templates/smtp/Dockerfile +++ b/decnet/templates/smtp/Dockerfile @@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* COPY syslog_bridge.py /opt/syslog_bridge.py +COPY instance_seed.py /opt/instance_seed.py COPY server.py /opt/server.py COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/decnet/templates/smtp/instance_seed.py b/decnet/templates/smtp/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/smtp/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0)