LAB

Automating Isolated Active Directory Lab Provisioning with Vagrant and Ansible

Advertisement

When testing detection logic or validating offensive techniques, maintaining a static lab host quickly leads to state pollution. Leftover registry keys, modified service binary paths, and populated Kerberos ticket caches introduce false positives and obscure true attack signals. To solve this, I built a zero-touch, code-defined Active Directory lab on a Linux hypervisor host that can be spun up, configured with granular logging, and restored from scratch in under fifteen minutes.

Here is the blueprint for orchestrating a fully isolated AD environment using Vagrant, Ansible, and KVM/libvirt.

Topology and Network Isolation

The lab architecture consists of two main target hosts residing on an isolated host-only network bridge (virbr1):

  1. DC01: Windows Server 2022 acting as the Primary Domain Controller (corp.local).
  2. WKSTN01: Windows 11 Enterprise acting as a domain-joined endpoint.

To prevent any inadvertent network leakage during malware execution or lateral movement testing, the management network interface is bound strictly to a local private subnet, while outbound internet access is routed through an explicit Linux network namespace acting as a logging gateway.

Here is the base Vagrantfile defining the Domain Controller host:

Vagrant.configure("2") do |config|
  config.vm.define "dc01" do |dc|
    dc.vm.box = "generic/windows2022"
    dc.vm.hostname = "DC01"
    dc.vm.network "private_network", ip: "192.168.56.10", libvirt__network_name: "ad-lab-net"

    dc.vm.provider :libvirt do |v|
      v.memory = 4096
      v.cpus = 2
      v.graphics_type = "spice"
    end

    dc.vm.provision "ansible" do |ansible|
      ansible.playbook = "playbooks/provision_dc.yml"
      ansible.user = "vagrant"
      ansible.extra_vars = {
        ansible_user: "vagrant",
        ansible_password: "VagrantPassword123!",
        ansible_connection: "winrm",
        ansible_winrm_server_cert_validation: "ignore"
      }
    end
  end
end

Infrastructure Configuration via Ansible

Once the virtual machines boot and WinRM becomes responsive over the local host network, Ansible takes over the configuration pipeline.

The primary objective during provisioning is ensuring that every target host boots straight into a known-good, baseline-audited state. The playbook installs Active Directory Domain Services, promotes the server, and establishes domain default policy configurations.

Here is an extract from playbooks/provision_dc.yml handling domain promotion and configuring advanced auditing policies:

---
- name: Provision Active Directory Domain Controller
  hosts: dc01
  gather_facts: no
  tasks:
    - name: Install AD-Domain-Services
      win_feature:
        name: AD-Domain-Services
        include_management_tools: yes
        state: present

    - name: Create Active Directory Domain
      microsoft.ad.domain:
        dns_domain_name: corp.local
        safe_mode_administrator_password: "ComplexPassword123!"
      register: ad_create
      notify: Reboot Server

    - name: Ensure Command Line Auditing (Event ID 4688) is Enforced
      win_regedit:
        path: HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System\Audit
        name: ProcessCreationIncludeCmdLine_Enabled
        data: 1
        type: dword

    - name: Enable PowerShell Script Block Logging (Event ID 4104)
      win_regedit:
        path: HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging
        name: EnableScriptBlockLogging
        data: 1
        type: dword

  handlers:
    - name: Reboot Server
      win_reboot:
        reboot_timeout: 600

To complete the telemetry pipeline, Ansible automatically deploys Sysmon using a tailored rule file focused on process creation anomalies, direct disk access, and named pipe creation.

Rapid Snapshot and State Reset Workflow

Once Ansible finishes configuring the domain services, setting up default domain users, and joining WKSTN01 to corp.local, the hypervisor issues a clean baseline snapshot across all virtual machines via virsh:

#!/usr/bin/env bash
# Take baseline snapshot of the lab environment
virsh snapshot-create-as --domain ad-lab_dc01 --name baseline-clean --atomic
virsh snapshot-create-as --domain ad-lab_wkstn01 --name baseline-clean --atomic

After executing an attack play (such as Kerberoasting, AS-REP Roasting, or DCSync), restoring the pristine environment requires a single automated call:

#!/usr/bin/env bash
# Rollback to pristine baseline state in seconds
virsh snapshot-revert --domain ad-lab_dc01 --snapshotname baseline-clean
virsh snapshot-revert --domain ad-lab_wkstn01 --snapshotname baseline-clean

Key Takeaways

  1. Eliminate Environment Drift: Treating lab infrastructure as disposable code guarantees that attack artifacts from previous tests never contaminate subsequent telemetry collections.
  2. Deterministic Logging Baseline: Hardcoding registry-level audit policies and Sysmon configs via Ansible ensures that every detection engineering experiment starts with identical log coverage.
  3. Speed: Going from a fresh hypervisor host to a fully populated, logging-ready Active Directory environment takes minutes, enabling quick iteration during threat research.

Photo by Nicolas Thomas on Unsplash.