// the cookbook

The Downrange Cookbook

How to bend a range to a real network — in a text file. Four chapters: range-as-code, the supported stack, firewall recipes, and how a run is scored. Built around one worked example, the Meridian Trust bank. No sign-up, no email wall.

Chapter 1 — Range-as-code: bend a range to your environment

Who this is for. Anyone who can edit a text file. You do not need to be a developer. If you’ve ever edited a config file or a YAML playbook, you already have every skill this chapter needs. By the end you’ll be able to take a stock range and reshape it to mirror a real network — its subnets, its hosts, its firewall — and launch it.


1.1 What “range-as-code” actually means

A Downrange range is described by a single text file: a scenario, written in YAML. The file doesn’t do anything — it describes a target network, and the platform reads it and builds that network for real: boots the machines, wires the segments, applies the firewall, and (optionally) turns a live adversary loose on it.

Think of it as a blueprint, not a program. You’re not laying bricks; you’re drawing labeled rooms. Change a label, get a different building.

The only YAML rules you need:

  • key: value — a setting and its value.
  • Indentation uses spaces, never tabs. Indentation shows what belongs inside what.
  • - (dash space) starts an item in a list.
  • { a: 1, b: 2 } is a compact one-line block — same meaning as indenting, just shorter.

That’s the whole syntax. Everything below is just those four rules applied.


1.2 The shape of a scenario

Every scenario has the same top-level sections, in this order. You’ll spend 90% of your editing time in just three of them: segments, nodes, and adversary.

SectionWhat it describesHow often you edit it
metadataName, title, difficulty, durationPer customer (cosmetic)
segmentsThe network zones / VLANsOften — first thing you change
nodesThe machines and tooling in each zoneOften
linksWhich node connects to whichRarely
adversaryThe attack to runSometimes
blue_teamWhat the defenders run (SIEM, agents)Sometimes
scoringWhat counts as a catch, and its pointsRarely
reportThe after-action outputRarely
runtimeCost + lifecycle guardrailsRarely

Two required header lines never change: apiVersion: downrange/v1 and kind: Scenario. They tell the parser what it’s reading.


1.3 The worked example: mirroring a customer

The companion file meridian-bank-scenario.yaml takes the stock ad-lateral-movement range and reshapes it to look like a fictional regional bank, Meridian Trust. Open it alongside this chapter. Below we walk the three sections you’d change in front of a customer.

Step 1 — Their network zones (segments)

This is the first question you ask a customer: “What are your subnets?” Then you type them. Meridian runs three zones:

segments:
  endpoints:                 # their workstation VLAN
    cidr: 10.40.10.0/24
    gateway: 10.40.10.1
  datacenter:                # their server VLAN
    cidr: 10.40.20.0/24
    gateway: 10.40.20.1
  security:                  # the SOC / tooling VLAN
    cidr: 10.40.99.0/24
    gateway: 10.40.99.1

Each segment needs a cidr (the subnet) and a gateway (an IP in that subnet). The names (endpoints, datacenter, security) are yours to choose — use names that match how the customer talks about their own network.

Step 2 — Their machines (nodes)

Each machine is a node. A node needs only two things: kind (vm or container) and image (which build to boot). Everything else — segment, ram, role, crown_jewel — is optional flavor.

nodes:
  mtdc01:                     # the customer's DC hostname
    kind: vm
    image: winsrv-adds        # the Windows domain-controller golden
    segment: datacenter
    crown_jewel: true         # this is the prize the attacker wants
    telemetry: [windows-security]

To mirror a customer: rename the node to their real hostname, place it in the right segment, and mark the box that matters most with crown_jewel: true.

The one rule about image. You can type any image name — the schema doesn’t restrict it — but the name only works if Downrange actually has a working build for it. See Chapter 2 — The Supported Stack for the catalog of what boots today, what’s bring-your-own-license, and what’s on the roadmap. Typing image: splunk does not make a Splunk box appear unless that image exists in the catalog.

Step 3 — Their firewall (policy)

This is the section a network engineer will love, because it reads exactly like an ACL. Firewall rules live under the fw node as an ordered list of allow/deny rules between segments:

policy:
  - allow: { from: endpoints,  to: datacenter, ports: [smb/445, rdp/3389, any:kerberos/88, any:ldap/389, any:dns/53] }
  - allow: { from: endpoints,  to: security,   ports: [wazuh-events/1514, wazuh-enroll/1515] }
  - deny:  { from: endpoints,  to: security }
  - deny:  { from: datacenter, to: endpoints }

How to read it:

  • fromto are segment names. Rules apply to traffic crossing between zones.
  • ports is a list of name/number tokens. The name is just a label for humans; the number is what’s enforced. Prefix with any: to open both TCP and UDP (needed for AD ports like Kerberos and LDAP that use both).
  • It is default-deny: anything not explicitly allowed is dropped.
  • Order matters — first matching rule wins, exactly like a real firewall.

