Compare commits

...

No commits in common. "master" and "ADDS" have entirely different histories.
master ... ADDS

46 changed files with 2699 additions and 85 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

48
.drone.yml Normal file
View File

@ -0,0 +1,48 @@
kind: pipeline
type: kubernetes
name: 'Packer Build'
steps:
- name: Debugging information
image: bv11-cr01.bessems.eu/library/packer-extended
commands:
- yamllint --version
- packer --version
- pwsh --version
- ovftool --version
- name: Active Directory Domain Services
image: bv11-cr01.bessems.eu/library/packer-extended
pull: always
commands:
- |
yamllint -d "{extends: relaxed, rules: {line-length: disable}}" scripts
- |
packer init -upgrade \
./packer
- |
packer validate \
-var vm_name=$DRONE_BUILD_NUMBER-${DRONE_COMMIT_SHA:0:10} \
-var vsphere_password=$${VSPHERE_PASSWORD} \
-var winrm_password=$${WINRM_PASSWORD} \
./packer
- |
packer build \
-on-error=cleanup -timestamp-ui \
-var vm_name=$DRONE_BUILD_NUMBER-${DRONE_COMMIT_SHA:0:10} \
-var vsphere_password=$${VSPHERE_PASSWORD} \
-var winrm_password=$${WINRM_PASSWORD} \
./packer
environment:
VSPHERE_PASSWORD:
from_secret: vsphere_password
WINRM_PASSWORD:
from_secret: winrm_password
# PACKER_LOG: 1
volumes:
- name: output
path: /output
volumes:
- name: output
claim:
name: flexvolsmb-drone-output

113
README.md
View File

@ -1,15 +1,108 @@
# Packer.Images
# Packer.Images [![Build Status](https://ci.spamasaurus.com/api/badges/djpbessems/Packer.Images/status.svg?ref=refs/heads/ADDS)](https://ci.spamasaurus.com/djpbessems/Packer.Images)
Opinionated set of packer templates for producing .OVA appliances, which can then be deployed (semi)unattended through the use of vApp properties:
This OVA appliance allows deploying an Active Directory Domain Controller fully automated:
## [![Build Status](https://ci.spamasaurus.com/api/badges/djpbessems/Packer.Images/status.svg?ref=refs/heads/UbuntuServer20.04) **Ubuntu Server 20.04**](https://code.spamasaurus.com/djpbessems/Packer.Images/src/branch/UbuntuServer20.04) - <small>LTS</small>
Lorem ipsum.
The included `.ovf` file has the following XML contents (simplified for clarity) to facilitate the different `DeploymentOption`s:
```xml
<Envelope [...]>
[...]
<DeploymentOptionSection>
<Info>Deployment Type</Info>
<Configuration ovf:id="primary">
<Label>Primary (redundant deployment)</Label>
<Description>Initial Domain Controller with 'PDC Emulator'-role</Description>
</Configuration>
<Configuration ovf:id="secondary">
<Label>Secondary (redundant deployment)</Label>
<Description>Additional Domain Controller</Description>
</Configuration>
<Configuration ovf:id="standalone">
<Label>Stand-alone (non-redundant deployment)</Label>
<Description>Single Domain Controller</Description>
</Configuration>
</DeploymentOptionSection>
<VirtualSystem ovf:id="[...]">
[...]
<ProductSection>
[...]
<Category>1) Operating System</Category>
<Property ovf:configuration="primary secondary standalone" ovf:key="guestinfo.hostname" [...]>
<Label>Hostname*</Label>
</Property>
[...]
<Category>2) Networking</Category>
<Property ovf:configuration="secondary" ovf:key="guestinfo.dnsserver" [...]>
<Label>DNS server*</Label>
</Property>
[...]
<Category>3) Active Directory Domain Services</Category>
<Property ovf:configuration="primary standalone" ovf:key="addsconfig.ntpserver" [...]>
<Label>NTP Server*</Label>
[...]
</Property>
</ProductSection>
</VirtualSystem>
</Envelope>
```
## [![Build Status](https://ci.spamasaurus.com/api/badges/djpbessems/Packer.Images/status.svg?ref=refs/heads/Server2019) **Windows Server 2019**](https://code.spamasaurus.com/djpbessems/Packer.Images/src/branch/Server2019) - <small>LTSC xx09</small>
This image in itself does not actually provide much benefit over other customization methods that are available during an unattended deployment; it serves primarily as a basis for the following images.
When **provisioning** the appliance through the vCenter 'Deploy OVF template...' wizard, or through vApp-compatible *Infrastructure as code* tooling (e.g. HashiCorp Terraform), it is possible to provide all relevant configuration through vApp properties.
## [![Build Status](https://ci.spamasaurus.com/api/badges/djpbessems/Packer.Images/status.svg?ref=refs/heads/ADDS) **ADDS**](https://code.spamasaurus.com/djpbessems/Packer.Images/src/branch/ADDS) - <small>Active Directory Domain Services</small>
Lorem ipsum.
<table>
<tr>
<td><em>vSphere 'Deploy OVF template...' wizard</em></td> <td> <a href="https://registry.terraform.io/providers/hashicorp/vsphere/latest/docs/resources/virtual_machine#deploying-vm-from-an-ovfova-template">HashiCorp Terraform vSphere provider</a> </td>
</tr>
<tr>
<td><img src=".assets/vAppConfigurations-ADDS-example.png" alt="vApp properties" width="400" /><br/><img src=".assets/vAppProperties-ADDS-example.png" alt="vApp properties" width="400" /></td>
<td>
## [![Build Status](https://ci.spamasaurus.com/api/badges/djpbessems/Packer.Images/status.svg?ref=refs/heads/ADCS) **ADCS**](https://code.spamasaurus.com/djpbessems/Packer.Images/src/branch/ADCS) - <small>Active Directory Certificate Services</small>
Lorem ipsum.
```hcl
vapp {
properties = {
# "deployment.type = "primary"
"guestinfo.hostname" = "DC01"
"guestinfo.ipaddress" = "10.0.0.21"
"guestinfo.prefixlength" = "24"
# "guestinfo.dnsserver" = "0.0.0.0"
"guestinfo.gateway" = "10.0.0.1"
"addsconfig.domainname" = "contoso.com"
"addsconfig.netbiosname" = "CONTOSO"
"addsconfig.administratorpw" = var.adds_adminpassword
"addsconfig.safemodepw" = var.adds_safemodepassword
# "addsconfig.ntpserver" = "0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org"
"vault.api" = "https://vault.example.org/v1"
"vault.token" = var.vault_token
"vault.pwpolicy" = "complex"
"vault.secret" = "contoso-project42"
# "dhcpconfig.startip" = "10.0.0.50"
# "dhcpconfig.endip" = "10.0.0.250"
# "dhcpconfig.subnetmask" = "255.255.255.0"
# "dhcpconfig.gateway" = "10.0.0.1"
# "dhcpconfig.leaseduration" = "01:00:00.00"
}
}
```
</td>
</tr>
</table>
On first boot, the appliance will start **configuring** itself without any further user-input, by performing the following steps:
- Change hostname
- Configure network
- Set password for local administrator
- Promote to Domain Controller
- Iterate through all payload scripts:
- Create Active Directory Organizational Units
- Create Active Directory security groups
- Create Active Directory user accounts
- Set up Delegation of Control
- Configure Active Directory Group Policy Objects with Windows Firewall settings
- Configure DHCP (scopes, options and Failover relationship)
- Create DNS records
- Define Active Directory Group Policy WMI Filters
- Define and link Active Directory Group Policy Objects and Preferences
- Set Active Directory Default domain Password policy

90
packer/adds.pkr.hcl Normal file
View File

@ -0,0 +1,90 @@
packer {
required_plugins {
windows-update = {
version = ">= 0.12.0"
source = "github.com/rgl/windows-update"
}
}
}
source "vsphere-clone" "adds" {
vcenter_server = var.vcenter_server
username = var.vsphere_username
password = var.vsphere_password
insecure_connection = "true"
vm_name = "adds-${var.vm_name}"
datacenter = var.vsphere_datacenter
host = var.vsphere_host
folder = var.vsphere_folder
datastore = var.vsphere_datastore
template = "Windows-Server-2019-LTSC"
boot_order = "disk,cdrom"
boot_command = [""]
boot_wait = "2m30s"
communicator = "winrm"
winrm_password = var.winrm_password
winrm_timeout = "10m"
winrm_username = "administrator"
RAM = 8192
CPUs = 2
floppy_files = [
"packer/preseed/ADDS/Sysprep_Unattend.xml"
]
shutdown_command = "C:\\Windows\\System32\\Sysprep\\sysprep.exe /generalize /oobe /unattend:A:\\Sysprep_Unattend.xml"
shutdown_timeout = "1h"
export {
images = false
}
}
build {
sources = ["source.vsphere-clone.adds"]
provisioner "powershell" {
inline = [
"New-Item -Path 'C:\\Payload\\Scripts' -ItemType 'Directory' -Force:$True -Confirm:$False"
]
}
provisioner "file" {
destination = "C:\\Payload\\"
source = "scripts/ADDS/payload/"
}
provisioner "powershell" {
scripts = [
"scripts/ADDS/Install-Prerequisites.ps1",
"scripts/ADDS/Register-ScheduledTask.ps1"
]
}
post-processor "shell-local" {
inline = [
"pwsh -command \"& scripts/Update-OvfConfiguration.ps1 \\",
" -OVFFile './output-adds/adds-${var.vm_name}.ovf' \\",
" -Parameter @{'appliance.name'='ADDS';'appliance.version'='${var.vm_name}'}\"",
"pwsh -file scripts/Update-Manifest.ps1 \\",
" -ManifestFileName './output-adds/adds-${var.vm_name}.mf'",
"ovftool --acceptAllEulas --allowExtraConfig --overwrite \\",
" './output-adds/adds-${var.vm_name}.ovf' \\",
" /output/ADDS-appliance.ova"
]
}
post-processor "shell-local" {
inline = [
"pwsh -file scripts/Remove-Resources.ps1 \\",
" -VMName 'adds-${var.vm_name}' \\",
" -VSphereFQDN '${var.vcenter_server}' \\",
" -VSphereUsername '${var.vsphere_username}' \\",
" -VSpherePassword '${var.vsphere_password}'"
]
}
}

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<settings pass="generalize">
<component name="Microsoft-Windows-Security-SPP" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SkipRearm>1</SkipRearm>
</component>
<component name="Microsoft-Windows-PnpSysprep" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<PersistAllDeviceInstalls>true</PersistAllDeviceInstalls>
<DoNotCleanUpNonPresentDevices>true</DoNotCleanUpNonPresentDevices>
</component>
</settings>
<settings pass="oobeSystem">
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<OOBE>
<HideEULAPage>true</HideEULAPage>
<HideLocalAccountScreen>true</HideLocalAccountScreen>
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
<NetworkLocation>Work</NetworkLocation>
<ProtectYourPC>1</ProtectYourPC>
<SkipMachineOOBE>true</SkipMachineOOBE>
<SkipUserOOBE>true</SkipUserOOBE>
</OOBE>
</component>
</settings>
</unattend>

14
packer/variables.pkr.hcl Normal file
View File

@ -0,0 +1,14 @@
variable "vcenter_server" {}
variable "vsphere_username" {}
variable "vsphere_password" {}
variable "vsphere_host" {}
variable "vsphere_datacenter" {}
variable "vsphere_templatefolder" {}
variable "vsphere_folder" {}
variable "vsphere_datastore" {}
variable "vsphere_network" {}
variable "vm_name" {}
variable "winrm_password" {}

View File

@ -2,8 +2,7 @@ vcenter_server = "bv11-vc.bessems.lan"
vsphere_username = "administrator@vsphere.local"
vsphere_datacenter = "DeSchakel"
vsphere_host = "bv11-esx.bessems.lan"
vsphere_hostip = "192.168.11.200"
vsphere_datastore = "Datastore01.SSD"
vsphere_folder = "/Packer"
vsphere_templatefolder = "/Templates"
vsphere_network = "LAN"
vsphere_network = "LAN"

View File

@ -0,0 +1,50 @@
[CmdletBinding()]
Param(
# No parameters
)
$InstallWindowsFeatureSplat = @{
Name = 'AD-Domain-Services', 'DHCP', 'RSAT-DNS-Server'
IncludeAllSubFeature = $True
IncludeManagementTools = $True
Restart = $False
Confirm = $False
}
Install-WindowsFeature @InstallWindowsFeatureSplat
$InstallPackageProviderSplat = @{
Name = 'NuGet'
MinimumVersion = '2.8.5.201'
Force = $True
Confirm = $False
}
Install-PackageProvider @InstallPackageProviderSplat
$SetPSRepositorySplat = @{
Name = 'PSGallery'
InstallationPolicy = 'Trusted'
}
Set-PSRepository @SetPSRepositorySplat
$InstallModuleSplat = @{
Name = 'powershell-yaml','gpwmifilter'
Force = $True
Confirm = $False
}
Install-Module @InstallModuleSplat
$SetPSRepositorySplat = @{
Name = 'PSGallery'
InstallationPolicy = 'Untrusted'
}
Set-PSRepository @SetPSRepositorySplat
# Double check whether the required PowerShell modules are available
$RequiredModules = @(
'powershell-yaml', # Provides cmdlets 'ConvertTo-Yaml' and 'ConvertFrom-Yaml'
'gpwmifilter', # Provides cmdlets '*-GPWmiFilter' and '*-GPWmiFilterAssignment'
'psframework' # Dependency for GMWmiFilter
)
ForEach ($Module in $RequiredModules) {
If ([boolean](Get-Module -Name $Module -ListAvailable) -ne $True) {
Write-Error -Message "Missing PowerShell module '$($Module)'"
Exit 1
}
}

