Infrastructure-as-code for lab environment: a homelab modeled on how real organizations (healthcare, government, universities) build and run Windows infrastructure. This repo is the single source of truth for host names, addresses, and configuration. Build flow for a Windows host: autounattend.xml does only the bootstrap (disk, edition, Ad ministrator password, SSH enabled), then Ansible applies everything else. See ms-server-ins tall-automation.md for the install side.
  • RouterOS Script 100%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-13 09:50:56 -07:00
inventory windows_mgmt_network: create working folder before writing script 2026-10-13 09:44:17 -07:00
network uv toolchain, switch baseline, static management address, LAN address plan 2026-10-13 00:48:16 -07:00
playbooks uv toolchain, switch baseline, static management address, LAN address plan 2026-10-13 00:48:16 -07:00
roles windows_mgmt_network: drop unneeded reset_connection 2026-10-13 09:50:56 -07:00
.gitignore uv toolchain, switch baseline, static management address, LAN address plan 2026-10-13 00:48:16 -07:00
ansible.cfg uv toolchain, switch baseline, static management address, LAN address plan 2026-10-13 00:48:16 -07:00
pyproject.toml uv toolchain, switch baseline, static management address, LAN address plan 2026-10-13 00:48:16 -07:00
README.md uv toolchain, switch baseline, static management address, LAN address plan 2026-10-13 00:48:16 -07:00
requirements.yml Initial inventory and Windows baseline 2026-10-12 22:33:40 -07:00
uv.lock uv toolchain, switch baseline, static management address, LAN address plan 2026-10-13 00:48:16 -07:00

picorealm-infra

Purpose

Infrastructure-as-code for the picorealm environment: a homelab modeled on how real organizations (healthcare, government, universities) build and run Windows infrastructure. This repo is the single source of truth for host names, addresses, and configuration.

Build flow for a Windows host: autounattend.xml does only the bootstrap (disk, edition, Administrator password, SSH enabled), then Ansible applies everything else. See ms-server-install-automation.md for the install side.

Conventions

Item Value
Public domain picorealm.net (Cloudflare; no records under ad.)
AD domain ad.picorealm.net — single forest, single domain
NetBIOS name PICO
UPN suffix picorealm.net
Site code SFO1 (nearest airport + building number); geography goes in AD Sites, not domain names
Hostnames <SITE>-<ROLE><NN>, max 15 characters (e.g. SFO1-HV01)
Time zone US Mountain Standard Time on all machines
Diagnostic data 1 (Required), matching autounattend.xml

Tier 0 systems (DCs, PKI, Entra Connect) never run on orange or on any host that serves internet-facing workloads.

Address plan

Home LAN 192.168.12.0/24, gateway BGW320 (.1). The BGW320 can only reserve addresses inside its DHCP pool, so static addresses (servers, AMT) live outside it.

Range Use
.1 Gateway (BGW320)
.2–.19 Network infrastructure: Pi-hole .2 (macvlan container on orange), SFO1-SW01 (MikroTik CRS125-24G-1S) .3, Brother printer .18
.20–.99 Static servers: SFO1-HV01 .21, orange .35, Forgejo .50 (macvlan container on orange)
.100–.199 BGW320 DHCP pool and reservations
.200–.229 Spare
.230–.239 Out-of-band management (Intel AMT, static inside AMT): SFO1-HV01 .231, orange .232
.240–.254 Spare

macvlan containers on orange (Pi-hole, Forgejo) are invisible to scans run from orange itself (a macvlan child cannot talk to its parent host), so check free addresses from another machine before assigning one. List them on orange with docker network ls --filter driver=macvlan -q | xargs -r docker network inspect -f '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}'.

DNS names for AMT addresses are Pi-hole local records under dunes.ixlo.net (orange-amt, sfo1-hv01-amt). Per-host addresses are recorded in inventory/hosts.yml (ansible_host, amt_ip).

Layout

ansible.cfg
requirements.yml                     collections: ansible.windows, community.windows, microsoft.ad
pyproject.toml, uv.lock              control-node toolchain (ansible-core), managed by uv
inventory/
  hosts.yml                          hosts and groups (windows > hyperv_hosts)
  group_vars/all.yml                 org-wide settings (domain, site, time zone)
  group_vars/windows/vars.yml        Windows connection + baseline settings
  group_vars/windows/vault.yml       encrypted secrets (create from vault.yml.example)
