Skip to content

Latest commit

 

History

History

ansible

README.md

ansible

host: phoenix VLAN 30: ImaginationLAN Ansible

This directory configures the lab domain's six guests from phoenix (ADR-0077). The why is in build-the-lab-domain.md and ADR-0029. This page covers what is here and how to run it.

set -a; . ~/.config/proxmox/phoenix.env; set +a
scripts/lab-known-hosts.sh                       # after any of the six is rebuilt
cd ansible
ansible-playbook lab-domain.yml --check --diff   # what would change (existing domain only)
ansible-playbook lab-domain.yml                  # apply
ansible-playbook verify.yml                      # read-only proof

What --check reports instead of doing. On a guest this playbook has never touched, base would create C:\ProgramData\lab, but check mode does not. Downloading into it would fail, and the serial play would stop there for every guest after it. So exporter decides "installed" from the pinned MSI's product code, and licence_clock looks for the folder. Where the work is still to do, each one reports it as a pending change and skips it in check mode. A win_powershell script runs in check mode only if it declares [CmdletBinding(SupportsShouldProcess)]. Every script here that writes either does, and guards the write with $Ansible.CheckMode, or is skipped in check mode by when: not ansible_check_mode beside a read-only probe, as base's static address is. A new one that does neither reports a change on every --check instead of looking.

Run it from this directory, because ansible.cfg is read from the current directory. Installing Ansible and the collections on phoenix, the two secrets, and the one-time step that lets the hand-built six be reached are covered in the runbook's Run it from phoenix section.