To mirror a customer’s firewall: get their ruleset (or an export from their pfSense / Palo / Fortinet), and translate the segment-to-segment rules into these lines. This is often the single most credible moment in a demo — you’re speaking their language back to them.

Behind the scenes, Downrange renders this policy into real nftables rules on the router’s forward chain. You never touch nftables; you write the readable version and the platform compiles it.


1.4 The adversary: what runs, and what doesn’t (yet)

The adversary section defines the attack. The key concept:

A step in the attack chain only runs if a matching capability (“ability”) exists for its technique. If you list a technique with no ability, the launcher refuses to start — on purpose. A half-armed adversary that silently skips steps is worse than one that loudly tells you it can’t.

So the attack chain is like a playlist: you can only add songs that are in the library. Today’s library — the techniques that fire and score live — is the domain-controller-side chain:

StepMITRE techniqueWhat it does
footholdT1078Validate stolen domain credentials
pass-the-hashT1550Authenticate to the DC with a stolen hash
kerberoastT1558.003Request a crackable ticket for a service account
dcsyncT1003.006Replicate secrets from the DC (the “game over” move)

The endpoint-side steps (process execution, LSASS credential dumping on a workstation) are roadmap — they need the Azure-delivered Windows endpoint track. In the Meridian file they’re written as commented-out lines so the intent is visible and honest, but the live chain is the four above.

On adding things like ransomware: you cannot simply append ransomware to the chain. A new attack behavior needs three things built first — an ability that performs it, a detection rule that catches it, and scoring wired to that rule. Until those exist, the step has nothing to run, nothing to detect, and nothing to score. New attack content is authored work, not a word you drop in. (Destructive behaviors like ransomware also warrant extra lab-safety care even once built.)


1.5 Launch, score, tear down

Once the file is right, the lifecycle is three actions:

  1. Launch — the platform clones the machines, wires the network, applies the firewall, and brings the range up in an isolated namespace. Minutes, not weeks.
  2. Run the exercise — the adversary executes; the blue team detects, contains, responds. Detections fire and score on real telemetry.
  3. Tear down — everything is reclaimed to zero, metered by the range-hour. Nothing is left running.

1.6 The demo script (≈10 minutes)

A repeatable way to show this to a design partner. Notice you barely touch a terminal — most of the impact is in the file.

  1. Open the file, not the app. “This file is the range. Let me point at a line and tell you what it becomes.” Point at crown_jewel: true: “That’s the domain controller the attacker is hunting.”
  2. Make it theirs, live. “What’s your server VLAN?” — edit the cidr. “Your DC’s hostname?” — edit the node name. You’ve just bent the range to their world in thirty seconds, with no statement of work.
  3. Launch it. One command. Narrate while it builds: “It’s cloning a real Windows DC, wiring the network, sealing it off from everything else.”
  4. Fire the adversary. The chain runs against the DC — pass-the-hash, kerberoast, DCSync. “This is a real attack on a real domain controller.”
  5. Show the score. Detections fire and grade in the after-action view. “Your team’s response, scored on real telemetry — here’s what was caught and how fast.”
  6. Tear down. One command, back to zero. “Metered by the hour. Nothing left to clean up.”

The arc the buyer feels: here’s a text file → watch me make it your network → watch it come alive → watch it get attacked → watch us catch it → gone.


1.7 Honesty notes (say these plainly)

Being straight about the boundary is what makes you credible. The truthful framing for today:

  • Boots live today: the Windows server tier (domain controller, file server), the Linux tooling (SIEM, router/firewall, sensor), and the DC-side adversary chain that fires and scores.
  • Roadmap (designed, not yet live): the Windows endpoint tier (workstations via the Azure track), the endpoint-side detections that depend on it, and bring-your-own SIEM/EDR integrations (the byo-splunk / byo-sentinel console options are already recognized by the schema — the images behind them are the build work).

“Here’s what’s live, here’s what’s next” beats a suspiciously perfect demo every time — especially with security buyers.


Next: Chapter 2 — The Supported Stack (what boots today, what’s BYOL, what’s roadmap) and Chapter 3 — Firewall recipes (translating common customer firewall postures into policy blocks).

Worked example — the Meridian Trust scenario

The complete companion file from Chapter 1. It takes the stock ad-lateral-movement range and bends it to a fictional regional bank — its VLANs, its hostnames, its firewall — by editing text. Lines marked # >> DEMO are the ones you'd edit live in front of a customer; # ROADMAP lines are designed-but-not-yet-live, called out honestly.

meridian-bank-scenario.yaml download raw ↓