View File

@ -0,0 +1,7 @@
[CmdletBinding()]
Param(
# No parameters
)
# Create scheduled task
& schtasks.exe /Create /TN 'FirstBoot' /SC ONSTART /RU SYSTEM /TR "powershell.exe -file C:\Payload\Apply-FirstBootConfig.ps1"

View File

@ -0,0 +1,300 @@
#Requires -Modules 'ADDSDeployment'
[CmdletBinding()]
Param(
# No parameters
)
$SetLocationSplat = @{
Path = $PSScriptRoot
}
Set-Location @SetLocationSplat
$NewEventLogSplat = @{
LogName = 'Application'
Source = 'FirstBoot'
ErrorAction = 'SilentlyContinue'
}
New-EventLog @NewEventLogSplat
$WriteEventLogSplat = @{
LogName = 'Application'
Source = 'FirstBoot'
EntryType = 'Information'
EventID = 1
Message = "FirstBoot sequence initiated [working directory: '$PWD']"
}
Write-EventLog @WriteEventLogSplat
$VMwareToolsExecutable = "C:\Program Files\VMware\VMware Tools\vmtoolsd.exe"
[xml]$ovfEnv = & $VMwareToolsExecutable --cmd "info-get guestinfo.ovfEnv" | Out-String
$ovfProperties = $ovfEnv.ChildNodes.NextSibling.PropertySection.Property
$ovfPropertyValues = @{}
foreach ($ovfProperty in $ovfProperties) {
$ovfPropertyValues[$ovfProperty.key] = $ovfProperty.Value
}
# Check for mandatory values
Switch ($ovfPropertyValues['deployment.type']) {
'primary' {
$MandatoryProperties, $MissingProperties = @('guestinfo.hostname', 'guestinfo.ipaddress', 'guestinfo.prefixlength', 'guestinfo.gateway', 'addsconfig.domainname', 'addsconfig.netbiosname', 'addsconfig.administratorpw', 'addsconfig.safemodepw', 'addsconfig.ntpserver'), @()
}
'secondary' {
$MandatoryProperties, $MissingProperties = @('guestinfo.hostname', 'guestinfo.ipaddress', 'guestinfo.prefixlength', 'guestinfo.dnsserver', 'guestinfo.gateway', 'addsconfig.domainname', 'addsconfig.netbiosname', 'addsconfig.administratorpw', 'addsconfig.safemodepw', 'dhcpconfig.startip', 'dhcpconfig.endip', 'dhcpconfig.subnetmask', 'dhcpconfig.gateway', 'dhcpconfig.leaseduration'), @()
}
'standalone' {
$MandatoryProperties, $MissingProperties = @('guestinfo.hostname', 'guestinfo.ipaddress', 'guestinfo.prefixlength', 'guestinfo.gateway', 'addsconfig.domainname', 'addsconfig.netbiosname', 'addsconfig.administratorpw', 'addsconfig.safemodepw', 'addsconfig.ntpserver', 'dhcpconfig.startip', 'dhcpconfig.endip', 'dhcpconfig.subnetmask', 'dhcpconfig.gateway', 'dhcpconfig.leaseduration'), @()
}
default {
# Mandatory values missing, cannot provision.
$WriteEventLogSplat = @{
LogName = 'Application'
Source = 'FirstBoot'
EntryType = 'Error'
EventID = 66
Message = "Unexpected or no value set for property 'deployment.type', cannot provision."
}
Write-EventLog @WriteEventLogSplat
& schtasks.exe /Change /TN 'FirstBoot' /DISABLE
Stop-Computer -Force
Exit
}
}
ForEach ($Property in $MandatoryProperties) {
If (!$ovfPropertyValues[$Property]) {
$MissingProperties += $Property
}
}
If ($MissingProperties.Length -gt 0) {
# Mandatory values missing, cannot provision.
$WriteEventLogSplat = @{
LogName = 'Application'
Source = 'FirstBoot'
EntryType = 'Error'
EventID = 66
Message = "Missing values for mandatory properties $(($MissingProperties | ForEach-Object {"'{0}'" -f $_}) -join ', '), cannot provision."
}
Write-EventLog @WriteEventLogSplat
& schtasks.exe /Change /TN 'FirstBoot' /DISABLE
Stop-Computer -Force
Exit
}
# Set hostname and description
If ($Env:ComputerName -ne $ovfPropertyValues['guestinfo.hostname']) {
$RenameComputerSplat = @{
NewName = $ovfPropertyValues['guestinfo.hostname']
Force = $True
Confirm = $False
}
Rename-Computer @RenameComputerSplat
$SetCimInstanceSplat = @{
InputObject = (Get-CimInstance -ClassName 'Win32_OperatingSystem')
Property = @{
Description = $ovfPropertyValues['guestinfo.hostname']
}
}
Set-CimInstance @SetCimInstanceSplat
# Restart the computer to apply changes
Restart-Computer -Force
Exit
}
# Configure network interface
If ((Get-WmiObject -Class 'Win32_NetworkAdapterConfiguration').IPAddress -NotContains $ovfPropertyValues['guestinfo.ipaddress']) {
$NewNetIPAddressSplat = @{
InterfaceAlias = (Get-NetAdapter).Name
AddressFamily = 'IPv4'
IPAddress = $ovfPropertyValues['guestinfo.ipaddress']
PrefixLength = $ovfPropertyValues['guestinfo.prefixlength']
DefaultGateway = $ovfPropertyValues['guestinfo.gateway']
}
$IPAddress = New-NetIPAddress @NewNetIPAddressSplat
# Wait for network connection to become available
$Timestamp, $TimeoutMinutes = (Get-Date), 5
Do {
If ($Timestamp.AddMinutes($TimeoutMinutes) -lt (Get-Date)) {
$WriteEventLogSplat = @{
LogName = 'Application'
Source = 'FirstBoot'
EntryType = 'Warning'
EventID = 13
Message = "Timeout after $($TimeoutMinutes) minutes waiting for network connection to become available."
}
Write-EventLog @WriteEventLogSplat
Break
}
Start-Sleep -Milliseconds 250
$GetNetIPAddressSplat = @{
IPAddress = $ovfPropertyValues['guestinfo.ipaddress']
InterfaceIndex = $IPAddress.InterfaceIndex
AddressFamily = 'IPv4'
ErrorAction = 'SilentlyContinue'
}
} Until ((Get-NetIPAddress @GetNetIPAddressSplat).AddressState -eq 'Preferred')
$OldErrorActionPreference, $ErrorActionPreference = $ErrorActionPreference, 'SilentlyContinue'
$TestNetConnectionSplat = @{
ComputerName = ([IPAddress]$ovfPropertyValues['guestinfo.dnsserver']).IPAddressToString
InformationLevel = 'Quiet'
}
$SetDnsClientServerAddressSplat = @{
InterfaceAlias = (Get-NetAdapter).Name
ServerAddresses = If (
[boolean]($ovfPropertyValues['guestinfo.dnsserver'] -as [IPaddress]) -and (Test-NetConnection @TestNetConnectionSplat)) {
($ovfPropertyValues['guestinfo.dnsserver'])
} else {
('127.0.0.1')
}
Validate = $False
}
Set-DnsClientServerAddress @SetDnsClientServerAddressSplat
$ErrorActionPreference, $OldErrorActionPreference = $OldErrorActionPreference, $NULL
}
# Promote to Domain Controller
If ((4,5) -NotContains (Get-WmiObject -Class 'Win32_ComputerSystem').DomainRole) {
# Change password of built-in Administrator
$BuiltinAdministrator = (Get-LocalUser | Where-Object {$_.SID -match '-500'})
$ConvertToSecureStringSplat = @{
String = $ovfPropertyValues['addsconfig.administratorpw']
AsPlainText = $True
Force = $True
}
$SetLocalUserSplat = @{
InputObject = $BuiltinAdministrator
Password = ConvertTo-SecureString @ConvertToSecureStringSplat
PasswordNeverExpires = $True
AccountNeverExpires = $True
### This setting is not allowed on the last administrator
#UserMayChangePassword = $False
Confirm = $False
}
Set-LocalUser @SetLocalUserSplat
$ResolveDNSNameSplat = @{
Name = "_ldap._tcp.dc._msdcs.$($ovfPropertyValues['addsconfig.domainname'])"
ErrorAction = 'SilentlyContinue'
}
$DNSRecord = Resolve-DnsName @ResolveDNSNameSplat
If ([boolean]$DNSRecord.PrimaryServer -eq $False) {
# No Primary Domain Controller found, installing as primary
$InstallADDSForestSplat = @{
DomainName = $ovfPropertyValues['addsconfig.domainname']
DomainNetbiosName = $ovfPropertyValues['addsconfig.netbiosname']
SafeModeAdministratorPassword = ConvertTo-SecureString $ovfPropertyValues['addsconfig.safemodepw'] -AsPlainText -Force
InstallDns = $True
DomainMode = 'WinThreshold'
ForestMode = 'WinThreshold'
Confirm = $False
Force = $True
ErrorAction = 'Stop'
}
Try {
Install-ADDSForest @InstallADDSForestSplat
# Previous cmdlet performs a reboot on completion; so these are commented out
# Restart-Computer -Force
# Exit
}
Catch {
& schtasks.exe /Change /TN 'FirstBoot' /DISABLE
Stop-Computer -Force
Exit
}
}
Else {
# Primary Domain Controller is present, installing as secondary
$InstallADDSDomainControllerSplat = @{
DomainName = $ovfPropertyValues['addsconfig.domainname']
Credential = New-Object System.Management.Automation.PSCredential("$($ovfPropertyValues['addsconfig.netbiosname'])\$($BuiltinAdministrator.Name)", (ConvertTo-SecureString @ConvertToSecureStringSplat))
SafeModeAdministratorPassword = ConvertTo-SecureString $ovfPropertyValues['addsconfig.safemodepw'] -AsPlainText -Force
InstallDns = $True
Confirm = $False
Force = $True
ErrorAction = 'Stop'
}
Try {
Install-ADDSDomainController @InstallADDSDomainControllerSplat
# Previous cmdlet performs a reboot on completion; so these are commented out
# Restart-Computer -Force
# Exit
}
Catch {
& schtasks.exe /Change /TN 'FirstBoot' /DISABLE
Stop-Computer -Force
Exit
}
}
}
# Wait for Active Directory to become available
$Timestamp, $TimeoutMinutes = (Get-Date), 15
Do {
If ($Timestamp.AddMinutes($TimeoutMinutes) -lt (Get-Date)) {
$WriteEventLogSplat = @{
LogName = 'Application'
Source = 'FirstBoot'
EntryType = 'Warning'
EventID = 13
Message = "Timeout after $($TimeoutMinutes) minutes waiting for Active Directory to become available."
}
Write-EventLog @WriteEventLogSplat
Break
}
Start-Sleep -Seconds 30
$GetADComputerSplat = @{
Identity = $Env:ComputerName
ErrorAction = 'SilentlyContinue'
}
Get-ADComputer @GetADComputerSplat | Out-Null
} Until ($?)
# Iterate through and invoke all payload scripts
#! TODO: add registry values to determine which scripts have already been invoked (in case of intermediate reboots)
$GetItemSplat = @{
Path = "$($PSScriptRoot)\Scripts\*.ps1"
}
ForEach ($Script in (Get-Item @GetItemSplat)) {
Try {
$WriteEventLogSplat = @{
LogName = 'Application'
Source = 'FirstBoot'
EntryType = 'Information'
EventID = 4
Message = "Running script: '$($Script.FullName)'"
}
Write-EventLog @WriteEventLogSplat
& $Script.FullName -Parameter $ovfPropertyValues
}
Catch {
$WriteEventLogSplat = @{
LogName = 'Application'
Source = 'FirstBoot'
EntryType = 'Error'
EventID = 66
Message = @"
Error occurred while executing script '$($Script.Name)':
$($_.Exception.Message)
"@
}
Write-EventLog @WriteEventLogSplat
}
}
$WriteEventLogSplat = @{
LogName = 'Application'
Source = 'FirstBoot'
EntryType = 'Information'
EventID = 42
Message = 'FirstBoot sequence applied and finished'
}
Write-EventLog @WriteEventLogSplat
& schtasks.exe /Change /TN 'FirstBoot' /DISABLE

View File