playbooks/site.yml                   windows_baseline on all Windows hosts, hyperv_host on Hyper-V hosts
playbooks/network.yml                static management address (windows_mgmt_network role)
roles/windows_baseline/              hostname, time zone, sshd + firewall, WER, telemetry, local admin, PowerShell 7
roles/hyperv_host/                   Hyper-V role (vSwitch pending the Realtek NIC)
roles/windows_mgmt_network/          static IP on the adapter matching mgmt_mac, DNS servers
network/sfo1-sw01.rsc                SFO1-SW01 baseline RouterOS config (applied with reset-configuration run-after-reset)

Control node setup

Runs from lumina (Ubuntu 24.04 under WSL2 on BACCHARIS, mirrored networking). Keep the clone in the WSL filesystem (e.g. ~/src/picorealm-infra), not under /mnt/c, which is slow and breaks file permissions. Default branch: master.

The control-node toolchain belongs to the repo, so every clone gets the same versions:

  • ansible-core is a uv project dependency, pinned in uv.lock (pyproject.toml declares it; Ubuntu's packaged ansible-core is too old for Windows over SSH, officially supported from 2.18).
  • Collections install into ./collections (collections_path in ansible.cfg; ignored by git), from requirements.yml.
  • .venv/ is created by uv and ignored by git.

Install sshpass (needed for the first, password-based run), then create the virtual environment from the lock file and install the collections:

sudo apt install sshpass && uv sync && uv run ansible-galaxy collection install -r requirements.yml

Run every Ansible command through uv run (e.g. uv run ansible-playbook ...), or activate the environment once per shell with source .venv/bin/activate. To upgrade ansible-core: uv lock --upgrade-package ansible-core && uv sync, then commit uv.lock.

Create and encrypt the vault, then set vault_windows_admin_password to the host's current Administrator password:

cp inventory/group_vars/windows/vault.yml.example inventory/group_vars/windows/vault.yml && uv run ansible-vault encrypt inventory/group_vars/windows/vault.yml

Set pwsh_version in inventory/group_vars/windows/vars.yml to the current PowerShell LTS release (the play stops with an assertion if it's empty).

First run

SFO1-HV01 still has its DHCP address (.125) until playbooks/network.yml has run, so pass it as mgmt_bootstrap_ip until then. Accept the host key once, so host key checking passes:

ssh Administrator@192.168.12.125 exit

Check connectivity, then apply the baseline:

uv run ansible windows -m ansible.windows.win_ping --ask-vault-pass -e mgmt_bootstrap_ip=192.168.12.125
uv run ansible-playbook playbooks/site.yml --ask-vault-pass -e mgmt_bootstrap_ip=192.168.12.125

The first run renames the host to SFO1-HV01 and reboots it, then reboots again after installing Hyper-V. Both reboots are handled by win_reboot. Re-running the playbook should report no changes; anything that changes on a re-run is drift or a non-idempotent task worth fixing.

Then move the host to its static address (mgmt_ip, .21). Do this near the machine: if it fails, recovery is the local console (sconfig). The change runs as a one-time scheduled task on the host (it would kill the SSH session otherwise); Ansible copies the trusted host key to the new address, reconnects there, and cleans up. The script's log is C:\ProgramData\picorealm\set-mgmt-ip.log.

uv run ansible-playbook playbooks/network.yml --ask-vault-pass -e mgmt_bootstrap_ip=192.168.12.125

Afterwards, drop -e mgmt_bootstrap_ip, remove the .125 reservation on the BGW320, and point the Pi-hole record sfo1-hv01.dunes.ixlo.net at .21.

Switching to SSH keys

Add the control node's public key to windows_admin_ssh_pubkeys in inventory/group_vars/windows/vars.yml and run the playbook. The role writes C:\ProgramData\ssh\administrators_authorized_keys (the file sshd uses for members of Administrators, instead of the user's .ssh folder) and restricts it to Administrators and SYSTEM, without which sshd ignores it. Once key login works, remove ansible_password from vars.yml.

Roadmap

  • RTL8125B NIC: driver, external vSwitch with AllowManagementOS = false; I219-LM stays for management and AMT.
  • First DCs for ad.picorealm.net: AD Site SFO1, UPN suffix, DNS forwarders to a filtering public resolver, conditional forwarders to Pi-hole for self-hosted zones, Pi-hole conditional forward for ad.picorealm.net.
  • Domain join (microsoft.ad.membership), DNS pointed at the DCs only, Windows LAPS for the local Administrator.
  • Move telemetry and password policy to GPO; Prometheus windows_exporter scraped from orange.
  • Later: member servers on orange's KVM (non-Tier 0, virtio drivers), MECM, Azure Arc.