# =============================================================================
#  meridian-bank.yaml  —  a CUSTOMER-MIRRORED range, built as a teaching example
# =============================================================================
#
#  Scenario: a fictional mid-size regional bank, "Meridian Trust."
#  This file shows how you take the stock ad-lateral-movement range and bend it
#  to look like a real customer's environment — their VLANs, their hostnames,
#  their firewall posture — by editing TEXT. No programming.
#
#  Every field below is real (validated by the downrange/v1 schema). Lines
#  marked  # >> DEMO  are the ones you'd edit live in front of a design partner.
#  Lines marked  # ROADMAP  describe capability that is designed but not yet
#  live — say so honestly; don't imply it boots today.
#
#  YAML rules you need (that's all): key: value, indent with SPACES (never
#  tabs), "- " starts a list item, "{ }" is a compact inline block. Done.
# =============================================================================

apiVersion: downrange/v1        # never change — tells the parser the format
kind: Scenario                  # never change

# ---- metadata: the label on the box -----------------------------------------
metadata:
  name: meridian-bank                       # kebab-case id (a-z, 0-9, dashes)
  title: "Meridian Trust — AD lateral movement"   # >> DEMO: customer's name
  difficulty: intermediate                  # beginner | intermediate | advanced
  est_duration_minutes: 45
  mitre_tactics:                            # the ATT&CK tactics this exercises
    - initial-access
    - credential-access
    - lateral-movement

# ---- segments: the customer's network zones (their VLANs) --------------------
#  THIS is the first thing you change for a customer. Ask: "what are your
#  subnets?" and type them here. Below is Meridian's addressing, not ours.
segments:
  endpoints:                                # >> DEMO: their workstation VLAN
    cidr: 10.40.10.0/24
    gateway: 10.40.10.1
  datacenter:                               # >> DEMO: their server VLAN
    cidr: 10.40.20.0/24
    gateway: 10.40.20.1
  security:                                 # the SOC / tooling VLAN
    cidr: 10.40.99.0/24
    gateway: 10.40.99.1

# ---- nodes: the machines that live in those zones ----------------------------
#  Each node needs only `kind` and `image`. Everything else is optional.
#  `image` must be something we have a working golden/container for (see the
#  Supported Stack list) — typing a name we don't build won't conjure it.
nodes:

  # -- the domain controller: the crown jewel the attacker is after ----------
  mtdc01:                                    # >> DEMO: their real DC hostname
    kind: vm
    image: winsrv-adds                       # the only Windows DC golden today
    segment: datacenter
    ram: 4G
    role: domain-controller
    crown_jewel: true                        # marks the prize for scoring
    telemetry: [windows-security]            # which logs feed the SIEM

  # -- a member file server --------------------------------------------------
  mtfs01:                                    # >> DEMO: their file server name
    kind: vm
    image: winsrv-fileserver
    segment: datacenter
    ram: 4G
    role: fileserver

  # -- an analyst workstation (the adversary's foothold) ---------------------
  #  ROADMAP: win11 endpoints are delivered via the Azure track (ADR 0002) and
  #  do NOT boot on the local cluster yet. Endpoint-side detections (process
  #  exec, LSASS access) light up when that track lands. Keep the node in the
  #  file so the topology is honest about what the network contains.
  teller-ws:                                 # >> DEMO: their workstation name
    kind: vm
    image: win11-domainjoined                # ROADMAP: Azure-delivered
    segment: endpoints
    role: foothold

  # -- the router/firewall: one node, owns the segment policy ----------------
  rtr:
    kind: container
    image: frrouting/frr:latest
    # rtr bridges all segments; it has no single `segment`.

  fw:
    kind: container
    image: downrange/nftables-fw:latest
    # the fw node OWNS the firewall policy below but renders no pod of its own —
    # the rules are enforced inside the rtr's network namespace.
    policy:                                  # <<< THE FIREWALL — see section 2
      # Read like an ACL: from-zone -> to-zone -> allowed ports. Default-deny,
      # first match wins. This is where you mirror a customer's firewall.
      - allow: { from: endpoints,  to: datacenter, ports: [smb/445, rdp/3389, any:kerberos/88, any:ldap/389, any:dns/53] }
      - allow: { from: endpoints,  to: security,   ports: [wazuh-events/1514, wazuh-enroll/1515] }
      - deny:  { from: endpoints,  to: security }              # block the rest to the SOC
      - deny:  { from: datacenter, to: endpoints }             # servers don't dial workstations

  # -- the blue-team SIEM console --------------------------------------------
  siem:
    kind: container
    image: downrange/wazuh-aio:latest
    segment: security
    ram: 4G
    role: blue-team-console