@ -0,0 +1,86 @@
[CmdletBinding()]
Param(
[Parameter()]
[string]$VaultAPIAddress,
[Parameter()]
[string]$VaultToken,
[Parameter()]
[string]$VaultPwPolicy,
[Parameter(Mandatory)]
[string]$VaultSecret,
[Parameter(Mandatory)]
[string]$Username
)
# Generate new password
$InvokeWebRequestSplat = @{
Uri = "$($VaultAPIAddress)/sys/policies/password/$($VaultPwPolicy)/generate"
Headers = @{'X-Vault-Token'="$VaultToken"}
UseBasicParsing = $True
}
$NewPassword = (Invoke-WebRequest @InvokeWebRequestSplat | ConvertFrom-Json).data.password
# Check for existense of secret
$Response, $ErrResponse = $Null, $Null
Try {
$InvokeWebRequestSplat = @{
Uri = "$($VaultAPIAddress)/secret/metadata/$($VaultSecret)"
Headers = @{'X-Vault-Token' = "$VaultToken"}
UseBasicParsing = $True
}
$Response = Invoke-WebRequest @InvokeWebRequestSplat
}
Catch [System.Net.WebException] {
$StreamReader = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream())
$StreamReader.BaseStream.Position = 0
$ErrResponse = $StreamReader.ReadToEnd()
$StreamReader.Close()
}
If ([boolean]$Response) {
# Secret already exists; retrieve existing key/value pairs
$InvokeWebRequestSplat = @{
Uri = "$($VaultAPIAddress)/secret/data/$($VaultSecret)"
Headers = @{'X-Vault-Token' = "$VaultToken"}
UseBasicParsing = $True
}
$Secret = (Invoke-WebRequest @InvokeWebRequestSplat | ConvertFrom-Json).data
# Merge new password into dictionary
$AddMemberSplat = @{
MemberType = 'NoteProperty'
Name = "password.$($Username)"
Value = $NewPassword
Force = $True
}
$Secret.data | Add-Member @AddMemberSplat
# Store as new version
$InvokeWebRequestSplat = @{
Uri = "$($VaultAPIAddress)/secret/data/$($VaultSecret)"
Method = 'POST'
UseBasicParsing = $True
Headers = @{'X-Vault-Token'="$VaultToken"}
Body = @{
data = $Secret.data
} | ConvertTo-Json
}
Invoke-WebRequest @InvokeWebRequestSplat | Out-Null
}
ElseIf ([boolean]$ErrResponse) {
# Secret did not exist yet, store as new secret
$InvokeWebRequestSplat = @{
Uri = "$($VaultAPIAddress)/secret/data/$($VaultSecret)"
Method = 'POST'
UseBasicParsing = $True
Headers = @{'X-Vault-Token'="$VaultToken"}
Body = @{
data = @{
"password.$($Username)" = $NewPassword
}
} | ConvertTo-Json
}
Invoke-WebRequest @InvokeWebRequestSplat | Out-Null
}
Return $NewPassword

View File

@ -0,0 +1,52 @@
#Requires -Modules 'ActiveDirectory'
Param(
[Parameter(Mandatory)]
[hashtable]$Parameter
)
# Only executed on primary or standalone Domain Controller
If (@('primary','standalone') -contains $Parameter['deployment.type']) {
$GetContentSplat = @{
Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', ".yml")
Raw = $True
}
$RawContent = Get-Content @GetContentSplat
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
# Check if the respective .yml file declared substitutions which need to be parsed
If (($YamlDocuments.Count -gt 1) -and $YamlDocuments[-1].Variables) {
ForEach ($Pattern in $YamlDocuments[-1].Variables) {
$RawContent = $RawContent -replace "\{\{ ($($Pattern.Name)) \}\}", [string](Invoke-Expression -Command $Pattern.Expression)
}
# Perform conversion to Yaml again, now with parsed file contents
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
$Entries = $YamlDocuments[0..($YamlDocuments.Count - 2)]
}
Else {
$Entries = $YamlDocuments
}
ForEach ($OU in $Entries.OrganizationalUnits) {
$OUName, $OUPath = $OU.DistinguishedName -split ',', 2
If ($OUPath.Length -ne 0) {
$OUPath += ','
}
$NewADOrganizationalUnitSplat = @{
Name = $OUName.Substring(3)
Path = $OUPath + (Get-ADRootDSE).rootDomainNamingContext
Description = $OU.Description
ProtectedFromAccidentalDeletion = $False
ErrorAction = 'SilentlyContinue'
}
New-ADOrganizationalUnit @NewADOrganizationalUnitSplat
}
}

View File

@ -0,0 +1,35 @@
OrganizationalUnits:
- DistinguishedName: OU=Computer accounts
Description: ''
- DistinguishedName: OU=Clients,OU=Computer accounts
Description: ''
- DistinguishedName: OU=Desktops,OU=Clients,OU=Computer accounts
Description: ''
- DistinguishedName: OU=Laptops,OU=Clients,OU=Computer accounts
Description: ''
- DistinguishedName: OU=Kiosks,OU=Clients,OU=Computer accounts
Description: ''
- DistinguishedName: OU=Servers,OU=Computer accounts
Description: ''
- DistinguishedName: OU=Groups
Description: ''
- DistinguishedName: OU=Resources,OU=Groups
Description: ''
- DistinguishedName: OU=Roles,OU=Groups
Description: ''
- DistinguishedName: OU=User accounts
Description: ''
- DistinguishedName: OU=Privileged,OU=User accounts
Description: ''
- DistinguishedName: OU=Administrators,OU=Privileged,OU=User accounts
Description: ''
- DistinguishedName: OU=Service accounts,OU=Privileged,OU=User accounts
Description: ''
- DistinguishedName: OU=Non-privileged,OU=User accounts
Description: ''
- DistinguishedName: OU=Employees,OU=Non-privileged,OU=User accounts
Description: ''
- DistinguishedName: OU=Contractors,OU=Non-privileged,OU=User accounts
Description: ''

View File

@ -0,0 +1,60 @@
#Requires -Modules 'ActiveDirectory'
Param(
[Parameter(Mandatory)]
[hashtable]$Parameter
)
# Only executed on primary or standalone Domain Controller
If (@('primary','standalone') -contains $Parameter['deployment.type']) {
$GetContentSplat = @{
Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', ".yml")
Raw = $True
}
$RawContent = Get-Content @GetContentSplat
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
# Check if the respective .yml file declared substitutions which need to be parsed
If (($YamlDocuments.Count -gt 1) -and $YamlDocuments[-1].Variables) {
ForEach ($Pattern in $YamlDocuments[-1].Variables) {
$RawContent = $RawContent -replace "\{\{ ($($Pattern.Name)) \}\}", [string](Invoke-Expression -Command $Pattern.Expression)
}
# Perform conversion to Yaml again, now with parsed file contents
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
$Entries = $YamlDocuments[0..($YamlDocuments.Count - 2)]
}
Else {
$Entries = $YamlDocuments
}
ForEach ($Group in $Entries.SecurityGroups) {
$NewADGroupSplat = @{
Name = ($Group.DistinguishedName -split ',', 2)[0].Substring(3)
Path = ($Group.DistinguishedName -split ',', 2)[1] + (',{0}' -f (Get-ADRootDSE).rootDomainNamingContext)
Description = $Group.Description
GroupCategory = 'Security'
GroupScope = $Group.Scope
PassThru = $True
ErrorAction = 'SilentlyContinue'
}
$NewADGroup = New-ADGroup @NewADGroupSplat
If ([boolean]$Group.MemberOf) {
ForEach ($ParentGroup in $Group.MemberOf) {
$AddADGroupMemberSplat = @{
Identity = $ParentGroup + (',{0}' -f (Get-ADRootDSE).rootDomainNamingContext)
Members = $NewADGroup.DistinguishedName
ErrorAction = 'SilentlyContinue'
}
Add-ADGroupMember @AddADGroupMemberSplat
}
}
}
}

View File

@ -0,0 +1,28 @@
SecurityGroups:
# Resource groups
- DistinguishedName: CN=RemoteDesktop - Management servers,OU=Resources,OU=Groups
Description: ''
Scope: 'DomainLocal'
MemberOf: []
- DistinguishedName: CN=ContentLibraryAdmin - vSphere servers,OU=Resources,OU=Groups
Description: ''
Scope: 'DomainLocal'
MemberOf: []
- DistinguishedName: CN=DatastoreAdmin - vSphere servers,OU=Resources,OU=Groups
Description: ''
Scope: 'DomainLocal'
MemberOf: []
# Role groups
- DistinguishedName: CN=Hypervisor administrators,OU=Roles,OU=Groups
Description: ''
Scope: 'Global'
MemberOf:
- CN=RemoteDesktop - Management servers,OU=Resources,OU=Groups
- CN=DatastoreAdmin - vSphere servers,OU=Resources,OU=Groups
- CN=ContentLibraryAdmin - vSphere servers,OU=Resources,OU=Groups
- DistinguishedName: CN=Firewall administrators,OU=Roles,OU=Groups
Description: ''
Scope: 'Global'
MemberOf:
- CN=RemoteDesktop - Management servers,OU=Resources,OU=Groups

View File

@ -0,0 +1,69 @@
#Requires -Modules 'ActiveDirectory'
Param(
[Parameter(Mandatory)]
[hashtable]$Parameter
)
# Only executed on primary or standalone Domain Controller
If (@('primary','standalone') -contains $Parameter['deployment.type']) {
$GetContentSplat = @{
Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', ".yml")
Raw = $True
}
$RawContent = Get-Content @GetContentSplat
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
# Check if the respective .yml file declared substitutions which need to be parsed
If (($YamlDocuments.Count -gt 1) -and $YamlDocuments[-1].Variables) {
ForEach ($Pattern in $YamlDocuments[-1].Variables) {
$RawContent = $RawContent -replace "\{\{ ($($Pattern.Name)) \}\}", [string](Invoke-Expression -Command $Pattern.Expression)
}
# Perform conversion to Yaml again, now with parsed file contents
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
$Entries = $YamlDocuments[0..($YamlDocuments.Count - 2)]
}
Else {
$Entries = $YamlDocuments
}
ForEach ($User in $Entries.Users) {
$UserName = ($User.DistinguishedName -split ',', 2)[0].Substring(3)
$SanitizedUPN = ($UserName -replace "[^a-zA-Z0-9'\.-_!#\^~]").Trim('.')
# Create new user
$NewADUserSplat = @{
Name = $UserName
UserPrincipalName = "$($SanitizedUPN)@$((Get-ADDomain).DNSRoot)"
Path = ($User.DistinguishedName -split ',', 2)[1] + (',{0}' -f (Get-ADRootDSE).rootDomainNamingContext)
AccountPassword = ConvertTo-SecureString $User.Password -AsPlainText -Force
PassThru = $True
ErrorAction = 'SilentlyContinue'
}
$NewADUser = New-ADUser @NewADUserSplat
# Add user to group(s)
If ([boolean]$User.MemberOf) {
ForEach ($Group in $User.MemberOf) {
$AddADGroupMemberSplat = @{
Identity = $Group + (',{0}' -f (Get-ADRootDSE).rootDomainNamingContext)
Members = $NewADUser.DistinguishedName
ErrorAction = 'SilentlyContinue'
}
Add-ADGroupMember @AddADGroupMemberSplat
}
}
# Enable user
$EnableADAccountSplat = @{
Identity = $NewADUser.DistinguishedName
ErrorAction = 'Continue'
}
Enable-ADAccount @EnableADAccountSplat
}
}

View File

@ -0,0 +1,27 @@
Users:
- DistinguishedName: CN=Jane Doe,OU=Employees,OU=Non-privileged,OU=User accounts
Password: "{{ password.janedoe }}"
MemberOf: []
- DistinguishedName: CN=John Doe,OU=Contractors,OU=Non-privileged,OU=User accounts
Password: "{{ password.johndoe }}"
MemberOf: []
- DistinguishedName: CN=admJaneD,OU=Administrators,OU=Privileged,OU=User accounts
Password: "{{ password.admjaned }}"
MemberOf: []
- DistinguishedName: CN=zzLDAP,OU=Service accounts,OU=Privileged,OU=User accounts
Password: "{{ password.zzldap }}"
MemberOf: []
---
Variables:
- Name: password.janedoe
Expression: |
& ".\Provision-VaultPassword.ps1" -VaultSecret $Parameter['vault.secret'] -Username 'janedoe' -VaultAPIAddress $Parameter['vault.api'] -VaultToken $Parameter['vault.token'] -VaultPwPolicy $Parameter['vault.pwpolicy']
- Name: password.johndoe
Expression: |
& ".\Provision-VaultPassword.ps1" -VaultSecret $Parameter['vault.secret'] -Username 'johndoe' -VaultAPIAddress $Parameter['vault.api'] -VaultToken $Parameter['vault.token'] -VaultPwPolicy $Parameter['vault.pwpolicy']
- Name: password.admjaned
Expression: |
& ".\Provision-VaultPassword.ps1" -VaultSecret $Parameter['vault.secret'] -Username 'admjaned' -VaultAPIAddress $Parameter['vault.api'] -VaultToken $Parameter['vault.token'] -VaultPwPolicy $Parameter['vault.pwpolicy']
- Name: password.zzldap
Expression: |
& ".\Provision-VaultPassword.ps1" -VaultSecret $Parameter['vault.secret'] -Username 'zzldap' -VaultAPIAddress $Parameter['vault.api'] -VaultToken $Parameter['vault.token'] -VaultPwPolicy $Parameter['vault.pwpolicy']