What is here

  • inventory/hosts.yaml: the six guests, at their final addresses.
    • Groups: dc_primary, dc_replica, dcs, members, endpoints, and servers (the four evaluation-licensed guests).
    • lab_role matches the role label in the lab Prometheus.
  • inventory/group_vars/all.yaml: ADR-0029's values, plus how a guest is reached:
    • SSH as Administrator, with phoenix's key and PowerShell as the shell.
    • The pinned windows_exporter version and its sha256.
    • The pinned Sysmon version, the zip's sha256, and the vendored config's sha256, read from roles/sysmon/files/VENDORED.
    • How a population password is derived from LAB_POPULATION_SEED.
  • population/: the domain's people (#449).
    • population.yaml is generated by scripts/gen_population.py from the two name lists beside it, and committed. It names each account's department, title and groups, and holds no passwords. Do not edit it by hand: the script's --self-test, which CI runs, fails if the file is not what its header regenerates. To change the population, change the arguments and regenerate.
  • lab-domain.yml: the playbook, one play per stage, in the runbook's order.
  • verify.yml: read-only. It runs the runbook's §9 checks, and asserts that each item on the "Do not harden this domain" list is still shipped. It also checks that OU=People holds exactly the population, and that the endpoints' generator ran as authgen and succeeded, and that Sysmon runs the pinned version with the vendored config.
  • pktmon-start.yml and pktmon-stop.yml: a packet capture on one guest, brought back to phoenix. See Capture on demand.
  • population-credentials.yml: prints the population's passwords on phoenix, derived again rather than stored. It touches no guest.
  • roles/, in order:
    • base: name, DC static addresses, resolvers, and the build password rotated.
    • dc_forest: forest, forwarder, no root hints, and the clock. It asserts the clock is right before anything joins.
    • dc_replica: the second DC, left on NT5DS.
    • dc_resolvers: each DC resolves through its partner, then loopback.
    • member: joins.
    • exporter: windows_exporter, plus the 9182 rule admitting alexander.
    • licence_clock: the evaluation gauge, on the four servers.
    • population: the OUs, groups and users in population/population.yaml, on bahamut. It reports accounts in OU=People that the file does not name, and removes none.
    • tiers: the five tier OUs, the three tier admins, the Tier 0 Admins group, and the members placed in Servers/Workstations (#448).
    • gpos: the GPOs, ensured on bahamut compare-first, so a second run writes nothing.
      • Deny Tier 0 Logon on Members, as #414 built it by hand.
      • Lab - Audit File Share: the member servers' audit policy, the Windows default set plus File Share (#1077).
      • Lab - Audit Domain Controllers: the DCs' 17 subcategories plus Directory Service Changes (#1077).
    • ds_audit: inherited audit entries on the domain head, so a write to an SPN or a delegation attribute on a user or computer logs a 5136 (#1077).
    • shares: titan's Public and Finance shares, with the decoy (#448).
    • soc_agents: Wazuh and Velociraptor installed directly, by product code, in place of the deploy GPOs (#448).
    • authgen: the authentication generator's batch right and scheduled task, on the two endpoints.
    • weakness_kerberoast, weakness_asreproast, weakness_dcsync, weakness_ucd, weakness_cd, weakness_rbcd: #449's deliberate weaknesses, on bahamut, each behind its own tag and off by default (ADR-0083). Their fixed identifiers are in group_vars/all.yaml.
    • sysmon: Sysmon at the pinned version, running sysmon-modular's balanced config, with its channel sized to 256 MiB (#450, ADR-0080). The config in files/ is vendored byte for byte by scripts/vendor-sysmon-config.sh. Do not edit it: re-vendor.
    • dns_forwarder: shared by both DC roles.
    • dns_client: a guest's IPv4 resolvers, set through netsh interface ipv4 so the IPv6 ones (::1 on a DC) are never touched. Used by base, dc_replica and dc_resolvers.
    • domain_role: whether a guest is a DC already, imported by base and dc_replica in their own plays.
  • requirements.txt, requirements-lint.txt and requirements.yml: exact pins for ansible-core, ansible-lint and the three collections.

Tags

Tag Stage Runbook
base Name, address, resolver, password, and the build's WinRM certificate removed §2
packer_cert Only that certificate, from a guest cloned from a template built before #1031 §2
forest bahamut: forest, forwarder, clock §3
replica leviathan, then both DCs' resolvers §4
join The four members join §5, first sentence
exporter windows_exporter and its firewall rule §7
licence The evaluation gauge §7
population The people: OUs, groups, users §5
authgen The generator on the endpoints, as authgen §6
tiers The tier OUs, admins, Tier 0 Admins, member placement §5
gpos Deny Tier 0 logon, and the audit policies of servers and DCs §5
audit The domain head's audit entries for attribute writes (ds_audit) §5
shares titan's Public and Finance, with the decoy §5
soc Wazuh and Velociraptor, in place of the deploy GPOs §11
kerberoast An SPN on a crackable account, ramuh's target §5a
asreproast Pre-auth disabled on a crackable account §5a
dcsync Replication rights on a non-DA principal §5a
ucd Unconstrained delegation on ramuh §5a
cd Constrained delegation, svc-web to titan CIFS §5a
rbcd Resource-based constrained delegation on titan §5a
sysmon Sysmon and its config, on every guest §7

The tiers, shares and soc tags (#448) apply the rest of §5's skeleton and §11's agents. The last six are #449's deliberate weaknesses (ADR-0083), each off until asked for: weakness_<name>_state is absent by default, so a plain run leaves none of them present. Enable one, watch the SOC see it, disable it, confirm the signal goes:

ansible-playbook lab-domain.yml --tags kerberoast -e weakness_kerberoast_state=present
ansible-playbook verify.yml      -e weakness_kerberoast_state=present   # it is on
ansible-playbook lab-domain.yml --tags kerberoast                       # off again
ansible-playbook verify.yml                                             # all six absent

A plain ansible-playbook verify.yml asserts every one of these six weakness primitives is absent — the per-primitive negative test. It does not walk the domain's authorization graph, so it does not by itself prove no path to Domain Admin exists; that graph-level check is a BloodHound collector run with the tags off (#451) — the "run the collector" half that #449 asks for.

Capture on demand

Pktmon is Windows' own packet capture, on all six guests already, with nothing to install. Start a capture on one guest, reproduce the thing, then stop it:

ansible-playbook pktmon-start.yml --limit titan -e pktmon_port=445
ansible-playbook pktmon-stop.yml --limit titan

The stop converts the capture to pcapng, fetches it to ~/captures/<guest>-<UTC time>.pcapng on phoenix, and deletes it from the guest.

  • Both playbooks refuse to run on anything but one guest (--limit).
  • Filters: pktmon_port and pktmon_ip are optional. Given together, a packet must match both.
  • The capture includes your own SSH session. Pktmon's filters only include, so an unfiltered capture has phoenix's session on 22 in it.
  • Size: it captures at the NIC, whole packets, circular at 512 MB (-e pktmon_file_mb=...).
  • A reboot ends a capture. pktmon-stop.yml still fetches what was written before it. So does a rerun after a stop that failed partway. pktmon-start.yml refuses while an unfetched capture is waiting.

Rules this tree keeps

  • Nothing here hardens anything. No role touches LLMNR, NetBIOS over TCP/IP, IPv6, WPAD or the DNS GlobalQueryBlockList, and verify.yml fails if any of them has changed. Do not add a hardening role, from a collection or by reflex. It deletes the exercises the segment exists for. Auditing is not hardening: gpos and ds_audit make the DCs and servers record more, and change nothing an exercise can reach (#1077).
  • The lab domain, and only the lab domain. No host outside ad.matrix.elysium goes in the inventory. Compose stacks converge on their own timer (ADR-0021), and phoenix never pushes to them (ADR-0043).
  • Secrets come from phoenix.env. The variables are LAB_ADMIN_PASSWORD, LAB_DSRM_PASSWORD, LAB_POPULATION_SEED, LAB_TIER_ADMIN_PASSWORD, LAB_WAZUH_REGISTRATION_PASSWORD and LAB_WEAK_PASSWORD (the deliberately crackable one the weakness accounts share, ADR-0083). Every task that uses one is no_log. The exception is population-credentials.yml, which exists to print passwords and is run by hand.
  • A second run changes nothing. Every task compares before it acts. A task that reports changed on a guest already in the right state is a bug in that task.
  • Host keys are checked, against .known_hosts only. scripts/lab-known-hosts.sh writes it from keys read inside each guest through the Proxmox guest agent, never from what the network presents (ADR-0085). A rebuilt guest is refused until it has run again. Do not turn the check off to get past that; run the script.
  • CI proves it parses, phoenix proves it configures. scripts/lint.sh runs ansible-lint, which includes the syntax check, under the production profile that .ansible-lint pins. CI has no route to VLAN 30.