# ---- links: which node interfaces connect to which ---------------------------
#  Format is  node:iface . For most ranges the defaults are fine; you rarely
#  hand-edit this once the segments and nodes are right.
links:
  - [rtr:eth1, endpoints:gw]
  - [rtr:eth2, datacenter:gw]
  - [rtr:eth3, security:gw]

# ---- adversary: the attack to run --------------------------------------------
#  engine: caldera drives a real attack. A ttp_chain step only RUNS if a Caldera
#  ability exists for its technique — otherwise the launcher refuses to start
#  (by design: "a half-armed adversary is worse than a loud failure").
#
#  RUNNABLE TODAY = the four DC-side techniques: T1078, T1550, T1558.003,
#  T1003.006. The endpoint steps (exec/creddump on the workstation) are ROADMAP
#  — they need the Azure endpoint track. This chain is written to run live now.
adversary:
  engine: caldera
  pace: medium                               # slow | medium | fast
  entry: { from: attacker, to: teller-ws, technique: T1566 }   # how the adversary gets in
  objective: domain-admin@mtdc01             # the win condition
  ttp_chain:
    - { step: foothold, mitre: T1078,     on: mtdc01 }         # validate stolen creds
    - { step: pth,      mitre: T1550,     on: mtdc01 }         # pass-the-hash to DA
    - { step: kerberoast, mitre: T1558.003, on: mtdc01 }       # roast the SQL service account
    - { step: dcsync,   mitre: T1003.006, on: mtdc01 }         # replicate krbtgt secrets
    # ROADMAP (needs Azure endpoint track — HAR-34 / HAR-60):
    # - { step: exec,     mitre: T1059, on: teller-ws }
    # - { step: creddump, mitre: T1003, on: teller-ws }

# ---- blue_team: what the defenders are running -------------------------------
#  console can be wazuh today; byo-splunk / byo-sentinel are roadmap values
#  already recognized by the schema — that's the "bring your own SIEM" seam.
blue_team:
  console: wazuh                             # ROADMAP options: byo-splunk, byo-sentinel
  endpoint: [wazuh-agent, sysmon]
  response_actions: [isolate-host, cut-segment]

# ---- scoring: what counts as a catch, and what it's worth --------------------
#  Each objective ties a detection rule to points + an SLA. The `rule` must
#  match a rule in the ruleset (format  source:number ). These four are the
#  DC-side detections that fire and score live today.
scoring:
  ruleset: meridian-detections
  objectives:
    - { id: pth,        mitre: T1550,     signal: ntlm-auth,  rule: "wazuh:100140", points: 20, sla_minutes: 10 }
    - { id: kerberoast, mitre: T1558.003, signal: kerb-tgs,   rule: "wazuh:100160", points: 20, sla_minutes: 10 }
    - { id: dcsync,     mitre: T1003.006, signal: dir-replication, rule: "wazuh:100150", points: 25, sla_minutes: 5 }
  containment:
    - { id: contain-before-dc, description: "Isolate the foothold before DCSync completes", type: boolean, points: 75 }
  metrics: [mttd, mttr, contained_before_dc, mitre_coverage]

# ---- report: the after-action output -----------------------------------------
report:
  template: after-action-v1
  surface: [score, timeline, mitre-map]

# ---- runtime: cost + lifecycle guardrails ------------------------------------
runtime:
  idle_suspend_minutes: 30                   # suspend an idle range (stop metering)
  hard_ttl_minutes: 180                      # hard kill at 3h so nothing runs forever
  metered: true                              # bill by the range-hour

Chapter 2 — The Supported Stack

What this chapter answers. The first question every buyer asks: “Does it work with my stuff?” This is the catalog of what you can put in a scenario — what boots today, what’s bring-your-own-license, and what’s on the roadmap. When you write image: or console: in a scenario, this page is the menu you choose from.

A note on how this differs from a tool like EVE-NG. Network-emulation platforms publish a long “how to add image X” index because the user supplies and builds every image themselves. Downrange is the opposite promise: the images in the Boots today tier are built and maintained for you, so a range comes up clean with no assembly. This list is therefore a catalog of what’s supported, not a pile of build-it-yourself tutorials. Shorter on purpose — that’s the point of self-serve.


2.1 How to read this catalog

Three tiers, and a scenario can only use what’s real today:

TierMeaningWhat you can do now
Boots todayBuilt, maintained, comes up cleanUse freely in any scenario
Bring your own (roadmap)Designed; the seam exists in the schemaDiscuss with us; not yet self-serve
AdversaryThe attack content the chain can runUse the live techniques today

What happens if I reference something not in the catalog? The image: field accepts any text, but a name only resolves if a real build exists behind it. If a scenario asks for something unavailable, Downrange tells you before it launches — naming exactly what’s missing and what to do — rather than failing halfway up or, worse, quietly running a weaker exercise. (See Chapter 1, §1.4 and the launcher-behavior decision.)