View File

@ -0,0 +1,133 @@
#Requires -Modules 'ActiveDirectory'
Param(
[Parameter(Mandatory)]
[hashtable]$Parameter
)
# Only executed on primary or standalone Domain Controller
If (@('primary','standalone') -contains $Parameter['deployment.type']) {
$PSDrive = Get-PSDrive -Name 'AD'
If ([boolean]$PSDrive -eq $False) {
$NewPSDriveSplat = @{
Name = 'ADDS'
Root = ''
PSProvider = 'ActiveDirectory'
}
$PSDrive = New-PSDrive @NewPSDriveSplat
}
$GetContentSplat = @{
Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', ".yml")
Raw = $True
}
$RawContent = Get-Content @GetContentSplat
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
# Check if the respective .yml file declared substitutions which need to be parsed
If (($YamlDocuments.Count -gt 1) -and $YamlDocuments[-1].Variables) {
ForEach ($Pattern in $YamlDocuments[-1].Variables) {
$RawContent = $RawContent -replace "\{\{ ($($Pattern.Name)) \}\}", [string](Invoke-Expression -Command $Pattern.Expression)
}
# Perform conversion to Yaml again, now with parsed file contents
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
$Delegations = $YamlDocuments[0..($YamlDocuments.Count - 2)]
}
Else {
$Delegations = $YamlDocuments
}
# Store GUIDs for all known AD schema classes
$GUIDMap, $GetADObjectSplat = @{}, @{
SearchBase = (Get-ADRootDSE).SchemaNamingContext
LDAPFilter = '(schemaidguid=*)'
Properties = 'lDAPDisplayName','schemaIDGUID'
}
Get-ADObject @GetADObjectSplat | ForEach-Object {
$GUIDMap[$_.lDAPDisplayName] = [GUID]$_.schemaIDGUID
}
# Store GUIDs for all extended rights
$GetADObjectSplat = @{
SearchBase = (Get-ADRootDSE).ConfigurationNamingContext
LDAPFilter = '(&(objectclass=controlAccessRight)(rightsguid=*))'
Properties = 'displayName','rightsGuid'
}
Get-ADObject @GetADObjectSplat | ForEach-Object {
$GUIDMap[$_.displayName] = [GUID]$_.rightsGuid
}
$GUIDMap['null'] = [Guid]::Empty
ForEach ($Entry in $Delegations.DelegationEntries) {
$GetADObjectSplat = @{
Filter = "sAMAccountName -eq '$($Entry.Principal)'"
Properties = 'objectSID'
}
$Principal = Get-ADObject @GetADObjectSplat
ForEach ($OU in $Entry.OrganizationalUnit) {
$GetADObjectSplat = @{
Identity = ($OU + (',{0}' -f (Get-ADRootDSE).rootDomainNamingContext))
ErrorAction = 'SilentlyContinue'
}
$OU = Get-ADObject @GetADObjectSplat
If ([boolean]$OU) {
$GetACLSPlat = @{
Path = "$($PSDrive.Name):\$($OU.DistinguishedName)"
}
$ACL = Get-ACL @GetACLSPlat
}
Else {
# Respective OU was not found in Active Directory; skipping permission assignment
Continue
}
ForEach ($Rule in $Entry.AccessRules) {
If ($Rule.ObjectType -eq '') {
$Rule.ObjectType = 'null'
}
If ($Rule.InheritedObjectType -eq '') {
$Rule.InheritedObjectType = 'null'
}
$NewACE = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
# An IdentityReference object that identifies the trustee of the access rule.
[System.Security.Principal.IdentityReference]$Principal.objectSID,
# A combination of one or more of the ActiveDirectoryRights enumeration values that specifies the rights of the access rule.
[System.DirectoryServices.ActiveDirectoryRights]$Rule.ActiveDirectoryRights,
# One of the AccessControlType enumeration values that specifies the access rule type.
[System.Security.AccessControl.AccessControlType]$Rule.AccessControlType,
# The schema GUID of the object to which the access rule applies.
[Guid]$GUIDMap[$Rule.ObjectType],
# One of the ActiveDirectorySecurityInheritance enumeration values that specifies the inheritance type of the access rule.
[System.DirectoryServices.ActiveDirectorySecurityInheritance]$Rule.ActiveDirectorySecurityInheritance,
# The schema GUID of the child object type that can inherit this access rule.
[Guid]$GUIDMap[$Rule.InheritedObjectType]
)
$ACL.AddAccessRule($NewACE)
}
$SetAclSplat = @{
Path = "$($PSDrive.Name):\$($OU.DistinguishedName)"
AclObject = $ACL
ErrorAction = 'Continue'
}
Set-Acl @SetAclSplat
}
}
If ([boolean]($PSDrive.Name -eq 'ADDS') -eq $True) {
$RemovePSDriveSplat = @{
Name = 'ADDS'
Force = $True
Confirm = $False
}
Remove-PSDrive @RemovePSDriveSplat | Out-Null
}
}

View File

@ -0,0 +1,76 @@
DelegationEntries:
- Principal: admJaneD # Entries will be concatenated with ',DC=<example>,DC=<org>' automatically
OrganizationalUnit:
- CN=Computers
- OU=Kiosks,OU=Clients,OU=Computer accounts
AccessRules:
- ActiveDirectoryRights: Self # A combination of one or more of the ActiveDirectoryRights enumeration values that specifies the rights of the access rule.
AccessControlType: Allow # One of the AccessControlType enumeration values that specifies the access rule type.
ActiveDirectorySecurityInheritance: Descendents # One of the ActiveDirectorySecurityInheritance enumeration values that specifies the inheritance type of the access rule.
ObjectType: Validated write to DNS host name # The object type to which the access rule applies.
InheritedObjectType: Computer # The child object type that can inherit this access rule.
- ActiveDirectoryRights: Self
AccessControlType: Allow
ActiveDirectorySecurityInheritance: Descendents
ObjectType: Validated write to service principal name
InheritedObjectType: Computer
- ActiveDirectoryRights: WriteProperty, WriteDacl
AccessControlType: Allow
ActiveDirectorySecurityInheritance: Descendents
ObjectType: ''
InheritedObjectType: Computer
- ActiveDirectoryRights: ExtendedRight
AccessControlType: Allow
ActiveDirectorySecurityInheritance: Descendents
ObjectType: Reset Password
InheritedObjectType: Computer
- ActiveDirectoryRights: ExtendedRight
AccessControlType: Allow
ActiveDirectorySecurityInheritance: Descendents
ObjectType: Change Password
InheritedObjectType: Computer
- ActiveDirectoryRights: ReadProperty
AccessControlType: Allow
ActiveDirectorySecurityInheritance: Descendents
ObjectType: ''
InheritedObjectType: Computer
- ActiveDirectoryRights: WriteProperty
AccessControlType: Allow
ActiveDirectorySecurityInheritance: Descendents
ObjectType: ''
InheritedObjectType: Computer
- ActiveDirectoryRights: CreateChild, DeleteChild
AccessControlType: Allow
ActiveDirectorySecurityInheritance: All
ObjectType: Computer
InheritedObjectType: ''
- ActiveDirectoryRights: GenericAll
AccessControlType: Allow
ActiveDirectorySecurityInheritance: Descendents
ObjectType: Computer
InheritedObjectType: ''
- Principal: admJaneD
OrganizationalUnit:
- OU=Clients,OU=Computer accounts
AccessRules:
- ActiveDirectoryRights: CreateChild, DeleteChild
AccessControlType: Allow
ActiveDirectorySecurityInheritance: All
ObjectType: User
InheritedObjectType: ''
- ActiveDirectoryRights: GenericAll
AccessControlType: Allow
ActiveDirectorySecurityInheritance: Descendents
ObjectType: ''
InheritedObjectType: ''
- ActiveDirectoryRights: WriteProperty, ReadProperty
AccessControlType: Allow
ActiveDirectorySecurityInheritance: Descendents
ObjectType: Member
InheritedObjectType: Group
# ---
# Variables:
# - Name: foo
# Expression: |
# Write-Host 'bar'

View File

@ -0,0 +1,65 @@
Name: 'COMP: Firewall (Clients)'
LinkedOUs:
- OU=Clients,OU=Computer accounts
FirewallRules:
- Description: Rule A
Action: Block
Direction: Inbound
Program: ''
Port: '21-22,25'
Protocol: TCP
- Description: Rule B
Action: Allow
Direction: Inbound
Program: D:\MSSQL\sqlsvr.exe
Port: ''
Protocol: ''
FirewallProfiles:
- Name: Domain
Enabled: 'True'
Connections:
Inbound: Block
Outbound: Allow
Settings:
DisplayNotification: 'False'
ApplyLocalFirewallRules: 'True'
ApplyLocalConnectionSecurityRules: 'True'
Logging:
Name: '%SYSTEMROOT%\System32\Logfiles\Firewall\domainfw.log'
SizeLimit: 16384
LogDroppedPackets: 'True'
LogSuccessfullConnections: 'False'
- Name: Private
Enabled: 'True'
Connections:
Inbound: Block
Outbound: Allow
Settings:
DisplayNotification: 'False'
ApplyLocalFirewallRules: 'True'
ApplyLocalConnectionSecurityRules: 'True'
Logging:
Name: '%SYSTEMROOT%\System32\Logfiles\Firewall\privatefw.log'
SizeLimit: 16384
LogDroppedPackets: 'True'
LogSuccessfullConnections: 'False'
- Name: Public
Enabled: 'True'
Connections:
Inbound: Block
Outbound: Allow
Settings:
DisplayNotification: 'False'
ApplyLocalFirewallRules: 'True'
ApplyLocalConnectionSecurityRules: 'True'
Logging:
Name: '%SYSTEMROOT%\System32\Logfiles\Firewall\publicfw.log'
SizeLimit: 16384
LogDroppedPackets: 'True'
LogSuccessfullConnections: 'False'
# ---
# Variables:
# - Name: foo
# Expression: |
# Write-Host 'bar'

View File

@ -0,0 +1,65 @@
Name: 'COMP: Firewall (DomainControllers)'
LinkedOUs:
- OU=Domain Controllers
FirewallRules:
- Description: Rule A
Action: Block
Direction: Inbound
Program: ''
Port: '21-22,25'
Protocol: TCP
- Description: Rule B
Action: Allow
Direction: Inbound
Program: D:\MSSQL\sqlsvr.exe
Port: ''
Protocol: ''
FirewallProfiles:
- Name: Domain
Enabled: 'True'
Connections:
Inbound: Block
Outbound: Allow
Settings:
DisplayNotification: 'False'
ApplyLocalFirewallRules: 'True'
ApplyLocalConnectionSecurityRules: 'True'
Logging:
Name: '%SYSTEMROOT%\System32\Logfiles\Firewall\domainfw.log'
SizeLimit: 16384
LogDroppedPackets: 'True'
LogSuccessfullConnections: 'False'
- Name: Private
Enabled: 'True'
Connections:
Inbound: Block
Outbound: Allow
Settings:
DisplayNotification: 'False'
ApplyLocalFirewallRules: 'True'
ApplyLocalConnectionSecurityRules: 'True'
Logging:
Name: '%SYSTEMROOT%\System32\Logfiles\Firewall\privatefw.log'
SizeLimit: 16384
LogDroppedPackets: 'True'
LogSuccessfullConnections: 'False'
- Name: Public
Enabled: 'True'
Connections:
Inbound: Block
Outbound: Allow
Settings:
DisplayNotification: 'False'
ApplyLocalFirewallRules: 'True'
ApplyLocalConnectionSecurityRules: 'True'
Logging:
Name: '%SYSTEMROOT%\System32\Logfiles\Firewall\publicfw.log'
SizeLimit: 16384
LogDroppedPackets: 'True'
LogSuccessfullConnections: 'False'
# ---
# Variables:
# - Name: foo
# Expression: |
# Write-Host 'bar'

View File

