Add Mutation and Randomization page
102
Mutation-and-Randomization.md
Normal file
102
Mutation-and-Randomization.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Mutation and Randomization
|
||||||
|
|
||||||
|
DECNET's value as a deception network depends on the decoy fleet looking heterogeneous at deploy time and shifting over its lifetime. This page documents the two mechanisms that deliver that: randomization at build, and mutation at runtime.
|
||||||
|
|
||||||
|
See also: [CLI reference](CLI-Reference), [Archetypes](Archetypes), [Distros](Distro-Profiles).
|
||||||
|
|
||||||
|
## Randomization at deploy time
|
||||||
|
|
||||||
|
### `--randomize-services`
|
||||||
|
|
||||||
|
When `decnet deploy` is invoked with `--randomize-services`, each decky receives a randomly drawn service set instead of the fixed list passed via `--services` or the set implied by `--archetype`.
|
||||||
|
|
||||||
|
The selection logic lives in `build_deckies()` (`decnet/fleet.py`). For each decky:
|
||||||
|
|
||||||
|
1. A count `k` is drawn uniformly from `[1, min(3, len(pool))]`.
|
||||||
|
2. `k` service names are sampled without replacement from the pool (`all_service_names()`, or the archetype service list if an archetype is set).
|
||||||
|
3. The chosen set is compared against a `used_combos: set[frozenset]` that tracks combinations already assigned in this deploy. If it collides with an existing combination, the draw is retried.
|
||||||
|
4. After 20 retries, the last draw is accepted even if duplicated. With small pools and many deckies, exact uniqueness is not always possible — the retry cap prevents an infinite loop.
|
||||||
|
|
||||||
|
### Random hostnames
|
||||||
|
|
||||||
|
Hostnames are generated by `random_hostname(distro_slug)` in `decnet/distros.py`. The style depends on the distro profile's `hostname_style` field:
|
||||||
|
|
||||||
|
| Style | Example | Distros |
|
||||||
|
|-----------|-----------------------|---------------------------------|
|
||||||
|
| `generic` | `SRV-PROD-42` | Debian, Ubuntu |
|
||||||
|
| `rhel` | `web37.localdomain` | Rocky, CentOS, Fedora |
|
||||||
|
| `minimal` | `alpha-18` | Alpine |
|
||||||
|
| `rolling` | `nova-backup` | Kali, Arch |
|
||||||
|
|
||||||
|
Word pool and numeric range are defined in `_NAME_WORDS` and the `random.randint(10, 99)` call in the same file.
|
||||||
|
|
||||||
|
### Random distros
|
||||||
|
|
||||||
|
`random_distro()` picks a uniform-random entry from the `DISTROS` dict (`decnet/distros.py`). Each entry is a `DistroProfile` with a slug, a Docker image, a display name, a hostname style, and a build base image used for service Dockerfiles (which assume `apt-get`, so non-Debian distros fall back to `debian:bookworm-slim` for builds).
|
||||||
|
|
||||||
|
The current set: `debian`, `ubuntu22`, `ubuntu20`, `rocky9`, `centos7`, `alpine`, `fedora`, `kali`, `arch`.
|
||||||
|
|
||||||
|
### MAC addresses
|
||||||
|
|
||||||
|
MAC addresses are not assigned by DECNET. The MACVLAN driver auto-generates a MAC for each container interface at container start. There is no knob to pin or rotate them from the deploy config; if you need deterministic MACs, attach them out-of-band at the Docker network layer.
|
||||||
|
|
||||||
|
## Mutation at runtime
|
||||||
|
|
||||||
|
Randomization only fires at build time. To keep the fleet moving, DECNET supports per-decky service rotation.
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
|
||||||
|
Every `DeckyConfig` (`decnet/config.py`) carries two mutation-related fields:
|
||||||
|
|
||||||
|
- `mutate_interval: int | None` — minutes between rotations for this decky. `None` disables automatic rotation for that decky.
|
||||||
|
- `last_mutated: float` — Unix timestamp of the most recent successful mutation.
|
||||||
|
|
||||||
|
The top-level `DecnetConfig` also holds a fleet-wide `mutate_interval`, which defaults to `DEFAULT_MUTATE_INTERVAL = 30` (minutes) from `decnet/config.py`. Per-decky values override the fleet default.
|
||||||
|
|
||||||
|
### Engine
|
||||||
|
|
||||||
|
`decnet/mutator/engine.py` exposes three async entry points, all operating against a `BaseRepository`:
|
||||||
|
|
||||||
|
- `mutate_decky(decky_name, repo)` — Intra-archetype shuffle for one decky. Rebuilds the service list by sampling 1-3 services from the decky's archetype pool (or the full registry if no archetype is set), retrying up to 20 times to avoid picking the exact same set. Updates `last_mutated`, persists state, rewrites the compose file, and runs `docker compose up -d --remove-orphans`.
|
||||||
|
- `mutate_all(repo, force=False)` — Iterates all deckies. For each, computes `elapsed = now - last_mutated` and calls `mutate_decky` when `elapsed >= interval * 60`. `force=True` bypasses the schedule.
|
||||||
|
- `run_watch_loop(repo, poll_interval_secs=10)` — Infinite loop that calls `mutate_all` every `poll_interval_secs`. Invoked by `decnet mutate --watch`.
|
||||||
|
|
||||||
|
### Trigger from the CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deploy a fleet that rotates every 15 minutes
|
||||||
|
decnet deploy --mode unihost --deckies 5 --interface eth0 \
|
||||||
|
--randomize-services --mutate-interval 15
|
||||||
|
|
||||||
|
# Mutate a single decky now (forces immediately, ignores schedule)
|
||||||
|
decnet mutate --decky decky-03
|
||||||
|
|
||||||
|
# Mutate all deckies now
|
||||||
|
decnet mutate --all
|
||||||
|
|
||||||
|
# Run the watcher in the foreground
|
||||||
|
decnet mutate --watch
|
||||||
|
|
||||||
|
# Run the watcher as a detached daemon
|
||||||
|
decnet mutate --watch --daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
`decnet deploy` also starts a background watcher automatically; the standalone `decnet mutate --watch` is for operators who want to run the loop themselves.
|
||||||
|
|
||||||
|
## Operational trade-offs
|
||||||
|
|
||||||
|
Mutation is a blunt instrument. The interval you pick is a trade between deception fidelity and observability:
|
||||||
|
|
||||||
|
- **Short intervals (≤ 5 min)**: The fleet looks lively and fingerprint scans will never converge, but every rotation churns containers, rewrites compose files, and wipes short-lived attacker state — half-finished brute-force sessions, partially uploaded payloads, active TCP connections. You will lose IOCs that would otherwise have been captured.
|
||||||
|
- **Default (30 min)**: Reasonable balance. Most scan-and-go attackers see a consistent snapshot; long-dwell attackers see the fleet shift under them, which itself is a useful signal.
|
||||||
|
- **Long intervals (hours) or `None`**: The fleet looks static. An attacker who fingerprints twice gets identical results, which is not how real networks behave under patching and reconfiguration.
|
||||||
|
|
||||||
|
For research deployments where you want a specific attacker session recorded end-to-end, disable mutation on the decky under observation (`mutate_interval=None` on that decky only) while leaving the rest of the fleet rotating.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- `decnet/distros.py` — `DISTROS`, `random_hostname`, `random_distro`
|
||||||
|
- `decnet/fleet.py` — `build_deckies`
|
||||||
|
- `decnet/mutator/engine.py` — `mutate_decky`, `mutate_all`, `run_watch_loop`
|
||||||
|
- `decnet/config.py` — `DEFAULT_MUTATE_INTERVAL`, `DeckyConfig`, `DecnetConfig`
|
||||||
|
- `decnet/cli.py` — `deploy`, `mutate` commands
|
||||||
Reference in New Issue
Block a user