2.2 Boots today — the live catalog

These are the images a range clones and boots on the cluster right now.

Windows hosts (VMs)

image:RoleNotes
winsrv-addsWindows Server — Active Directory domain controllerThe range.local DC. The usual crown_jewel.
winsrv-fileserverWindows Server — member file serverDomain-joined member.

The endpoint caveat (important and honest). Windows workstation images (win11-domainjoined) are delivered via the Azure track and do not boot on the local cluster yet. You can list a workstation node in a scenario to model the topology truthfully, but it won’t power on here, and endpoint-side detections (process execution, LSASS access) depend on it. Say this plainly to customers: the server tier is live; the workstation tier is the Azure roadmap.

Linux / container nodes

image:Role
downrange/wazuh-aioBlue-team SIEM console + indexer (the detection brain)
downrange/kali-calderaThe adversary host (attacker agent)
downrange/nftables-fwThe firewall policy enforcer
frrouting/frrThe router that bridges segments
downrange/zeek-suricataNetwork sensor (NIDS)
downrange/velociraptorDFIR / endpoint forensics server

A scenario doesn’t have to use all of these. A minimal range is a DC, a SIEM, and a router. Add the sensor and DFIR nodes when the exercise calls for them.

Blue-team consoles (blue_team.console)

ValueStatus
wazuhLive today — the default detection console
security-onionRecognized by the schema
elastic-securityRecognized by the schema

2.3 Bring your own (roadmap)

This is the enterprise direction: run a range with the security tools you already own and license. The seams exist in the architecture and schema today; the images and wiring behind them are the build work. Three integration patterns:

Pattern A — self-hosted tools as in-range nodes. Tools that run inside the range as their own box — a self-hosted Splunk, Elastic, or a Nessus/Tenable scanner. These fit the existing node model (kind + image). You bring the license; it’s injected at launch as a per-range secret, never baked into an image.

Pattern B — agents that report to your cloud tenant. EDR/XDR whose console is your SaaS, not the range — Microsoft Defender, Sentinel, CrowdStrike, SentinelOne. The agent installs on a host and phones home to your tenant. The new piece is controlled egress: ranges are sealed by default, so this opens a deliberate, allowlisted path to just that vendor’s endpoint.

Pattern C — score from your console. Because the scoring engine reads a generic alert stream, the detection source can be swapped. The console field already recognizes byo-splunk and byo-sentinel as values — the seam is in the schema. The clients behind them (read your Splunk/Sentinel via API, with read-only credentials you supply) are the roadmap work.

console: valuePatternStatus
byo-splunkC — score from your SplunkRoadmap (schema seam exists)
byo-sentinelC — score from your SentinelRoadmap (schema seam exists)

The pitch this unlocks: “Run our live adversary against a replica of your network, with your real EDR and your real SIEM and your real detection rules — and we’ll score what your stack actually caught.” That’s detection-validation of a customer’s production tooling, not generic training. It’s the enterprise wedge the closed platforms can’t follow without unlocking their own consoles.

How to talk about this tier: “You pick from our supported catalog and bring your license; cloud EDR agents report to your tenant. A tool we don’t yet have an image for is integration work we scope together.” Never imply BYO tools self-serve today.


2.4 Adversary — what the attack chain can run

The adversary runs abilities mapped to MITRE ATT&CK techniques. A ttp_chain step only fires if an ability exists for its technique (Chapter 1, §1.4).

Live today — the domain-controller chain

TechniqueStepWhat it does
T1078footholdValidate stolen domain credentials
T1550pass-the-hashAuthenticate to the DC with a stolen hash
T1558.003kerberoastRequest a crackable ticket for a service account
T1003.006dcsyncReplicate DC secrets — the “game over” move

These four fire against the domain controller and score live on real telemetry.

Roadmap — endpoint and beyond

TechniqueStepNeeds
T1566entry / phishingEndpoint host (Azure track)
T1059execEndpoint host (Azure track)
T1003creddumpEndpoint host (Azure track)
T1021lateralEndpoint host (Azure track)

On custom attacks (e.g. ransomware): new attack content isn’t a word you drop into the chain. Each technique needs three things built — an ability that performs it, a detection rule that catches it, and scoring wired to that rule. Until all three exist, the step has nothing to run, detect, or score. New adversary content is authored work (and destructive behaviors like ransomware warrant extra lab-safety review even once built).


2.5 The engine (adversary.engine)

ValueStatus
calderaLive today — drives the real attack chain
aiRecognized by the schema; roadmap

2.6 The honest one-paragraph summary (for a buyer)