@ -0,0 +1,140 @@
#Requires -Modules 'NetSecurity'
Param(
[Parameter(Mandatory)]
[hashtable]$Parameter
)
# Only executed on primary or standalone Domain Controller
If (@('primary','standalone') -contains $Parameter['deployment.type']) {
$GetItemSplat = @{
Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', '.yml')
}
ForEach ($File in (Get-Item @GetItemSplat)) {
Try {
Write-Host "Loading/parsing file '$($File)' ..."
$GetContentSplat = @{
Path = $File
Raw = $True
}
$RawContent = Get-Content @GetContentSplat
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
}
Catch {
$ParseErrors += "While processing '$($File)': $($_.Exception.Message)"
Continue
}
# Check if the respective .yml file declared substitutions which need to be parsed
If (($YamlDocuments.Count -gt 1) -and $YamlDocuments[-1].Variables) {
Try {
ForEach ($Pattern in $YamlDocuments[-1].Variables) {
$RawContent = $RawContent -replace "\{\{ ($($Pattern.Name)) \}\}", [string](Invoke-Expression -Command $Pattern.Expression)
}
# Perform conversion to Yaml again, now with parsed file contents
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
}
Catch {
$ParseErrors += "While processing '$($File)' (after substitutions): $($_.Exception.Message)"
Continue
}
$Settings = $YamlDocuments[0..($YamlDocuments.Count - 2)]
}
Else {
$Settings = $YamlDocuments
}
$NewGPOSplat = @{
Name = $Settings.Name
}
$NewGPO = New-GPO @NewGPOSplat
$OpenNetGPOSplat = @{
PolicyStore = "$($Parameter['addsconfig.domainname'])\$($NewGPO.DisplayName)"
}
$GPOSession = Open-NetGPO @OpenNetGPOSplat
ForEach ($Rule in $Settings.FirewallRules) {
$NewNetFirewallRuleSplat = @{
# Using so-called string formatting with the '-f' operator (looks more complicated than it is) to create consistent policy names:
# Examples:
# 'DENY: Inbound port 443 (TCP)'
# 'ALLOW: Inbound 'D:\MSSQL\bin\sqlservr.exe'
DisplayName = ("{0}: {1} {2} {3} {4}" -f
$Rule.Action.ToUpper(),
$Rule.Direction,
("'$($Rule.Program)'", $NULL)[!($Rule.Program)],
("Port $($Rule.Port)", $NULL)[!($Rule.Port)],
("($($Rule.Protocol))", $NULL)[!($Rule.Protocol)]
) -replace '\s+',' '
Description = $Rule.Description
Action = $Rule.Action
Direction = $Rule.Direction
Program = ($Rule.Program, 'Any')[!($Rule.Program)]
LocalPort = ($Rule.Port.Split(','), 'Any')[!($Rule.Port)]
Protocol = ($Rule.Protocol, 'Any')[!($Rule.Protocol)]
GPOSession = $GPOSession
PolicyStore = $NewGPO.DisplayName
Confirm = $False
}
New-NetFirewallRule @NewNetFirewallRuleSplat
}
ForEach ($Profile in $Settings.FirewallProfiles) {
$SetNetFirewallProfileSplat = @{
Name = $Profile.Name
Enabled = $Profile.Enabled
DefaultInboundAction = $Profile.Connections.Inbound
DefaultOutboundAction = $Profile.Connections.Outbound
LogAllowed = $Profile.Logging.LogSuccessfullConnections
LogBlocked = $Profile.Logging.LogDroppedPackets
LogFileName = $Profile.Logging.Name
LogMaxSizeKilobytes = $Profile.Logging.SizeLimit
AllowLocalFirewallRules = $Profile.Settings.ApplyLocalFirewallRules
AllowLocalIPsecRules = $Profile.Settings.ApplyLocalConnectionSecurityRules
NotifyOnListen = $Profile.Settings.DisplayNotification
GPOSession = $GPOSession
PolicyStore = $NewGPO.DisplayName
Confirm = $False
}
Set-NetFirewallProfile @SetNetFirewallProfileSplat
}
$SaveNetGPOSplat = @{
GPOSession = $GPOSession
}
Save-NetGPO @SaveNetGPOSplat
ForEach ($OU in $Settings.LinkedOUs) {
If (Test-Path "AD:\$($OU + (',{0}' -f (Get-ADRootDSE).rootDomainNamingContext))") {
Try {
Write-Host "Linking policy '$($NewGPO.DisplayName)' to OU '$($OU)' ..."
$NewGPLinkSplat = @{
Name = $NewGPO.DisplayName
Target = $OU + (',{0}' -f (Get-ADRootDSE).rootDomainNamingContext)
}
New-GPLink @NewGPLinkSplat | Out-Null
}
Catch {
$ParseErrors += "Could not link GPO '$($NewGPO.DisplayName)' to OU '$($OU)'"
Continue
}
}
Else {
$ParseErrors += "Path not accessible (referred to by '$($NewGPO.DisplayName)'): 'AD:\$($OU + (',{0}' -f (Get-ADRootDSE).rootDomainNamingContext))'"
Continue
}
}
}
If ($ParseErrors) {
Throw "One or more errors occurred:`n$($ParseErrors -join "`n")"
}
}

View File

@ -0,0 +1,65 @@
Name: 'COMP: Firewall (Servers)'
LinkedOUs:
- OU=Servers,OU=Computer accounts
FirewallRules:
- Description: Rule A
Action: Block
Direction: Inbound
Program: ''
Port: '21-22,25'
Protocol: TCP
- Description: Rule B
Action: Allow
Direction: Inbound
Program: D:\MSSQL\sqlsvr.exe
Port: ''
Protocol: ''
FirewallProfiles:
- Name: Domain
Enabled: 'True'
Connections:
Inbound: Block
Outbound: Allow
Settings:
DisplayNotification: 'False'
ApplyLocalFirewallRules: 'True'
ApplyLocalConnectionSecurityRules: 'True'
Logging:
Name: '%SYSTEMROOT%\System32\Logfiles\Firewall\domainfw.log'
SizeLimit: 16384
LogDroppedPackets: 'True'
LogSuccessfullConnections: 'False'
- Name: Private
Enabled: 'True'
Connections:
Inbound: Block
Outbound: Allow
Settings:
DisplayNotification: 'False'
ApplyLocalFirewallRules: 'True'
ApplyLocalConnectionSecurityRules: 'True'
Logging:
Name: '%SYSTEMROOT%\System32\Logfiles\Firewall\privatefw.log'
SizeLimit: 16384
LogDroppedPackets: 'True'
LogSuccessfullConnections: 'False'
- Name: Public
Enabled: 'True'
Connections:
Inbound: Block
Outbound: Allow
Settings:
DisplayNotification: 'False'
ApplyLocalFirewallRules: 'True'
ApplyLocalConnectionSecurityRules: 'True'
Logging:
Name: '%SYSTEMROOT%\System32\Logfiles\Firewall\publicfw.log'
SizeLimit: 16384
LogDroppedPackets: 'True'
LogSuccessfullConnections: 'False'
# ---
# Variables:
# - Name: foo
# Expression: |
# Write-Host 'bar'

View File

@ -0,0 +1,27 @@
#Requires -Modules 'DhcpServer'
Param(
[Parameter(Mandatory)]
[hashtable]$Parameter
)
# Configure DHCP (if and only if this server is not already an authorized DHCP server)
If ((Get-DHCPServerInDC).IPAddress -NotContains $Parameter['guestinfo.ipaddress']) {
# Add DHCP security groups
& netsh dhcp add securitygroups
# Authorize DHCP server
$AddDhcpServerInDCSplat = @{
DnsName = "$($Parameter['guestinfo.hostname']).$($Parameter['addsconfig.domainname'])"
IPAddress = $($Parameter['guestinfo.ipaddress'])
Confirm = $False
}
Add-DhcpServerInDC @AddDhcpServerInDCSplat
# Notify Server Manager post-install configuration has completed
$SetItemPropertySplat = @{
Path = 'HKLM:\SOFTWARE\Microsoft\ServerManager\Roles\12'
Name = 'ConfigurationState'
Value = 2
}
Set-ItemProperty @SetItemPropertySplat
}

View File

@ -0,0 +1,54 @@
#Requires -Modules 'DhcpServer'
Param(
[Parameter(Mandatory)]
[hashtable]$Parameter
)
# Only executed on secondary or standalone Domain Controller
If (@('secondary','standalone') -contains $Parameter['deployment.type']) {
$AddDhcpServerv4ScopeSplat = @{
Name = 'Default DHCP scope'
StartRange = [ipaddress]$Parameter['dhcpconfig.startip']
EndRange = [ipaddress]$Parameter['dhcpconfig.endip']
SubnetMask = [ipaddress]$Parameter['dhcpconfig.subnetmask']
LeaseDuration = [timespan]$Parameter['dhcpconfig.leaseduration']
State = 'Active'
PassThru = $True
Confirm = $False
}
$DhcpScope = Add-DhcpServerv4Scope @AddDhcpServerv4ScopeSplat
$ScopeOptions = @(
@{
# 003 Router
OptionId = 3
Value = $Parameter['dhcpconfig.gateway']
},
@{
# 004 Time Server
OptionId = 4
Value = (Resolve-DnsName -Name $Parameter['addsconfig.domainname']).IPAddress
},
@{
# 006 DNS Server
OptionId = 6
Value = (Resolve-DnsName -Name $Parameter['addsconfig.domainname']).IPAddress
},
@{
# 015 DNS Domain Name
OptionId = 15
Value = $Parameter['addsconfig.domainname']
}
)
ForEach ($Option in $ScopeOptions) {
$SetDhcpServerv4OptionValueSplat = @{
ScopeId = $DhcpScope.ScopeId
OptionId = $Option.OptionId
Value = $Option.Value
Force = $True
Confirm = $False
}
Set-DhcpServerv4OptionValue @SetDhcpServerv4OptionValueSplat
}
}

View File

@ -0,0 +1,42 @@
#Requires -Modules 'DhcpServer'
Param(
[Parameter(Mandatory)]
[hashtable]$Parameter
)
# Only executed on secondary Domain Controller
If ($Parameter['deployment.type'] -eq 'secondary') {
# Wait for secondary DHCP server to be registered in DNS
$Timestamp, $TimeoutMinutes = (Get-Date), 5
Do {
If ($Timestamp.AddMinutes($TimeoutMinutes) -lt (Get-Date)) {
$WriteEventLogSplat = @{
LogName = 'Application'
Source = 'OVF-Properties'
EntryType = 'Warning'
EventID = 13
Message = "Timeout after $($TimeoutMinutes) minutes waiting for secondary Domain Controller to be registered in DNS."
}
Write-EventLog @WriteEventLogSplat
Break
}
Start-Sleep -Seconds 5
} Until ((Get-DhcpServerInDC).Count -gt 1)
$NewCimSessionSplat = @{
Credential = New-Object System.Management.Automation.PSCredential(
(Get-ADUser -Filter * | Where-Object {$_.SID -match '-500'}).SamAccountName,
(ConvertTo-SecureString $Parameter['addsconfig.administratorpw'] -AsPlainText -Force)
)
}
$AddDhcpServerv4FailoverSplat = @{
Name = 'Failover #42'
PartnerServer = (Get-DhcpServerInDC).DnsName | Where-Object {$_ -ne "$($Parameter['guestinfo.hostname']).$($Parameter['addsconfig.domainname'])"}
ServerRole = 'Active'
ScopeId = (Get-DhcpServerv4Scope).ScopeId.IPAddressToString
CimSession = New-CimSession @NewCimSessionSplat
}
Add-DhcpServerv4Failover @AddDhcpServerv4FailoverSplat
}

View File

@ -0,0 +1,88 @@
#Requires -Modules 'DnsServer'
Param(
[Parameter(Mandatory)]
[hashtable]$Parameter
)
# Only executed on secondary or standalone Domain Controller
If (@('secondary','standalone') -contains $Parameter['deployment.type']) {
$GetContentSplat = @{
Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', ".$($Parameter['deployment.type']).yml")
Raw = $True
}
$RawContent = Get-Content @GetContentSplat
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
# Check if the respective .yml file declared substitutions which need to be parsed
If (($YamlDocuments.Count -gt 1) -and $YamlDocuments[-1].Variables) {
ForEach ($Pattern in $YamlDocuments[-1].Variables) {
$RawContent = $RawContent -replace "\{\{ ($($Pattern.Name)) \}\}", [string](Invoke-Expression -Command $Pattern.Expression -ErrorAction 'SilentlyContinue')
}
# Perform conversion to Yaml again, now with parsed file contents
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
$Records = $YamlDocuments[0..($YamlDocuments.Count - 2)]
}
Else {
$Records = $YamlDocuments
}
ForEach ($Record in $Records.Entries) {
$AddDnsServerResourceRecordSplat = @{
ComputerName = $Parameter['guestinfo.dnsserver']
ZoneName = $Parameter['addsconfig.domainname']
Name = [string]$Record.Name
TimeToLive = (New-TimeSpan -Hours 1)
AgeRecord = $False
Confirm = $False
}
Switch ($Record.Type) {
'A' {
$AddDnsServerResourceRecordSplat.Add('A', $True)
$AddDnsServerResourceRecordSplat.Add('IPv4Address', $Record.Value)
}
'AAAA' {
$AddDnsServerResourceRecordSplat.Add('AAAA', $True)
$AddDnsServerResourceRecordSplat.Add('IPv6Address', $Record.Value)
}
'CNAME' {
$AddDnsServerResourceRecordSplat.Add('CNAME', $True)
$AddDnsServerResourceRecordSplat.Add('HostNameAlias', $Record.Value)
}
'MX' {
$AddDnsServerResourceRecordSplat.Add('MX', $True)
# Value should match pattern '<fqdn>:<preference>'
# ie. 'mail.contoso.com:10'
$MailExch = $Record.Value -split ':'
$AddDnsServerResourceRecordSplat.Add('MailExchange', $MailExch[0])
$AddDnsServerResourceRecordSplat.Add('Preference', $MailExch[1])
}
'NS' {
$AddDnsServerResourceRecordSplat.Add('NS', $True)
$AddDnsServerResourceRecordSplat.Add('NameServer', $Record.Value)
}
'SRV' {
$AddDnsServerResourceRecordSplat.Add('SRV', $True)
# Value should match pattern '<fqdn>:<priority>:<weight>:<port>'
# ie. 'sipserver.contoso.com:0:0:5060'
$SrvLocator = $Record.Value -split ':'
$AddDnsServerResourceRecordSplat.Add('DomainName', $SrvLocator[0])
$AddDnsServerResourceRecordSplat.Add('Priority', $SrvLocator[1])
$AddDnsServerResourceRecordSplat.Add('Weight', $SrvLocator[2])
$AddDnsServerResourceRecordSplat.Add('Port', $SrvLocator[3])
}
'TXT' {
$AddDnsServerResourceRecordSplat.Add('TXT', $True)
$AddDnsServerResourceRecordSplat.Add('DescriptiveText', $Record.Value)
}
}
Add-DnsServerResourceRecord @AddDnsServerResourceRecordSplat
}
}

View File

@ -0,0 +1,27 @@
Entries:
- Name: ldap
Type: A
Value: "{{ primarydc }}"
- Name: ldap
Type: A
Value: "{{ secondarydc }}"
- Name: timeserver
Type: A
Value: "{{ primarydc }}"
- Name: timeserver
Type: A
Value: "{{ secondarydc }}"
# - Name: mail
# Type: MX
# Value: mail.contoso.com:10 # Value should match pattern '<fqdn>:<preference>'
# - Name: voipserver
# Type: SRV
# Value: sip.contoso.com:0:0:5060 # Value should match pattern '<fqdn>:<priority>:<weight>:<port>'
---
Variables:
- Name: primarydc
Expression: |
(Resolve-DnsName -Name $Parameter['addsconfig.domainname'] | Sort-Object)[0].IPAddress
- Name: secondarydc
Expression: |
(Resolve-DnsName -Name $Parameter['addsconfig.domainname'] | Sort-Object)[1].IPAddress

View File

@ -0,0 +1,18 @@
Entries:
- Name: ldap
Type: A
Value: "{{ primarydc }}"
- Name: timeserver
Type: A
Value: "{{ primarydc }}"
# - Name: mail
# Type: MX
# Value: mail.contoso.com:10 # Value should match pattern '<fqdn>:<preference>'
# - Name: voipserver
# Type: SRV
# Value: sip.contoso.com:0:0:5060 # Value should match pattern '<fqdn>:<priority>:<weight>:<port>'
---
Variables:
- Name: primarydc
Expression: |
(Resolve-DnsName -Name $Parameter['addsconfig.domainname'] | Sort-Object)[0].IPAddress

View File

@ -0,0 +1,47 @@
#Requires -Modules 'GPWmiFilter'
Param(
[Parameter(Mandatory)]
[hashtable]$Parameter
)
# Only executed on primary or standalone Domain Controller
If (@('primary','standalone') -contains $Parameter['deployment.type']) {
$GetContentSplat = @{
Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', '.yml')
Raw = $True
}
$RawContent = Get-Content @GetContentSplat
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
# Check if the respective .yml file declared substitutions which need to be parsed
If (($YamlDocuments.Count -gt 1) -and $YamlDocuments[-1].Variables) {
ForEach ($Pattern in $YamlDocuments[-1].Variables) {
$RawContent = $RawContent -replace "\{\{ ($($Pattern.Name)) \}\}", [string](Invoke-Expression -Command $Pattern.Expression)
}
# Perform conversion to Yaml again, now with parsed file contents
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
$WmiFilters = $YamlDocuments[0..($YamlDocuments.Count - 2)]
}
Else {
$WmiFilters = $YamlDocuments
}
ForEach ($Filter in $WmiFilters) {
$NewGPWmiFilterSplat = @{
Name = $Filter.Name
Description = $Filter.Description
Expression = $Filter.Expressions
Server = $Parameter['addsconfig.domainname']
ErrorAction = 'SilentlyContinue'
}
New-GPWmiFilter @NewGPWmiFilterSplat
}
}

View File

@ -0,0 +1,9 @@
- Name: PDC Emulator
Description: Primary Domain Controller Emulator only
Expressions:
- 'SELECT * FROM Win32_ComputerSystem WHERE DomainRole = 5'
# ---
# Variables:
# - Name: foo
# Expression: |
# Write-Host 'bar'

View File

@ -0,0 +1,15 @@
Name: 'COMP: Disable Server Manager at Logon'
Type: Object
LinkedOUs:
- OU=Servers,OU=Computer accounts
- OU=Domain Controllers
WMIFilters: []
RegistryEntries:
- Key: HKLM\Software\Microsoft\ServerManager
Type: Dword
ValueName: DoNotOpenAtServerManagerAtLogon
Value: 1
- Key: HKLM\Software\Microsoft\ServerManager
Type: Dword
ValueName: DoNotPopWACConsoleAtSMLaunch
Value: 1

View File

@ -0,0 +1,19 @@
Name: 'COMP: Loopback processing (Merge)'
Type: Object
LinkedOUs: []
WMIFilters: []
RegistryEntries:
- Key: HKLM\Software\Policies\Microsoft\Windows\System
Type: Dword
ValueName: UserPolicyMode
Value: 1
---
Name: 'COMP: Loopback processing (Replace)'
Type: Object
LinkedOUs: []
WMIFilters: []
RegistryEntries:
- Key: HKLM\Software\Policies\Microsoft\Windows\System
Type: Dword
ValueName: UserPolicyMode
Value: 2

View File

@ -0,0 +1,36 @@
Name: 'COMP: Timeserver configuration (W32Time)'
Type: Object
LinkedOUs:
- OU=Domain Controllers
WMIFilters:
- PDC Emulator
RegistryEntries:
- Key: HKLM\SYSTEM\CurrentControlSet\Services\W32Time\Parameters
Type: String
ValueName:
- Type
- NtpServer
Value:
- NTP
- "{{ addsconfig.ntpserver }}"
- Key: HKLM\SYSTEM\CurrentControlSet\Services\W32Time\Config
Type: DWord
ValueName: AnnounceFlags
Value: 0xA
- Key: HKLM\SYSTEM\CurrentControlSet\Services\W32Time\Config
Type: DWord
ValueName: MaxPosPhaseCorrection
Value: 0xFFFFFFFF
- Key: HKLM\SYSTEM\CurrentControlSet\Services\W32Time\Config
Type: DWord
ValueName: MaxNegPhaseCorrection
Value: 0xFFFFFFFF
- Key: HKLM\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer
Type: DWord
ValueName: Enabled
Value: 1
---
Variables:
- Name: addsconfig.ntpserver
Expression: |
($Parameter['addsconfig.ntpserver'] -split ',' | ForEach-Object {'{0},0x1' -f $_}) -join ' '

View File

@ -0,0 +1,116 @@
Name: 'COMP: Restrict Internet Communication'
Type: Object
LinkedOUs:
- OU=Servers,OU=Computer accounts
WMIFilters: []
RegistryEntries:
- Key: HKLM\Software\Policies\Microsoft\InternetManagement
Type: DWord
ValueName: RestrictCommunication
Value: 1
# All below settings are set such that their respective features cannot access the Internet
# If any of these settings are in conflict with the above setting, gpmc.msc will behave erratic!
- Key: HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer
Type: Dword
ValueName: NoPublishingWizard
Value: 1
- Key: HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer
Type: Dword
ValueName: NoWebServices
Value: 1
- Key: HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer
Type: DWord
ValueName: NoOnlinePrintsWizard
Value: 1
- Key: HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer
Type: DWord
ValueName: NoInternetOpenWith
Value: 1
- Key: HKLM\Software\Policies\Microsoft\EventViewer
Type: DWord
ValueName: MicrosoftEventVwrDisableLinks
Value: 1
- Key: HKLM\Software\Policies\Microsoft\Messenger\Client
Type: DWord
ValueName: CEIP
Value: 2
- Key: HKLM\Software\Policies\Microsoft\PCHealth\ErrorReporting
Type: DWord
ValueName: DoReport
Value: 0
- Key: HKLM\Software\Policies\Microsoft\PCHealth\HelpSvc
Type: DWord
ValueName: Headlines
Value: 0
- Key: HKLM\Software\Policies\Microsoft\PCHealth\HelpSvc
Type: DWord
ValueName: MicrosoftKBSearch
Value: 0
- Key: HKLM\Software\Policies\Microsoft\SearchCompanion
Type: DWord
ValueName: DisableContentFileUpdates
Value: 1
- Key: HKLM\Software\Policies\Microsoft\SystemCertificates\AuthRoot
Type: DWord
ValueName: DisableRootAutoUpdate
Value: 1
- Key: HKLM\Software\Policies\Microsoft\SQMClient\Windows
Type: DWord
ValueName: CEIPEnable
Value: 0
- Key: HKLM\Software\Policies\Microsoft\Windows\DriverSearching
Type: DWord
ValueName: DontSearchWindowsUpdate
Value: 1
- Key: HKLM\Software\Policies\Microsoft\Windows\HandwritingErrorReports
Type: DWord
ValueName: PreventHandwritingErrorReports
Value: 1
- Key: HKLM\Software\Policies\Microsoft\Windows\Internet Connection Wizard
Type: DWord
ValueName: ExitOnMSICW
Value: 1
- Key: HKLM\Software\Policies\Microsoft\Windows\NetworkConnectivityStatusIndicator
Type: Dword
ValueName: NoActiveProbe
Value: 1
- Key: HKLM\Software\Policies\Microsoft\Windows\Registration Wizard Control
Type: DWord
ValueName: NoRegistration
Value: 1
- Key: HKLM\Software\Policies\Microsoft\Windows\TabletPC
Type: DWord
ValueName: PreventHandwritingDataSharing
Value: 1
- Key: HKLM\Software\Policies\Microsoft\Windows\Windows Error Reporting
Type: DWord
ValueName: Disabled
Value: 1
- Key: HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate
Type: DWord
ValueName: DisableWindowsUpdateAccess
Value: 1
- Key: HKLM\Software\Policies\Microsoft\Windows NT\CurrentVersion\Software Protection Platform
Type: DWord
ValueName: NoGenTicket
Value: 1
- Key: HKLM\Software\Policies\Microsoft\Windows NT\Printers
Type: DWord
ValueName: DisableHTTPPrinting
Value: 1
- Key: HKLM\Software\Policies\Microsoft\Windows NT\Printers
Type: DWord
ValueName: DisableWebPnPDownload
Value: 1
- Key: HKLM\Software\Policies\Microsoft\WindowsMovieMaker
Type: DWord
ValueName: WebHelp
Value: 1
- Key: HKLM\Software\Policies\Microsoft\WindowsMovieMaker
Type: DWord
ValueName: CodecDownload
Value: 1
- Key: HKLM\Software\Policies\Microsoft\WindowsMovieMaker
Type: DWord
ValueName: WebPublish
Value: 1

View File

@ -0,0 +1,44 @@
Name: 'COMP: Example GPO' # Prefix the name with either 'COMP:' or 'USER:'
Type: Object # Either 'Object' or 'Preference' (respectively for GPO or GPP)
LinkedOUs: # Entries will be concatenated with ',DC=<example>,DC=<org>' automatically
- OU=Servers
WMIFilters:
- FilterA
- FilterB
RegistryEntries:
- Key: HKLM\SOFTWARE\Policies\Microsoft\Windows\System
Type: DWord
ValueName: PropertyA
Value: 1
- Key: HKLM\SOFTWARE\Policies\Microsoft\Windows\System
Type: DWord
ValueName: PropertyB
Value: 0xFFFFFFFF # Hexadecimal values are prefixed with '0x'
- Key: HKLM\SYSTEM\CurrentControlSet\Services\W32Time\Parameters
Type: String
ValueName: # Multiple entries are possible, but *only* for the data type 'String' and 'ExpandString' (REG_SZ and REG_EXPAND_SZ)
- PropertyP
- PropertyQ
- PropertyR
Value: # The amount of entries must match with 'ValueName'
- ValueP
- ValueQ
- ValueR
- Key: HKLM\Software\Test
Type: String
ValueName:
- PropertyX
- PropertyDate
- PropertyOVF
Value: # Values can contain variablenames (respective entries must be declared under 'Variables' below)
- ValueX
- "{{ date }}"
- "{{ guestinfo.dnsserver }}"
---
Variables: # Each variable consists of a name that is used as a placeholder in the yaml file above, and a PowerShell expression
- Name: date
Expression: | # The PowerShell script's output must evaluate to a [string]
Get-Date
- Name: guestinfo.dnsserver
Expression: | # The variable '$Parameter' will automatically contain all defined OVF Properties
$Parameter['guestinfo.dnsserver']

View File

@ -0,0 +1,34 @@
Name: 'COMP: Example GPO' # Prefix the name with either 'COMP:' or 'USER:'
Type: Preference # Either 'Object' or 'Preference' (respectively for GPO or GPP)
LinkedOUs: # Entries will be concatenated with ',DC=<example>,DC=<org>' automatically
- OU=Servers
WMIFilters:
- FilterA
- FilterB
RegistryEntries:
- Key: HKLM\SOFTWARE\Policies\Microsoft\Windows\System
Type: DWord
ValueName: PropertyA
Value: 1
Action: Replace # Valid values are: Create, Update, Replace or Delete
Context: Computer # Valid values are: User or Computer
Disable: False # Change to 'True' when GPP entry should not be applied
- Key: HKLM\SOFTWARE\Policies\Microsoft\Windows\System
Type: DWord
ValueName: PropertyB
Value: 0xFFFFFFFF # Hexadecimal values are prefixed with '0x'
Action: Replace
Context: Computer
Disable: False
- Key: HKLM\Software\Test
Type: String
ValueName: PropertyOVF
Value: "{{ guestinfo.dnsserver }}" # Values can contain variablenames (respective entries must be declared under 'Variables' below)
Action: Replace
Context: Computer
Disable: False
---
Variables: # Each variable consists of a name that is used as a placeholder in the yaml file above, and a PowerShell expression
- Name: guestinfo.dnsserver
Expression: | # The variable '$Parameter' will automatically contain all defined OVF Properties
$Parameter['guestinfo.dnsserver']

View File

@ -0,0 +1,201 @@
#Requires -Modules 'powershell-yaml'
Param(
[Parameter(Mandatory)]
[hashtable]$Parameter
)
# Only executed on primary or standalone Domain Controller
If (@('primary','standalone') -contains $Parameter['deployment.type']) {
$NewPSSessionSplat = @{
ComputerName = $Parameter['guestinfo.hostname']
Credential = New-Object System.Management.Automation.PSCredential(
(Get-ADUser -Filter * | Where-Object {$_.SID -match '-500'}).SamAccountName,
(ConvertTo-SecureString $Parameter['addsconfig.administratorpw'] -AsPlainText -Force)
)
}
$PSSession = New-PSSession @NewPSSessionSplat
$ParseErrors = @()
$GetItemSplat = @{
Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', '.*.yml')
}
ForEach ($File in (Get-Item @GetItemSplat)) {
Try {
Write-Host "Loading/parsing file '$($File)' ..."
$GetContentSplat = @{
Path = $File
Raw = $True
}
$RawContent = Get-Content @GetContentSplat
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
}
Catch {
$ParseErrors += "While processing '$($File)': $($_.Exception.Message)"
Continue
}
# Check if the respective .yml file declared substitutions which need to be parsed
If (($YamlDocuments.Count -gt 1) -and $YamlDocuments[-1].Variables) {
Try {
ForEach ($Pattern in $YamlDocuments[-1].Variables) {
$RawContent = $RawContent -replace "\{\{ ($($Pattern.Name)) \}\}", [string](Invoke-Expression -Command $Pattern.Expression)
}
# Perform conversion to Yaml again, now with parsed file contents
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
}
Catch {
$ParseErrors += "While processing '$($File)' (after substitutions): $($_.Exception.Message)"
Continue
}
$GroupPolicies = $YamlDocuments[0..($YamlDocuments.Count - 2)]
}
Else {
$GroupPolicies = $YamlDocuments
}
ForEach ($GroupPolicy in $GroupPolicies) {
Write-Host "Initiating policy '$($GroupPolicy.Name)' ..."
$NewGPOSplat = @{
Name = $GroupPolicy.Name
ErrorAction = 'SilentlyContinue'
ErrorVariable = 'Failure'
}
$NewGPO = New-GPO @NewGPOSplat
If ($Failure) {
Continue
}
Switch ($GroupPolicy.Type) {
'Object' {
ForEach ($ValueSet in $GroupPolicy.RegistryEntries) {
Write-Host "Adding key/value to policy '$($NewGPO.DisplayName)' ...`n [$($ValueSet.Key)/$($ValueSet.ValueName)]"
$SetGPRegistryValueSplat = @{
Name = $NewGPO.DisplayName
Key = $ValueSet.Key
ValueName = $ValueSet.ValueName
Type = $ValueSet.Type
Value = Switch ($ValueSet.Type) {
'Binary' {
# Accepted formats:
# 000A0F0100
# 00 0A 0F 01 00
# 00,0A,0F,01,00
[byte[]]([regex]::split(($ValueSet.Value -replace '[ ,]'), '([0-9a-eA-E]{2})') | Where-Object {$_} | ForEach-Object {'0x{0}' -f $_})
}
'DWord' {
[uint32]$ValueSet.Value
}
'QWord' {
[uint64]$ValueSet.Value
}
Default {
$ValueSet.Value
}
}
ErrorAction = 'SilentlyContinue'
}
Set-GPRegistryValue @SetGPRegistryValueSplat | Out-Null
}
}
'Preference' {
ForEach ($ValueSet in $GroupPolicy.RegistryEntries) {
Write-Host "Adding key/value to policy '$($NewGPO.DisplayName)' ...`n [$($ValueSet.Key)/$($ValueSet.ValueName)]"
$SetGPPrefRegistryValueSplat = @{
Name = $NewGPO.DisplayName
Key = $ValueSet.Key
Context = $ValueSet.Context
Action = $ValueSet.Action
ValueName = $ValueSet.ValueName
Type = $ValueSet.Type
Value = Switch ($ValueSet.Type) {
'Binary' {
# Accepted formats:
# 000A0F0100
# 00 0A 0F 01 00
# 00,0A,0F,01,00
[byte[]]([regex]::split(($ValueSet.Value -replace '[ ,]'), '([0-9a-eA-E]{2})') | Where-Object {$_} | ForEach-Object {'0x{0}' -f $_})
}
'DWord' {
[uint32]$ValueSet.Value
}
'QWord' {
[uint64]$ValueSet.Value
}
Default {
$ValueSet.Value
}
}
Disable = [Convert]::ToBoolean($ValueSet.Disable)
ErrorAction = 'SilentlyContinue'
}
Set-GPPrefRegistryValue @SetGPPrefRegistryValueSplat | Out-Null
}
}
}
ForEach ($Filter in $GroupPolicy.WMIFilters) {
$InvokeCommandSplat = @{
Session = $PSSession
ArgumentList = $Filter, $Parameter, $NewGPO
ScriptBlock = {
#Requires -Modules 'GPWmiFilter'
Param(
$Filter,
$Parameter,
$NewGPO
)
$GetGPWmiFilterSplat = @{
Name = $Filter
Server = $Parameter['addsconfig.domainname']
ErrorAction = 'SilentlyContinue'
}
If (Get-GPWMIFilter @GetGPWmiFilterSplat) {
$SetGPWmiFilterAssignmentSplat = @{
Policy = $NewGPO
Filter = $Filter
EnableException = $True
ErrorAction = 'SilentlyContinue'
}
Set-GPWmiFilterAssignment @SetGPWmiFilterAssignmentSplat
}
}
}
Invoke-Command @InvokeCommandSplat
}
ForEach ($OU in $GroupPolicy.LinkedOUs) {
If (Test-Path "AD:\$($OU + (',{0}' -f (Get-ADRootDSE).rootDomainNamingContext))") {
Try {
Write-Host "Linking policy '$($NewGPO.DisplayName)' to OU '$($OU)' ..."
$NewGPLinkSplat = @{
Name = $NewGPO.DisplayName
Target = $OU + (',{0}' -f (Get-ADRootDSE).rootDomainNamingContext)
}
New-GPLink @NewGPLinkSplat | Out-Null
}
Catch {
$ParseErrors += "Could not link GPO '$($NewGPO.DisplayName)' to OU '$($OU)'"
Continue
}
}
Else {
$ParseErrors += "Path not accessible (referred to by '$($NewGPO.DisplayName)'): 'AD:\$($OU + (',{0}' -f (Get-ADRootDSE).rootDomainNamingContext))'"
Continue
}
}
}
}
If ($ParseErrors) {
Throw "One or more errors occurred:`n$($ParseErrors -join "`n")"
}
}

View File

@ -0,0 +1,83 @@
#Requires -Modules 'ActiveDirectory','powershell-yaml'
Param(
[Parameter(Mandatory)]
[hashtable]$Parameter
)
# Only executed on primary or standalone Domain Controller
If (@('primary','standalone') -contains $Parameter['deployment.type']) {
$PSDrive = Get-PSDrive -Name 'AD'
If ([boolean]$PSDrive -eq $False) {
$NewPSDriveSplat = @{
Name = 'ADDS'
Root = ''
PSProvider = 'ActiveDirectory'
}
$PSDrive = New-PSDrive @NewPSDriveSplat
}
$GetContentSplat = @{
Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', '.yml')
Raw = $True
}
$RawContent = Get-Content @GetContentSplat
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$WhiteList = ConvertFrom-Yaml @ConvertFromYamlSplat
$GetADObjectSplat = @{
Filter = '*'
SearchBase = 'DC=' + $Parameter['addsconfig.domainname'].Replace('.', ',DC=')
SearchScope = 'OneLevel'
}
$WhiteListedOUs = @()
ForEach ($OU in $WhiteList.WhiteListedOUs) {
$WhiteListedOUs += Get-ADObject @GetADObjectSplat | Where-Object {
$_.DistinguishedName -match $OU
}
}
$ParentContainers = Get-ADObject @GetADObjectSplat | Where-Object {
('builtinDomain', 'container', 'organizationalUnit', <#'lostAndFound',#> 'msDS-QuotaContainer', 'msTPM-InformationObjectsContainer') -contains $_.ObjectClass
}
ForEach ($Parent in $ParentContainers) {
If ($WhiteListedOUs.DistinguishedName -notcontains $Parent.DistinguishedName) {
ForEach ($SecurityPrincipal in $WhiteList.LimitedSecurityPrincipals) {
$GetACLSPlat = @{
Path = "$($PSDrive.Name):\$($Parent.DistinguishedName)"
}
$ACL = Get-ACL @GetACLSPlat
$GetADObjectSplat = @{
Filter = "sAMAccountName -eq '$($SecurityPrincipal)'"
Properties = 'objectSID'
}
$NewACE = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
(Get-ADObject @GetADObjectSplat).objectSID,
[System.DirectoryServices.ActiveDirectoryRights]"GenericAll",
[System.Security.AccessControl.AccessControlType]"Deny",
[System.DirectoryServices.ActiveDirectorySecurityInheritance]"All"
)
$ACL.AddAccessRule($NewACE)
$SetAclSplat = @{
Path = "$($PSDrive.Name):\$($Parent.DistinguishedName)"
AclObject = $ACL
ErrorAction = 'Continue'
}
Set-Acl @SetAclSplat
}
}
}
If ([boolean]$PSDrive.Name -eq 'ADDS') {
$RemovePSDriveSplat = @{
Name = 'ADDS'
Force = $True
Confirm = $False
}
Remove-PSDrive @RemovePSDriveSplat | Out-Null
}
}

View File

@ -0,0 +1,4 @@
WhiteListedOUs: [] # Entries will be concatenated with ',DC=<example>,DC=<org>' automatically
#- OU=User accounts
LimitedSecurityPrincipals: []
#- Servicedesk employees

View File

@ -0,0 +1,34 @@
#Requires -Modules 'ActiveDirectory'
Param(
[Parameter(Mandatory)]
[hashtable]$Parameter
)
# Only executed on primary or standalone Domain Controller
If (@('primary','standalone') -contains $Parameter['deployment.type']) {
$GetContentSplat = @{
Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', ".yml")
Raw = $True
}
$RawContent = Get-Content @GetContentSplat
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$Policy = ConvertFrom-Yaml @ConvertFromYamlSplat
$SetADDefaultDomainPasswordPolicySplat = @{
Identity = $Parameter['addsconfig.domainname']
ComplexityEnabled = [Convert]::ToBoolean($Policy.Password.RequireComplexity)
LockoutThreshold = [uint32]$Policy.Account.Lockout.Threshold
# LockoutDuration = [timespan]$Policy.Account.Lockout.Duration
# LockoutObservationWindow = [timespan]$Policy.Account.Lockout.ObservationWindow
MaxPasswordAge = [timespan]$Policy.Password.Age.Maximum
MinPasswordAge = [timespan]$Policy.Password.Age.Minimum
MinPasswordLength = [uint32]$Policy.Password.Length.Minimum
PasswordHistoryCount = [uint32]$Policy.Password.History
ReversibleEncryptionEnabled = [Convert]::ToBoolean($Policy.Password.ReversibleEncryption)
Confirm = $False
}
Set-ADDefaultDomainPasswordPolicy @SetADDefaultDomainPasswordPolicySplat
}

View File

@ -0,0 +1,14 @@
Account:
Lockout:
Threshold: 0
# Duration: '00:15:00.00'
# ObservationWindow: '00:05:00.00'
Password:
RequireComplexity: True
Age:
Minimum: 0
Maximum: 0
Length:
Minimum: 10
History: 0
ReversibleEncryption: False

View File

@ -1,52 +0,0 @@
[CmdletBinding()]
Param(
[Parameter(Mandatory)]
[string]$VMName,
[Parameter(Mandatory)]
[string]$VSphereFQDN,
[Parameter(Mandatory)]
[string]$VSphereUsername,
[Parameter(Mandatory)]
[string]$VSpherePassword
)
$PowerCliConfigurationSplat = @{
Scope = 'User'
ParticipateInCEIP = $False
Confirm = $False
InvalidCertificateAction = 'Ignore'
}
Set-PowerCLIConfiguration @PowerCliConfigurationSplat
$ConnectVIServerSplat = @{
Server = $VSphereFQDN
User = "$VSphereUsername"
Password = "$VSpherePassword"
WarningAction = 'SilentlyContinue'
}
Connect-VIServer @ConnectVIServerSplat | Out-Null
$GetVMSplat = @{
Name = $VMName
}
$VM = Get-VM @GetVMSplat
$GetHarddiskSplat = @{
VM = $VM
}
$Harddisk = Get-Harddisk @GetHarddiskSplat
$VMFolder = ($Harddisk.Filename.Substring(0, $Harddisk.Filename.LastIndexOf('/')) -split ' ')[1]
$NewDatastoreDriveSplat = @{
Name = 'ds'
Datastore = ($VM | Get-Datastore)
}
New-DatastoreDrive @NewDatastoreDriveSplat
$CopyDatastoreItemSplat = @{
Item = "ds:\$($VMFolder)\*.vmdk"
Destination = (Get-Item $PWD)
}
Copy-DatastoreItem @CopyDatastoreItemSplat
Disconnect-VIServer * -Confirm:$False

View File

@ -1,15 +1,6 @@
#Requires -Modules 'powershell-yaml'
[CmdletBinding()]
Param(
[Parameter(Mandatory)]
[ValidateScript({
If ([boolean]($_.IndexOfAny([io.path]::GetInvalidFileNameChars()) -lt 0)) {
$True
} Else {
Throw 'Provided input contains invalid characters; aborting.'
}
})]
[string]$TemplateName,
[Parameter(Mandatory)]
[ValidateScript({
If (Test-Path($_)) {
@ -18,11 +9,12 @@ Param(
Throw "'$_' is not a valid filename (within working directory '$PWD'), or access denied; aborting."
}
})]
[string]$OVFFile
[string]$OVFFile,
[hashtable]$Parameter
)
$GetContentSplat = @{
Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', ".$($TemplateName).yml")
Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', ".yml")
Raw = $True
}
$RawContent = Get-Content @GetContentSplat
@ -30,7 +22,24 @@ $ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$OVFConfig = ConvertFrom-Yaml @ConvertFromYamlSplat
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
# Check if the respective .yml file declared substitutions which need to be parsed
If (($YamlDocuments.Count -gt 1) -and $YamlDocuments[-1].Variables) {
ForEach ($Pattern in $YamlDocuments[-1].Variables) {
$RawContent = $RawContent -replace "\{\{ ($($Pattern.Name)) \}\}", [string](Invoke-Expression -Command $Pattern.Expression)
}
# Perform conversion to Yaml again, now with parsed file contents
$ConvertFromYamlSplat = @{
Yaml = $RawContent
AllDocuments = $True
}
$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat
$OVFConfig = $YamlDocuments[0..($YamlDocuments.Count - 2)]
}
Else {
$OVFConfig = $YamlDocuments
}
$SourceFile = Get-Item -Path $OVFFile
$GetContentSplat = @{
@ -71,6 +80,22 @@ If ($OVFConfig.DeploymentConfigurations.Count -gt 0) {
$XMLAttrTransport = $XML.CreateAttribute('transport', $XML.DocumentElement.ovf)
$XMLAttrTransport.Value = 'com.vmware.guestInfo'
[void]$XML.SelectSingleNode('//Any:VirtualHardwareSection', $NS).Attributes.Append($XMLAttrTransport)
ForEach ($ExtraConfig in $OVFConfig.AdvancedOptions) {
$XMLExtraConfig = $XML.CreateElement('vmw:ExtraConfig', $XML.DocumentElement.vmw)
$XMLExtraConfigAttrRequired = $XML.CreateAttribute('required', $XML.DocumentElement.ovf)
$XMLExtraConfigAttrRequired.Value = "$([boolean]$ExtraConfig.Required)".ToLower()
$XMLExtraConfigAttrKey = $XML.CreateAttribute('key', $XML.DocumentElement.vmw)
$XMLExtraConfigAttrKey.Value = $ExtraConfig.Key
$XMLExtraConfigAttrValue = $XML.CreateAttribute('value', $XML.DocumentElement.vmw)
$XMLExtraConfigAttrValue.Value = $ExtraConfig.Value
[void]$XMLExtraConfig.Attributes.Append($XMLExtraConfigAttrRequired)
[void]$XMLExtraConfig.Attributes.Append($XMLExtraConfigAttrKey)
[void]$XMLExtraConfig.Attributes.Append($XMLExtraConfigAttrValue)
[void]$XML.SelectSingleNode('//Any:VirtualHardwareSection', $NS).AppendChild($XMLExtraConfig)
}
Write-Host "Added $($OVFConfig.AdvancedOptions.Count) 'vmw:ExtraConfig' nodes"
$XMLProductSection = $XML.SelectSingleNode('//Any:ProductSection', $NS)
If ($XMLProductSection -eq $Null) {
@ -103,13 +128,13 @@ ForEach ($Category in $OVFConfig.PropertyCategories) {
$XMLPropertyAttrKey.Value = $Property.Key
$XMLPropertyAttrType = $XML.CreateAttribute('type', $XML.DocumentElement.ovf)
Switch -regex ($Property.Type) {
'boolean' {
'^boolean' {
$XMLPropertyAttrType.Value = 'boolean'
}
'int' {
'^int' {
$XMLPropertyAttrType.Value = 'uint8'
$Qualifiers = @()
If ($Property.Type -match 'int\((\d*)\.\.(\d*)\)') {
If ($Property.Type -match '^int\((\d*)\.\.(\d*)\)') {
If ($Matches[1]) {
$Qualifiers += "MinValue($($Matches[1]))"
}
@ -121,20 +146,20 @@ ForEach ($Category in $OVFConfig.PropertyCategories) {
[void]$XMLProperty.Attributes.Append($XMLPropertyAttrQualifiers)
}
}
'ip' {
'^ip' {
$XMLPropertyAttrType.Value = 'string'
$XMLPropertyAttrQualifiers = $XML.CreateAttribute('qualifiers', $XML.DocumentElement.vmw)
$XMLPropertyAttrQualifiers.Value = 'Ip'
[void]$XMLProperty.Attributes.Append($XMLPropertyAttrQualifiers)
}
'password' {
'^password' {
$XMLPropertyAttrType.Value = 'string'
$XMLPropertyAttrPassword = $XML.CreateAttribute('password', $XML.DocumentElement.ovf)
$XMLPropertyAttrPassword.Value = 'true'
[void]$XMLProperty.Attributes.Append($XMLPropertyAttrPassword)
$Qualifiers = @()
If ($Property.Type -match 'password\((\d*)\.\.(\d*)\)') {
If ($Property.Type -match '^password\((\d*)\.\.(\d*)\)') {
If ($Matches[1]) {
$Qualifiers += "MinLen($($Matches[1]))"
}
@ -146,10 +171,10 @@ ForEach ($Category in $OVFConfig.PropertyCategories) {
[void]$XMLProperty.Attributes.Append($XMLPropertyAttrQualifiers)
}
}
'string' {
'^string' {
$XMLPropertyAttrType.Value = 'string'
$Qualifiers = @()
If ($Property.Type -match 'string\((\d*)\.\.(\d*)\)') {
If ($Property.Type -match '^string\((\d*)\.\.(\d*)\)') {
If ($Matches[1]) {
$Qualifiers += "MinLen($($Matches[1]))"
}
@ -159,7 +184,7 @@ ForEach ($Category in $OVFConfig.PropertyCategories) {
$XMLPropertyAttrQualifiers = $XML.CreateAttribute('qualifiers', $XML.DocumentElement.ovf)
$XMLPropertyAttrQualifiers.Value = $Qualifiers -join ' '
[void]$XMLProperty.Attributes.Append($XMLPropertyAttrQualifiers)
} ElseIf ($Property.Type -match 'string\[(.*)\]') {
} ElseIf ($Property.Type -match '^string\[(.*)\]') {
$XMLPropertyAttrQualifiers = $XML.CreateAttribute('qualifiers', $XML.DocumentElement.ovf)
$XMLPropertyAttrQualifiers.Value = "ValueMap{$($Matches[1] -replace '","', '", "')}"
[void]$XMLProperty.Attributes.Append($XMLPropertyAttrQualifiers)

View File

@ -0,0 +1,200 @@
DeploymentConfigurations:
- Id: primary
Label: Primary (redundant deployment)
Description: Initial Domain Controller with 'PDC Emulator'-role
- Id: secondary
Label: Secondary (redundant deployment)
Description: Additional Domain Controller
- Id: standalone
Label: Stand-alone (non-redundant deployment)
Description: Single Domain Controller
PropertyCategories:
- Name: ''
ProductProperties:
- Key: deployment.type
Type: string
Value:
- primary
- secondary
- standalone
UserConfigurable: false
- Name: 1) Operating System
ProductProperties:
- Key: guestinfo.hostname
Type: string(1..15)
Label: Hostname*
Description: '(max length: 15 characters)'
DefaultValue: ''
Configurations: '*'
UserConfigurable: true
- Name: 2) Networking
ProductProperties:
- Key: guestinfo.ipaddress
Type: ip
Label: IP Address*
Description: ''
DefaultValue: ''
Configurations: '*'
UserConfigurable: true
- Key: guestinfo.prefixlength
Type: int(8..32)
Label: Subnet prefix length*
Description: ''
DefaultValue: '24'
Configurations: '*'
UserConfigurable: true
- Key: guestinfo.dnsserver
Type: ip
Label: DNS server*
Description: Specify IP address of existing primary Domain Controller
DefaultValue: '127.0.0.1'
Configurations:
- secondary
UserConfigurable: true
- Key: guestinfo.gateway
Type: ip
Label: Gateway*
Description: ''
DefaultValue: ''
Configurations: '*'
UserConfigurable: true
- Name: 3) Active Directory Domain Services
ProductProperties:
- Key: addsconfig.domainname
Type: string(5..)
Label: Domain name*
Description: 'Must be a valid FQDN'
DefaultValue: ''
Configurations: '*'
UserConfigurable: true
- Key: addsconfig.netbiosname
Type: string(1..15)
Label: Domain short name (NetBIOS)*
Description: '(max length: 15 characters)'
DefaultValue: ''
Configurations: '*'
UserConfigurable: true
- Key: addsconfig.administratorpw
Type: password(7..)
Label: Domain Administrator password*
Description: Must meet password complexity rules
DefaultValue: ''
Configurations: '*'
UserConfigurable: true
- Key: addsconfig.safemodepw
Type: password(7..)
Label: Safe-mode password*
Description: Must meet password complexity rules
DefaultValue: ''
Configurations: '*'
UserConfigurable: true
- Key: addsconfig.ntpserver
Type: string(1..)
Label: Time server*
Description: A comma-separated list of upstream timeservers
DefaultValue: 0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org
Configurations:
- primary
- standalone
UserConfigurable: true
- Name: 4) Credential Management
ProductProperties:
- Key: vault.api
Type: string
Label: Vault API address
Description: The uri on which a HashiCorp Vault REST API can be reached
DefaultValue: ''
Configurations:
- primary
- standalone
UserConfigurable: true
- Key: vault.token
Type: password
Label: Vault API token
Description: An access token which has permissions to read/write to the Vault secrets engine
DefaultValue: ''
Configurations:
- primary
- standalone
UserConfigurable: true
- Key: vault.pwpolicy
Type: string
Label: Vault password policy
Description: A Vault password policy which determines complexity rules for generated passwords
DefaultValue: ''
Configurations:
- primary
- standalone
UserConfigurable: true
- Key: vault.secret
Type: string
Label: Vault secret name
Description: The name of the secret that all generated passwords will be stored in (as key/value pairs)
DefaultValue: ''
Configurations:
- primary
- standalone
UserConfigurable: true
- Name: 5) DHCP default scope
ProductProperties:
- Key: dhcpconfig.startip
Type: ip
Label: Start IP address
Description: ''
DefaultValue: '0.0.0.0'
Configurations:
- secondary
- standalone
UserConfigurable: true
- Key: dhcpconfig.endip
Type: ip
Label: End IP address
Description: ''
DefaultValue: '0.0.0.0'
Configurations:
- secondary
- standalone
UserConfigurable: true
- Key: dhcpconfig.subnetmask
Type: ip
Label: Subnet mask
Description: ''
DefaultValue: '255.255.255.0'
Configurations:
- secondary
- standalone
UserConfigurable: true
- Key: dhcpconfig.gateway
Type: ip
Label: Gateway IP address
Description: ''
DefaultValue: '0.0.0.0'
Configurations:
- secondary
- standalone
UserConfigurable: true
- Key: dhcpconfig.leaseduration
Type: string(1..)
Label: Lease duration
Description: 'Enter as timestamp format (DD.HH:MM:SS.FFFF), or as a number of seconds'
DefaultValue: '01:00:00.00'
Configurations:
- secondary
- standalone
UserConfigurable: true
AdvancedOptions:
- Key: appliance.name
Value: "{{ appliance.name }}"
Required: false
- Key: appliance.version
Value: "{{ appliance.version }}"
Required: false
---
Variables:
- Name: appliance.name
Expression: |
$Parameter['appliance.name']
- Name: appliance.version
Expression: |
$Parameter['appliance.version']