“Today, Downrange boots a real Windows Active Directory environment — domain controller and file server — plus the blue-team tooling: a Wazuh SIEM, a router/firewall, a network sensor, and a DFIR server. A live Caldera adversary runs a domain-controller attack chain — pass-the-hash, kerberoast, DCSync — and your team’s detections are scored on real telemetry. On the roadmap: Windows endpoints via Azure, and bring-your-own SIEM and EDR so you can validate detections on the exact stack you run in production. The scoring engine is console-agnostic by design, which is what makes that roadmap real rather than aspirational.”


Next: Chapter 3 — Firewall recipes (translating common customer firewall postures — flat network, segmented, zero-trust microsegmentation — into policy blocks).

Chapter 3 — Firewall recipes

What this chapter is. A pattern library. Most customer networks fall into a handful of firewall postures — flat, segmented, zero-trust. This chapter shows each one as a ready-to-paste policy block, plus the building blocks to mix your own. If you can read an ACL, you can write these.


3.1 The model in one minute

Firewall policy lives under the fw node as an ordered list of allow / deny rules between segments. It behaves like a real firewall:

  • Default-deny. Anything you don’t explicitly allow is dropped. You only write the holes you want open.
  • First match wins. Rules are evaluated top to bottom; the first one that matches a packet decides it. Order is significant — put specific allows above broad denies.
  • Segment to segment. Rules match on from / to segment names (which become subnet CIDRs under the hood), not individual hosts.

Each rule:

- allow: { from: <segment>, to: <segment>, ports: [ <token>, ... ] }
- deny:  { from: <segment>, to: <segment> }          # ports optional on a deny

Port tokens are name/number, optionally prefixed with a protocol:

Token formMeaning
smb/445TCP port 445 (TCP is the default)
tcp:rdp/3389Explicit TCP 3389
udp:syslog/514UDP 514
any:dns/53Both TCP and UDP 53

The name is a human label — call it whatever’s clear. The number is what’s actually enforced. Use any: for the AD discovery ports (Kerberos 88, LDAP 389, DNS 53) because they ride both TCP and UDP.

Downrange compiles this into real nftables rules on the router’s forward chain. You write the readable version; the platform renders the firewall.


3.2 The AD essentials block (memorize this one)

Almost every Windows range needs workstations to reach domain controllers for authentication. This is the canonical allow:

- allow: { from: workstations, to: servers, ports: [smb/445, rdp/3389, any:kerberos/88, any:ldap/389, any:dns/53] }

What each port is doing, so you can explain it in a demo:

PortPurpose
445 (SMB)File shares, and the channel several attacks ride
3389 (RDP)Remote desktop
88 (Kerberos)Domain authentication — any: (TCP+UDP)
389 (LDAP)Directory lookups — any:
53 (DNS)Name resolution, including locating the DC — any:

If a customer’s workstations can log into the domain, these five are open somewhere in their network. This block models that reality.


3.3 Recipe: flat network (small shop)

Everything can talk to everything; the firewall mostly keeps the SOC segment clean. Common in very small orgs. The teaching point in a demo: “this is how most breaches spread laterally — nothing stops them.”

policy:
  - allow: { from: workstations, to: servers }            # no ports = all ports
  - allow: { from: servers,      to: workstations }
  - allow: { from: workstations, to: mgmt, ports: [wazuh-events/1514, wazuh-enroll/1515] }
  - allow: { from: servers,      to: mgmt, ports: [wazuh-events/1514, wazuh-enroll/1515] }
  - deny:  { from: workstations, to: mgmt }               # SOC segment stays isolated
  - deny:  { from: servers,      to: mgmt }

A deny with no ports, and an allow with no ports, match all traffic between those segments. Use sparingly — that’s exactly the over-permissiveness you’re often there to expose.


3.4 Recipe: segmented network (typical mid-market)

Workstations reach servers only on the ports they need; servers never initiate back to workstations; the SOC segment is reachable only for telemetry. This is the most common real customer posture and the one the Meridian Bank example uses.

policy:
  - allow: { from: workstations, to: servers, ports: [smb/445, rdp/3389, any:kerberos/88, any:ldap/389, any:dns/53] }
  - allow: { from: workstations, to: mgmt,    ports: [wazuh-events/1514, wazuh-enroll/1515] }
  - deny:  { from: workstations, to: mgmt }                # block everything else to the SOC
  - deny:  { from: servers,      to: workstations }        # servers don't dial workstations

The two trailing denies are the whole point: the allow above them opens exactly the telemetry ports to mgmt, then the broad deny closes the rest — order matters, the specific allow must come first. And servers having no path back to workstations is a real containment control: it limits how far an attacker who owns a server can pivot.


3.5 Recipe: zero-trust / microsegmentation (mature SOC)

Tightest posture. Every allow is justified; the file reads as an explicit inventory of permitted flows. Use when a customer prides themselves on segmentation — mirroring it back shows you take their maturity seriously.

policy:
  # workstations: only what's needed to authenticate and report telemetry
  - allow: { from: workstations, to: servers, ports: [any:kerberos/88, any:ldap/389, any:dns/53] }
  - allow: { from: workstations, to: servers, ports: [smb/445] }      # file shares, called out separately on purpose
  - allow: { from: workstations, to: mgmt,    ports: [wazuh-events/1514, wazuh-enroll/1515] }
  # servers: talk to the DC and to telemetry, nothing else
  - allow: { from: servers, to: servers, ports: [any:kerberos/88, any:ldap/389, any:dns/53] }
  - allow: { from: servers, to: mgmt,    ports: [wazuh-events/1514, wazuh-enroll/1515] }
  # everything not listed above is denied by default — no explicit denies needed

In a default-deny model you often don’t need deny lines at all — the absence of an allow is the deny. Explicit denies are for when you want to short- circuit above a broader allow (the segmented recipe in §3.4 shows that).


3.6 Translating a customer’s real firewall

The demo move your background makes powerful. Ask the customer for their ruleset (or a screenshot of their pfSense / Palo / Fortinet rules), then:

  1. Map their zones to segments. Their “USER” VLAN → workstations, “SERVER” / “DMZ” → servers, “MGMT” / “SOC” → mgmt. Rename freely; segment names are yours.
  2. Take the allows, drop the noise. Their ruleset has dozens of rules; you need the handful that govern inter-zone traffic. Ignore intra-zone and internet-bound rules for the range.
  3. Write each cross-zone allow as a line. Their “USERS may reach SERVERS on 445, 3389, 88, 389, 53” becomes the AD essentials block from §3.2.
  4. Preserve their order, since first-match-wins matches how their firewall already thinks.
  5. Point out the gaps. If their workstations can reach servers on all ports, model it honestly — and note it. That over-permissiveness is often exactly the lateral-movement path the exercise will exploit. Now your range proves the risk instead of asserting it.

That last step is the consultative sell: you’re not just mirroring their network, you’re using the mirror to show them where an attacker walks.


3.7 Gotchas

  • Spaces, not tabs — YAML rejects tabs. (True everywhere, worth repeating.)
  • any: for AD ports — Kerberos / LDAP / DNS use UDP too; TCP-only allows will cause flaky, hard-to-debug domain behavior.
  • SMB and RDP are TCP-only — don’t wrap them in any:; it’s harmless but misleading to a reader.
  • Order is real — a broad deny placed above a specific allow will swallow it. Specific first, broad last.
  • from/to must be defined segments — a typo’d segment name fails at render with an error naming the bad reference. (Same fail-early-and-clearly principle as the adversary launcher.)

Next: Chapter 4 — Scoring and the after-action report (how detections map to points, what MTTD/MTTR mean in the report, and how containment is scored).

Chapter 4 — Scoring and the after-action report

What this chapter is. A run ends in a score and an after-action report. This chapter explains how the score is built, why the design rewards containment over raw detection, and how to read — and explain — the report a customer walks away with. Understand this and you can answer the question that closes deals: “What does my team actually get out of a run?“


4.1 The idea: graded on real telemetry, not a quiz

Downrange doesn’t ask the analyst multiple-choice questions. It runs a real attack, watches what the blue team’s tooling actually detected and did, and grades that. The score answers three questions:

  1. Did you see it? (detection — did the right alert fire?)
  2. How fast? (timeliness — within the SLA, or too late?)
  3. Did you stop it? (containment — did you cut the attacker off before the crown jewel fell?)

That third question is the one most training tools miss, and it’s where the design has an opinion worth explaining.


4.2 The scoring section, field by field

From a real scenario:

scoring:
  ruleset: meridian-detections
  objectives:
    - { id: pth,        mitre: T1550,     signal: ntlm-auth,       rule: "wazuh:100140", points: 20, sla_minutes: 10 }
    - { id: kerberoast, mitre: T1558.003, signal: kerb-tgs,        rule: "wazuh:100160", points: 20, sla_minutes: 10 }
    - { id: dcsync,     mitre: T1003.006, signal: dir-replication, rule: "wazuh:100150", points: 25, sla_minutes: 5 }
  containment:
    - { id: contain-before-dc, description: "Isolate the foothold before DCSync completes", type: boolean, points: 75 }
  metrics: [mttd, mttr, contained_before_dc, mitre_coverage]

objectives — each one is a detection worth scoring:

FieldWhat it means
idA short unique name for this objective
mitreThe ATT&CK technique it corresponds to
signalA human label for what the detection keys on
ruleThe detection rule that must fire, as source:number (e.g. wazuh:100140)
pointsWhat detecting it is worth
sla_minutesThe window to detect it in to earn full credit

The rule is the link between the attack and the score: when that rule fires in the SIEM during the run, the objective is met. The points weight tells the trainee what matters most — note dcsync is worth more (25) and has the tightest SLA (5 min), because it’s the “game over” move.

containment — points for stopping the attack, not just seeing it:

FieldWhat it means
idShort unique name
descriptionPlain-language statement of what counts as contained
typeboolean — either they contained it in time or they didn’t
pointsWhat containment is worth (often the largest single award)

metrics — what the report computes and shows: mttd (mean time to detect), mttr (mean time to respond), contained_before_dc (did they cut it off before the DC fell), mitre_coverage (how much of the attack’s ATT&CK footprint was seen).


4.3 The design opinion: containment beats detection

Here’s the most important thing to understand, because it’s a genuine product philosophy and a great demo talking point.

Notice in the example that containment is worth 75 points — more than all three detections combined. That is deliberate. Consider a team that:

  • Detects the early steps, but
  • Misses one detection entirely (say, they have no rule for one technique), yet
  • Recognizes the intrusion and isolates the foothold before DCSync completes.

In a naive scoring model that team gets penalized for the missing detection. In Downrange they get rewarded, because they did the thing that actually mattered: they stopped the breach before the crown jewel fell. A missed alert with a contained outcome is a better result than every alert firing while the domain gets owned.

This is the line to say out loud in a demo:

“We don’t just score whether your tools saw the attack. We score whether your team stopped it. You can miss a detection and still win the exercise — if you contained the breach before it reached the domain controller. That’s how real incident response is judged.”

It reframes the whole exercise from “did your SIEM light up” (a tooling question) to “did your team win” (an outcome question) — which is what a SOC lead actually cares about.


4.4 Reading the after-action report

report:
  template: after-action-v1
  surface: [score, timeline, mitre-map]

The report (after-action-v1) surfaces three things:

  • Score — the total, broken down by objective and containment, so a team sees exactly where points came from and where they were left on the table.
  • Timeline — the attack and the response on one clock: when each technique fired, when each detection alerted, when containment happened. This is where MTTD and MTTR become visible — the gaps between attacker action and defender response are the coaching moments.
  • MITRE map — the attack laid over the ATT&CK matrix, showing coverage: which techniques were detected, which slipped through. This is the artifact a SOC lead forwards to their boss — it speaks the language security leadership reports in.

The containment callout. When a team contains the breach, the report states it plainly — e.g. “DCSync prevented; +75 containment.” The point is that the trainee sees they were rewarded for the right call, not dinged for the missing detection. The report teaches the lesson, not just the number.


4.5 How a run is judged — the worked example

Walk a customer through Meridian’s scoring as a story:

  1. The adversary lands a foothold and validates stolen creds (T1078).
  2. It pass-the-hashes to the DC (T1550) → if rule wazuh:100140 fires within 10 min, +20.
  3. It kerberoasts the SQL service account (T1558.003) → rule wazuh:100160+20.
  4. It runs DCSync to replicate krbtgt (T1003.006) → rule wazuh:100150 within 5 min → +25.
  5. The decisive moment: if the team isolated the foothold before DCSync completed → +75 containment, and the report reads “DCSync prevented.”

Two teams can run the identical attack and score very differently — and the team that contained outscores the team that merely watched. That contrast is the demo’s emotional peak: run it twice, contain once, and let the customer see the difference in the report.


4.6 Tuning scoring for a customer

Scoring is editable text, like everything else:

  • Reweight what matters to them. A customer obsessed with ransomware dwell time might weight early detection higher; one focused on crown-jewel protection leans on containment. Change the points.
  • Tighten or loosen SLAs. A mature SOC might demand a 2-minute DCSync SLA; a developing team might start at 15. Change sla_minutes.
  • Add objectives as detections come online. Each new detection rule becomes a new objective line. (Remember: an objective’s rule must correspond to a real rule in the ruleset, and the attack must actually run the technique — scoring, detection, and adversary ability are the three legs, per Chapter 1 §1.4.)

One constraint from the schema: objective ids and rules must be unique within a scenario, and every rule follows the source:number form (e.g. wazuh:100150). A duplicate or malformed rule fails at validation with a clear message — fail-early, same as everywhere else.


4.7 The buyer’s takeaway (say this)

“At the end of a run your team gets an after-action report: a score broken down by what you detected and what you contained, a timeline showing how fast you responded, and an ATT&CK map of what the attack touched. It’s graded on your real telemetry, and it rewards stopping the breach — not just seeing it. It’s the artifact you hand your leadership to show, in their language, exactly how ready your team is.”


This completes the core cookbook (Chapters 1–4): write a range, know the stack, shape the firewall, read the score. Later chapters can cover the integrations roadmap (BYO SIEM/EDR), authoring new detections, and the API surface.

← Back to downrange.pro Download the cookbook as PDF ↓