Full Script Source
This page renders the reference files directly from /scripts. The displayed code is therefore the same version that is parsed and versioned in the repository; it is not a second, manually maintained copy.
Use the copy button on each block to paste the complete script into the appropriate local PowerShell or shell session. Read the relevant runbook first: execution order, prerequisites, configurable parameters, validation evidence, and validated boundaries remain in the runbook and Script Catalog.
Deploy-HQ-CHR01.sh
#!/usr/bin/env bash
set -Eeuo pipefail
VMID="${VMID:-4010}"
SOURCE_TEMPLATE_VMID="${SOURCE_TEMPLATE_VMID:-4004}"
VM_NAME="${VM_NAME:-HQ-CHR01}"
WAN_BRIDGE="${WAN_BRIDGE:-GEILWAN}"
LAN_BRIDGE="${LAN_BRIDGE:-GEILLAN}"
WAN_IP_CIDR="${WAN_IP_CIDR:-172.31.255.2/30}"
WAN_GATEWAY="${WAN_GATEWAY:-172.31.255.1}"
TIMEZONE_NAME="${TIMEZONE_NAME:-America/Santo_Domingo}"
AUTORUN_IDENTITY_NAME="${AUTORUN_IDENTITY_NAME:-$VM_NAME}"
STARTUP_ORDER="${STARTUP_ORDER:-10}"
STARTUP_UP_DELAY="${STARTUP_UP_DELAY:-30}"
STARTUP_DOWN_DELAY="${STARTUP_DOWN_DELAY:-60}"
KEEP_AUTORUN_TEST_FILE="${KEEP_AUTORUN_TEST_FILE:-0}"
SKIP_BOOT_VALIDATION="${SKIP_BOOT_VALIDATION:-0}"
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}
fail() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "Required command not found: $1"
}
bridge_exists() {
ip link show "$1" >/dev/null 2>&1
}
cleanup_mount() {
mountpoint -q /mnt/gntech-chr-rw && umount /mnt/gntech-chr-rw || true
}
mount_rw_partition() {
local disk_path zvol_dev
disk_path="$(qm config "$VMID" | awk -F': ' '/^virtio0:/ {print $2; exit}' | cut -d',' -f1)"
[[ -n "$disk_path" ]] || fail "Could not resolve virtio0 path for VM $VMID"
zvol_dev="$(readlink -f "/dev/zvol/$disk_path")"
[[ -b "$zvol_dev" ]] || fail "Resolved zvol device is not a block device: $zvol_dev"
[[ -b "${zvol_dev}p2" ]] || fail "RouterOS RW partition not found: ${zvol_dev}p2"
mkdir -p /mnt/gntech-chr-rw
mount -o rw "${zvol_dev}p2" /mnt/gntech-chr-rw
}
write_autorun() {
local marker_line=""
if [[ "$KEEP_AUTORUN_TEST_FILE" == "1" ]]; then
marker_line='/file print file=bootstrap-validation'
fi
cat > /mnt/gntech-chr-rw/rw/autorun.scr <<EOF
/system identity set name=${AUTORUN_IDENTITY_NAME}
/system clock set time-zone-name=${TIMEZONE_NAME}
/ip dhcp-client remove [find where interface=ether1]
/ip address add address=${WAN_IP_CIDR} interface=ether1 comment=WAN
/ip route add dst-address=0.0.0.0/0 gateway=${WAN_GATEWAY} comment=Default
${marker_line}
EOF
sync
}
validate_boot() {
log "Waiting for QEMU Guest Agent on VM ${VMID}"
local ready=0
for _ in $(seq 1 30); do
if qm guest cmd "$VMID" ping >/dev/null 2>&1; then
ready=1
break
fi
sleep 2
done
[[ "$ready" == "1" ]] || fail "QEMU Guest Agent did not respond within 60 seconds on VM ${VMID}"
log "Collecting first-boot guest state"
qm guest cmd "$VMID" get-osinfo
qm guest cmd "$VMID" get-host-name
qm guest cmd "$VMID" network-get-interfaces
}
trap cleanup_mount EXIT
require_command qm
require_command ip
require_command mount
require_command umount
require_command readlink
qm status "$SOURCE_TEMPLATE_VMID" >/dev/null 2>&1 || fail "Source template VMID ${SOURCE_TEMPLATE_VMID} not found"
qm config "$SOURCE_TEMPLATE_VMID" | grep -q '^template: 1' || fail "Source VMID ${SOURCE_TEMPLATE_VMID} is not a template"
qm status "$VMID" >/dev/null 2>&1 && fail "Target VMID ${VMID} already exists"
bridge_exists "$WAN_BRIDGE" || fail "WAN bridge ${WAN_BRIDGE} does not exist"
bridge_exists "$LAN_BRIDGE" || fail "LAN bridge ${LAN_BRIDGE} does not exist"
log "Cloning template ${SOURCE_TEMPLATE_VMID} to ${VMID}"
qm clone "$SOURCE_TEMPLATE_VMID" "$VMID" --name "$VM_NAME" \
--description "Enterprise headquarters MikroTik CHR router and firewall. Clean validation deploy from TPL-CHR-7.21.5 (${SOURCE_TEMPLATE_VMID})."
log "Applying Proxmox-side runtime settings"
qm set "$VMID" \
--net0 "virtio,bridge=${WAN_BRIDGE},firewall=0" \
--net1 "virtio,bridge=${LAN_BRIDGE},firewall=0" \
--onboot 0 \
--startup "order=${STARTUP_ORDER},up=${STARTUP_UP_DELAY},down=${STARTUP_DOWN_DELAY}" \
--tags "enterprise;hq;router;firewall;mikrotik"
log "Injecting first-boot RouterOS bootstrap into rw/autorun.scr"
mount_rw_partition
write_autorun
cleanup_mount
log "Starting VM ${VMID}"
qm start "$VMID"
if [[ "$SKIP_BOOT_VALIDATION" != "1" ]]; then
validate_boot
fi
log "Deployment completed for VM ${VMID}"
Invoke-Qga-WindowsBootstrap.sh
#!/usr/bin/env bash
set -Eeuo pipefail
VMID="${VMID:-}"
GUEST_NAME="${GUEST_NAME:-Windows guest}"
IPV4_ADDRESS="${IPV4_ADDRESS:-}"
PREFIX_LENGTH="${PREFIX_LENGTH:-24}"
GATEWAY="${GATEWAY:-}"
DNS_SERVERS="${DNS_SERVERS:-}"
ENABLE_RDP="${ENABLE_RDP:-1}"
TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-900}"
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}
fail() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "Required command not found: $1"
}
require_value() {
local name="$1"
local value="$2"
[[ -n "$value" ]] || fail "Missing required value: $name"
}
wait_for_qga() {
local ready=0
for _ in $(seq 1 60); do
if qm guest cmd "$VMID" ping >/dev/null 2>&1; then
ready=1
break
fi
sleep 2
done
[[ "$ready" == "1" ]] || fail "QEMU Guest Agent did not respond on VM $VMID"
}
run_powershell_via_qga() {
local script_file="$1"
local encoded
encoded="$(
iconv -f UTF-8 -t UTF-16LE "$script_file" |
base64 -w 0
)"
local response pid
response="$(
qm guest exec "$VMID" -- \
powershell.exe \
-NoProfile \
-NonInteractive \
-ExecutionPolicy Bypass \
-EncodedCommand "$encoded"
)"
pid="$(printf '%s\n' "$response" | sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -n 1)"
[[ -n "$pid" ]] || fail "Could not obtain guest PID from qm guest exec response"
local waited=0
while (( waited < TIMEOUT_SECONDS )); do
local status
status="$(qm guest exec-status "$VMID" "$pid")"
if printf '%s' "$status" | grep -q '"exited"[[:space:]]*:[[:space:]]*true'; then
printf '%s\n' "$status"
return 0
fi
sleep 5
waited=$(( waited + 5 ))
done
fail "Guest command timed out after ${TIMEOUT_SECONDS} seconds"
}
require_command qm
require_command iconv
require_command base64
require_command sed
require_value "VMID" "$VMID"
require_value "IPV4_ADDRESS" "$IPV4_ADDRESS"
require_value "GATEWAY" "$GATEWAY"
require_value "DNS_SERVERS" "$DNS_SERVERS"
qm status "$VMID" >/dev/null 2>&1 || fail "VMID $VMID was not found"
log "Waiting for QEMU Guest Agent on VM $VMID ($GUEST_NAME)"
wait_for_qga
TMP_SCRIPT="$(mktemp)"
trap 'rm -f "$TMP_SCRIPT"' EXIT
DNS_SERVERS_CSV="$DNS_SERVERS" python3 - <<'PY' > "${TMP_SCRIPT}.dns"
import os
servers = [s.strip() for s in os.environ["DNS_SERVERS_CSV"].split(",") if s.strip()]
print(",".join("'" + s.replace("'", "''") + "'" for s in servers))
PY
DNS_ARRAY_LITERAL="$(cat "${TMP_SCRIPT}.dns")"
rm -f "${TMP_SCRIPT}.dns"
cat > "$TMP_SCRIPT" <<EOF
\$ErrorActionPreference = 'Stop'
\$ProgressPreference = 'SilentlyContinue'
function Get-PrimaryAdapter {
\$adapter = Get-NetAdapter |
Where-Object Status -eq 'Up' |
Sort-Object ifIndex |
Select-Object -First 1
if (-not \$adapter) {
throw 'No active network adapter was found.'
}
\$adapter
}
function Set-ExactIPv4Configuration {
param(
[int]\$InterfaceIndex,
[string]\$DesiredIPv4,
[int]\$DesiredPrefixLength,
[string]\$DesiredGateway
)
\$existing = Get-NetIPAddress -InterfaceIndex \$InterfaceIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue
foreach (\$address in \$existing) {
if (\$address.IPAddress -eq \$DesiredIPv4 -and \$address.PrefixLength -eq \$DesiredPrefixLength) {
continue
}
if (\$address.IPAddress -like '169.254.*') {
continue
}
Remove-NetIPAddress -InterfaceIndex \$InterfaceIndex -AddressFamily IPv4 -IPAddress \$address.IPAddress -Confirm:\$false -ErrorAction SilentlyContinue
}
\$desiredAddress = Get-NetIPAddress -InterfaceIndex \$InterfaceIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object { \$_.IPAddress -eq \$DesiredIPv4 -and \$_.PrefixLength -eq \$DesiredPrefixLength } |
Select-Object -First 1
if (-not \$desiredAddress) {
New-NetIPAddress -InterfaceIndex \$InterfaceIndex -IPAddress \$DesiredIPv4 -PrefixLength \$DesiredPrefixLength | Out-Null
}
\$defaultRoutes = Get-NetRoute -InterfaceIndex \$InterfaceIndex -DestinationPrefix '0.0.0.0/0' -AddressFamily IPv4 -ErrorAction SilentlyContinue
foreach (\$route in \$defaultRoutes) {
if (\$route.NextHop -ne \$DesiredGateway) {
Remove-NetRoute -InterfaceIndex \$InterfaceIndex -DestinationPrefix '0.0.0.0/0' -NextHop \$route.NextHop -Confirm:\$false -ErrorAction SilentlyContinue
}
}
\$desiredRoute = Get-NetRoute -InterfaceIndex \$InterfaceIndex -DestinationPrefix '0.0.0.0/0' -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object NextHop -eq \$DesiredGateway |
Select-Object -First 1
if (-not \$desiredRoute) {
New-NetRoute -InterfaceIndex \$InterfaceIndex -DestinationPrefix '0.0.0.0/0' -NextHop \$DesiredGateway -RouteMetric 256 | Out-Null
}
}
\$adapter = Get-PrimaryAdapter
Set-ExactIPv4Configuration -InterfaceIndex \$adapter.ifIndex -DesiredIPv4 '${IPV4_ADDRESS}' -DesiredPrefixLength ${PREFIX_LENGTH} -DesiredGateway '${GATEWAY}'
Set-DnsClientServerAddress -InterfaceIndex \$adapter.ifIndex -ServerAddresses @(${DNS_ARRAY_LITERAL})
if (${ENABLE_RDP} -eq 1) {
Set-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server' -Name 'fDenyTSConnections' -Value 0
Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' | Out-Null
}
[pscustomobject]@{
ComputerName = \$env:COMPUTERNAME
Adapter = \$adapter.Name
IPv4 = '${IPV4_ADDRESS}'
PrefixLength = ${PREFIX_LENGTH}
Gateway = '${GATEWAY}'
DnsServers = '${DNS_SERVERS}'
RdpEnabled = [bool](${ENABLE_RDP})
}
EOF
log "Applying guest network bootstrap through QGA"
run_powershell_via_qga "$TMP_SCRIPT"
log "Bootstrap completed for VM $VMID"
qga-run.sh
#!/usr/bin/env bash
set -Eeuo pipefail
usage() {
cat <<'EOF'
Usage:
qga-run.sh <VMID> <PowerShell-script.ps1> [timeout-seconds]
Example:
qga-run.sh 4000 validate-golden-image.ps1 900
EOF
}
fail() {
echo "ERROR: $*" >&2
exit 1
}
[[ $# -ge 2 && $# -le 3 ]] || {
usage
exit 2
}
VMID="$1"
SCRIPT_FILE="$2"
TIMEOUT_SECONDS="${3:-900}"
POLL_SECONDS=5
command -v qm >/dev/null 2>&1 || fail "qm was not found."
command -v jq >/dev/null 2>&1 || fail "jq was not found. Install it with: apt install jq"
command -v iconv >/dev/null 2>&1 || fail "iconv was not found."
command -v base64 >/dev/null 2>&1 || fail "base64 was not found."
[[ -f "$SCRIPT_FILE" ]] || fail "PowerShell script not found: $SCRIPT_FILE"
[[ "$TIMEOUT_SECONDS" =~ ^[0-9]+$ ]] || fail "Timeout must be an integer."
qm status "$VMID" >/dev/null 2>&1 || fail "VMID $VMID does not exist."
[[ "$(qm status "$VMID" | awk '{print $2}')" == "running" ]] || fail "VMID $VMID is not running."
qm guest cmd "$VMID" ping >/dev/null 2>&1 || fail "QEMU Guest Agent is not responding for VMID $VMID."
ENCODED_COMMAND="$(iconv -f UTF-8 -t UTF-16LE "$SCRIPT_FILE" | base64 -w 0)"
START_RESPONSE="$(
qm guest exec "$VMID" -- powershell.exe -NoLogo -NoProfile -NonInteractive \
-ExecutionPolicy Bypass -EncodedCommand "$ENCODED_COMMAND"
)"
if jq -e '.exited == 1' >/dev/null 2>&1 <<<"$START_RESPONSE"; then
FINAL_RESPONSE="$START_RESPONSE"
else
PID="$(jq -r '.pid // empty' <<<"$START_RESPONSE")"
[[ -n "$PID" ]] || fail "Unable to determine guest execution PID. Response: $START_RESPONSE"
echo "Guest execution started with PID $PID."
START_TIME="$(date +%s)"
while true; do
FINAL_RESPONSE="$(qm guest exec-status "$VMID" "$PID")"
jq -e '.exited == 1' >/dev/null 2>&1 <<<"$FINAL_RESPONSE" && break
CURRENT_TIME="$(date +%s)"
if (( CURRENT_TIME - START_TIME >= TIMEOUT_SECONDS )); then
echo "Timeout reached; the guest process may still be running."
echo "Check it with: qm guest exec-status $VMID $PID"
exit 124
fi
sleep "$POLL_SECONDS"
done
fi
STDOUT="$(jq -r '."out-data" // empty' <<<"$FINAL_RESPONSE")"
STDERR="$(jq -r '."err-data" // empty' <<<"$FINAL_RESPONSE")"
EXIT_CODE="$(jq -r '.exitcode // 1' <<<"$FINAL_RESPONSE")"
[[ -z "$STDOUT" ]] || printf '%s\n' "$STDOUT"
[[ -z "$STDERR" ]] || printf '%s\n' "$STDERR" >&2
[[ "$EXIT_CODE" -eq 0 ]] || fail "PowerShell script failed with exit code $EXIT_CODE."
echo "QGA execution completed successfully."
Configure-HQ-DC01-GPOBaseline.ps1
param(
[string]$DomainFqdn,
[string]$DomainDn,
[string]$EnterpriseRootOuDn,
[string]$DomainControllersDn,
[string]$ServersOuDn,
[string]$WorkstationsOuDn,
[string]$DcBaselineGpoName,
[string]$ServerBaselineGpoName,
[string]$WorkstationBaselineGpoName,
[switch]$ValidateOnly
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Read-RequiredValue {
param(
[string]$Prompt,
[string]$DefaultValue
)
$fullPrompt = $Prompt
if ($DefaultValue) { $fullPrompt = "$Prompt [$DefaultValue]" }
while ($true) {
$value = Read-Host -Prompt $fullPrompt
if (-not [string]::IsNullOrWhiteSpace($value)) { return $value.Trim() }
if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) { return $DefaultValue }
}
}
function Resolve-Inputs {
$script:DomainFqdn = if ([string]::IsNullOrWhiteSpace($DomainFqdn)) { Read-RequiredValue 'Domain FQDN' 'corp.gntech.me' } else { $DomainFqdn.Trim() }
$script:DomainDn = if ([string]::IsNullOrWhiteSpace($DomainDn)) { Read-RequiredValue 'Domain DN' 'DC=corp,DC=gntech,DC=me' } else { $DomainDn.Trim() }
$script:EnterpriseRootOuDn = if ([string]::IsNullOrWhiteSpace($EnterpriseRootOuDn)) { Read-RequiredValue 'Enterprise root OU DN' 'OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $EnterpriseRootOuDn.Trim() }
$script:DomainControllersDn = if ([string]::IsNullOrWhiteSpace($DomainControllersDn)) { Read-RequiredValue 'Domain Controllers OU DN' 'OU=Domain Controllers,DC=corp,DC=gntech,DC=me' } else { $DomainControllersDn.Trim() }
$script:ServersOuDn = if ([string]::IsNullOrWhiteSpace($ServersOuDn)) { Read-RequiredValue 'Servers OU DN' 'OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $ServersOuDn.Trim() }
$script:WorkstationsOuDn = if ([string]::IsNullOrWhiteSpace($WorkstationsOuDn)) { Read-RequiredValue 'Workstations OU DN' 'OU=Workstations,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $WorkstationsOuDn.Trim() }
$script:DcBaselineGpoName = if ([string]::IsNullOrWhiteSpace($DcBaselineGpoName)) { Read-RequiredValue 'DC baseline GPO name' 'GPO-DC-Baseline' } else { $DcBaselineGpoName.Trim() }
$script:ServerBaselineGpoName = if ([string]::IsNullOrWhiteSpace($ServerBaselineGpoName)) { Read-RequiredValue 'Server baseline GPO name' 'GPO-Servers-Baseline' } else { $ServerBaselineGpoName.Trim() }
$script:WorkstationBaselineGpoName = if ([string]::IsNullOrWhiteSpace($WorkstationBaselineGpoName)) { Read-RequiredValue 'Workstation baseline GPO name' 'GPO-Workstations-Baseline' } else { $WorkstationBaselineGpoName.Trim() }
}
function Ensure-Module {
param([string]$Name)
Import-Module $Name
}
function Ensure-Gpo {
param(
[string]$Name,
[string]$Comment
)
$gpo = Get-GPO -Name $Name -ErrorAction SilentlyContinue
if (-not $gpo) {
$gpo = New-GPO -Name $Name -Comment $Comment
}
return $gpo
}
function Ensure-GpoLink {
param(
[string]$Name,
[string]$Target
)
$existingLink = @((Get-GPInheritance -Target $Target).GpoLinks | Where-Object DisplayName -eq $Name)
if ($existingLink.Count -eq 0) {
New-GPLink -Name $Name -Target $Target -LinkEnabled Yes | Out-Null
}
}
function Set-BaselineRegistryValue {
param(
[string]$GpoName,
[string]$Key,
[string]$ValueName,
[string]$Type,
[object]$Value
)
Set-GPRegistryValue -Name $GpoName -Key $Key -ValueName $ValueName -Type $Type -Value $Value | Out-Null
}
Resolve-Inputs
Ensure-Module -Name ActiveDirectory
Ensure-Module -Name GroupPolicy
$summary = [pscustomobject]@{
DomainFqdn = $script:DomainFqdn
DomainDn = $script:DomainDn
EnterpriseRootOuDn = $script:EnterpriseRootOuDn
DomainControllersDn = $script:DomainControllersDn
ServersOuDn = $script:ServersOuDn
WorkstationsOuDn = $script:WorkstationsOuDn
DcBaselineGpoName = $script:DcBaselineGpoName
ServerBaselineGpoName = $script:ServerBaselineGpoName
WorkstationBaselineGpoName = $script:WorkstationBaselineGpoName
ValidateOnly = [bool]$ValidateOnly
}
$summary | Format-List | Out-String | Write-Output
if ($ValidateOnly) {
Write-Output 'Validation mode only. GPO baseline was not applied.'
return
}
Set-ADDefaultDomainPasswordPolicy `
-Identity $script:DomainFqdn `
-ComplexityEnabled $true `
-MinPasswordLength 14 `
-PasswordHistoryCount 24 `
-MinPasswordAge 1.00:00:00 `
-MaxPasswordAge 90.00:00:00 `
-LockoutThreshold 10 `
-LockoutDuration 0.00:15:00 `
-LockoutObservationWindow 0.00:15:00
$definitions = @(
@{ Name = $script:DcBaselineGpoName; Comment = 'Baseline for domain controllers'; Target = $script:DomainControllersDn },
@{ Name = $script:ServerBaselineGpoName; Comment = 'Baseline for member servers'; Target = $script:ServersOuDn },
@{ Name = $script:WorkstationBaselineGpoName; Comment = 'Baseline for workstation endpoints'; Target = $script:WorkstationsOuDn }
)
foreach ($definition in $definitions) {
Ensure-Gpo -Name $definition.Name -Comment $definition.Comment | Out-Null
Ensure-GpoLink -Name $definition.Name -Target $definition.Target
}
foreach ($gpoName in @($script:DcBaselineGpoName, $script:ServerBaselineGpoName)) {
Set-BaselineRegistryValue -GpoName $gpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Application' -ValueName 'MaxSize' -Type DWord -Value 131072
Set-BaselineRegistryValue -GpoName $gpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Security' -ValueName 'MaxSize' -Type DWord -Value 262144
Set-BaselineRegistryValue -GpoName $gpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\System' -ValueName 'MaxSize' -Type DWord -Value 131072
}
Set-BaselineRegistryValue -GpoName $script:WorkstationBaselineGpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Application' -ValueName 'MaxSize' -Type DWord -Value 65536
Set-BaselineRegistryValue -GpoName $script:WorkstationBaselineGpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Security' -ValueName 'MaxSize' -Type DWord -Value 131072
Set-BaselineRegistryValue -GpoName $script:WorkstationBaselineGpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\EventLog\System' -ValueName 'MaxSize' -Type DWord -Value 65536
foreach ($gpoName in @($script:DcBaselineGpoName, $script:ServerBaselineGpoName, $script:WorkstationBaselineGpoName)) {
Set-BaselineRegistryValue -GpoName $gpoName -Key 'HKLM\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging' -ValueName 'EnableScriptBlockLogging' -Type DWord -Value 1
}
Write-Output 'GPO baseline completed successfully.'
Configure-HQ-DC01-PostPromotion.ps1
param(
[string]$DomainFqdn,
[string]$DomainDn,
[string]$DcFqdn,
[string]$DcIp,
[string]$UpnSuffix,
[string[]]$DnsForwarders,
[string]$ServerReverseNetworkId,
[string]$WorkstationReverseNetworkId,
[string]$DhcpScopeName,
[string]$DhcpScopeId,
[string]$DhcpStartRange,
[string]$DhcpEndRange,
[string]$DhcpSubnetMask,
[string]$DhcpRouter,
[string]$EnterpriseRootOuName,
[string]$OuTemplateCsvPath,
[switch]$ValidateOnly
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Read-RequiredValue {
param(
[string]$Prompt,
[string]$DefaultValue
)
$fullPrompt = $Prompt
if ($DefaultValue) {
$fullPrompt = "$Prompt [$DefaultValue]"
}
while ($true) {
$value = Read-Host -Prompt $fullPrompt
if (-not [string]::IsNullOrWhiteSpace($value)) {
return $value.Trim()
}
if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) {
return $DefaultValue
}
}
}
function Read-StringList {
param(
[string]$Prompt,
[string[]]$DefaultValues
)
while ($true) {
$defaultValue = $DefaultValues -join ','
$value = Read-RequiredValue -Prompt $Prompt -DefaultValue $defaultValue
$items = @(
$value.Split(',') |
ForEach-Object { $_.Trim() } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
if ($items.Count -gt 0) {
return $items
}
}
}
function Resolve-Inputs {
$script:DomainFqdn = if ([string]::IsNullOrWhiteSpace($DomainFqdn)) {
Read-RequiredValue -Prompt 'Domain FQDN' -DefaultValue 'corp.gntech.me'
} else { $DomainFqdn.Trim() }
$script:DomainDn = if ([string]::IsNullOrWhiteSpace($DomainDn)) {
Read-RequiredValue -Prompt 'Domain distinguished name' -DefaultValue 'DC=corp,DC=gntech,DC=me'
} else { $DomainDn.Trim() }
$script:DcFqdn = if ([string]::IsNullOrWhiteSpace($DcFqdn)) {
Read-RequiredValue -Prompt 'Domain controller FQDN' -DefaultValue 'HQ-DC01.corp.gntech.me'
} else { $DcFqdn.Trim() }
$script:DcIp = if ([string]::IsNullOrWhiteSpace($DcIp)) {
Read-RequiredValue -Prompt 'Domain controller IPv4' -DefaultValue '172.20.20.11'
} else { $DcIp.Trim() }
$script:UpnSuffix = if ([string]::IsNullOrWhiteSpace($UpnSuffix)) {
Read-RequiredValue -Prompt 'User UPN suffix' -DefaultValue 'gntech.me'
} else { $UpnSuffix.Trim() }
$script:DnsForwarders = if ($DnsForwarders.Count -eq 0) {
Read-StringList -Prompt 'DNS forwarders (comma-separated)' -DefaultValues @('1.1.1.1','9.9.9.9')
} else {
@($DnsForwarders | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
$script:ServerReverseNetworkId = if ([string]::IsNullOrWhiteSpace($ServerReverseNetworkId)) {
Read-RequiredValue -Prompt 'Server reverse zone network' -DefaultValue '172.20.20.0/24'
} else { $ServerReverseNetworkId.Trim() }
$script:WorkstationReverseNetworkId = if ([string]::IsNullOrWhiteSpace($WorkstationReverseNetworkId)) {
Read-RequiredValue -Prompt 'Workstation reverse zone network' -DefaultValue '172.20.30.0/24'
} else { $WorkstationReverseNetworkId.Trim() }
$script:DhcpScopeName = if ([string]::IsNullOrWhiteSpace($DhcpScopeName)) {
Read-RequiredValue -Prompt 'DHCP scope name' -DefaultValue 'VLAN30 Workstations'
} else { $DhcpScopeName.Trim() }
$script:DhcpScopeId = if ([string]::IsNullOrWhiteSpace($DhcpScopeId)) {
Read-RequiredValue -Prompt 'DHCP scope ID' -DefaultValue '172.20.30.0'
} else { $DhcpScopeId.Trim() }
$script:DhcpStartRange = if ([string]::IsNullOrWhiteSpace($DhcpStartRange)) {
Read-RequiredValue -Prompt 'DHCP start range' -DefaultValue '172.20.30.100'
} else { $DhcpStartRange.Trim() }
$script:DhcpEndRange = if ([string]::IsNullOrWhiteSpace($DhcpEndRange)) {
Read-RequiredValue -Prompt 'DHCP end range' -DefaultValue '172.20.30.199'
} else { $DhcpEndRange.Trim() }
$script:DhcpSubnetMask = if ([string]::IsNullOrWhiteSpace($DhcpSubnetMask)) {
Read-RequiredValue -Prompt 'DHCP subnet mask' -DefaultValue '255.255.255.0'
} else { $DhcpSubnetMask.Trim() }
$script:DhcpRouter = if ([string]::IsNullOrWhiteSpace($DhcpRouter)) {
Read-RequiredValue -Prompt 'DHCP router option' -DefaultValue '172.20.30.1'
} else { $DhcpRouter.Trim() }
$script:EnterpriseRootOuName = if ([string]::IsNullOrWhiteSpace($EnterpriseRootOuName)) {
Read-RequiredValue -Prompt 'Enterprise root OU name' -DefaultValue 'GNTECH'
} else { $EnterpriseRootOuName.Trim() }
$script:OuTemplateCsvPath = if ([string]::IsNullOrWhiteSpace($OuTemplateCsvPath)) {
$scriptDirectory = if (-not [string]::IsNullOrWhiteSpace($PSScriptRoot)) {
$PSScriptRoot
} else {
(Get-Location).Path
}
$defaultCsv = Join-Path -Path $scriptDirectory -ChildPath 'ou-baseline.csv'
if (Test-Path -LiteralPath $defaultCsv) { $defaultCsv } else { '' }
} else { $OuTemplateCsvPath.Trim() }
}
function Ensure-Module {
param([string]$Name)
Import-Module $Name
}
function Ensure-UpnSuffix {
$partitionsDn = "CN=Partitions,CN=Configuration,$script:DomainDn"
$partitionsObject = Get-ADObject $partitionsDn -Properties uPNSuffixes
if ($partitionsObject.uPNSuffixes -notcontains $script:UpnSuffix) {
Set-ADObject $partitionsDn -Add @{ uPNSuffixes = $script:UpnSuffix }
}
}
function Ensure-DnsForwarders {
$existingForwarders = @(Get-DnsServerForwarder -ErrorAction SilentlyContinue | ForEach-Object {
if ($_.IPAddress -is [array]) {
$_.IPAddress | ForEach-Object { $_.IPAddressToString }
}
elseif ($_.IPAddress) {
$_.IPAddress.IPAddressToString
}
})
foreach ($forwarder in $script:DnsForwarders) {
if ($existingForwarders -notcontains $forwarder) {
Add-DnsServerForwarder -IPAddress $forwarder
}
}
}
function Ensure-DnsZone {
param([string]$NetworkId)
$networkAddress = $NetworkId.Split('/')[0]
$octets = $networkAddress.Split('.')
$zoneName = "$($octets[2]).$($octets[1]).$($octets[0]).in-addr.arpa"
if (-not (Get-DnsServerZone -Name $zoneName -ErrorAction SilentlyContinue)) {
Add-DnsServerPrimaryZone -NetworkId $NetworkId -ReplicationScope Forest
}
}
function Ensure-DhcpAuthorization {
$authorized = @(Get-DhcpServerInDC -ErrorAction SilentlyContinue)
if (-not ($authorized | Where-Object DnsName -eq $script:DcFqdn)) {
Add-DhcpServerInDC -DnsName $script:DcFqdn -IpAddress $script:DcIp
}
& netsh dhcp add securitygroups | Out-Null
Restart-Service DHCPServer -Force
Start-Sleep -Seconds 10
}
function Ensure-DhcpScope {
if (-not (Get-DhcpServerv4Scope -ErrorAction SilentlyContinue | Where-Object ScopeId -eq $script:DhcpScopeId)) {
Add-DhcpServerv4Scope `
-Name $script:DhcpScopeName `
-StartRange $script:DhcpStartRange `
-EndRange $script:DhcpEndRange `
-SubnetMask $script:DhcpSubnetMask `
-State Active
}
Set-DhcpServerv4OptionValue `
-ScopeId $script:DhcpScopeId `
-Router $script:DhcpRouter `
-DnsServer $script:DcIp `
-DnsDomain $script:DomainFqdn
Set-DhcpServerv4DnsSetting `
-DynamicUpdates Always `
-DeleteDnsRRonLeaseExpiry $true `
-UpdateDnsRRForOlderClients $true
}
function Ensure-OU {
param(
[string]$Name,
[string]$Path,
[bool]$ProtectedFromAccidentalDeletion = $true
)
$ouDn = "OU=$Name,$Path"
if (-not (Get-ADOrganizationalUnit -LDAPFilter "(distinguishedName=$ouDn)" -ErrorAction SilentlyContinue)) {
New-ADOrganizationalUnit -Name $Name -Path $Path -ProtectedFromAccidentalDeletion $ProtectedFromAccidentalDeletion
}
}
function Get-DefaultOuDefinitions {
$rootDn = "OU=$($script:EnterpriseRootOuName),$($script:DomainDn)"
@(
@{ Name = $script:EnterpriseRootOuName; ParentDn = $script:DomainDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Tier0'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Tier1'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Tier2'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Users'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Workstations'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Servers'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Groups'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'ServiceAccounts'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Admins'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Sites'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Staging'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Quarantine'; ParentDn = $rootDn; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Corporate'; ParentDn = "OU=Users,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Regional'; ParentDn = "OU=Users,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Contractors'; ParentDn = "OU=Users,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'PrivilegedExcluded'; ParentDn = "OU=Users,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Disabled'; ParentDn = "OU=Users,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'AMER'; ParentDn = "OU=Workstations,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'EMEA'; ParentDn = "OU=Workstations,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'APAC'; ParentDn = "OU=Workstations,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Kiosks'; ParentDn = "OU=Workstations,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'VDI'; ParentDn = "OU=Workstations,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Quarantine'; ParentDn = "OU=Workstations,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Infrastructure'; ParentDn = "OU=Servers,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Applications'; ParentDn = "OU=Servers,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Regional'; ParentDn = "OU=Servers,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'JumpHosts'; ParentDn = "OU=Servers,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'RoleBased'; ParentDn = "OU=Groups,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'ResourceBased'; ParentDn = "OU=Groups,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'MailEnabled'; ParentDn = "OU=Groups,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Delegation'; ParentDn = "OU=Groups,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Tier0'; ParentDn = "OU=ServiceAccounts,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Tier1'; ParentDn = "OU=ServiceAccounts,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Tier2'; ParentDn = "OU=ServiceAccounts,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Applications'; ParentDn = "OU=ServiceAccounts,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Tier0'; ParentDn = "OU=Admins,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Tier1'; ParentDn = "OU=Admins,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Tier2'; ParentDn = "OU=Admins,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'EmergencyAccess'; ParentDn = "OU=Admins,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'AMER'; ParentDn = "OU=Sites,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'EMEA'; ParentDn = "OU=Sites,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'APAC'; ParentDn = "OU=Sites,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'AdminGroups'; ParentDn = "OU=Tier0,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Delegation'; ParentDn = "OU=Tier0,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'PrivilegedRoles'; ParentDn = "OU=Tier0,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'AdminGroups'; ParentDn = "OU=Tier1,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Delegation'; ParentDn = "OU=Tier1,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'PrivilegedRoles'; ParentDn = "OU=Tier1,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'AdminGroups'; ParentDn = "OU=Tier2,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'Delegation'; ParentDn = "OU=Tier2,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
@{ Name = 'PrivilegedRoles'; ParentDn = "OU=Tier2,$rootDn"; ProtectedFromAccidentalDeletion = $true; Enabled = $true }
)
}
function Get-OuDefinitions {
if (-not [string]::IsNullOrWhiteSpace($script:OuTemplateCsvPath)) {
if (-not (Test-Path -LiteralPath $script:OuTemplateCsvPath)) {
throw "OU template CSV '$($script:OuTemplateCsvPath)' was not found."
}
return @(Import-Csv -LiteralPath $script:OuTemplateCsvPath | Where-Object {
$_.Enabled -match '^(?i:true|1|yes)$'
})
}
return @(Get-DefaultOuDefinitions)
}
function Ensure-OuHierarchy {
$definitions = @(Get-OuDefinitions)
foreach ($definition in $definitions) {
$protected = $true
if ($definition.PSObject.Properties.Name -contains 'ProtectedFromAccidentalDeletion') {
$protected = $definition.ProtectedFromAccidentalDeletion -match '^(?i:true|1|yes)$'
}
Ensure-OU -Name $definition.Name -Path $definition.ParentDn -ProtectedFromAccidentalDeletion $protected
}
}
Resolve-Inputs
Ensure-Module -Name ActiveDirectory
Ensure-Module -Name DhcpServer
Ensure-Module -Name DnsServer
$summary = [pscustomobject]@{
DomainFqdn = $script:DomainFqdn
DomainDn = $script:DomainDn
DcFqdn = $script:DcFqdn
DcIp = $script:DcIp
UpnSuffix = $script:UpnSuffix
DnsForwarders = ($script:DnsForwarders -join ', ')
ServerReverseNetworkId = $script:ServerReverseNetworkId
WorkstationReverseNetworkId = $script:WorkstationReverseNetworkId
DhcpScopeId = $script:DhcpScopeId
DhcpRouter = $script:DhcpRouter
EnterpriseRootOuName = $script:EnterpriseRootOuName
OuTemplateCsvPath = $(if ([string]::IsNullOrWhiteSpace($script:OuTemplateCsvPath)) { 'Built-in default hierarchy' } else { $script:OuTemplateCsvPath })
ValidateOnly = [bool]$ValidateOnly
}
$summary | Format-List | Out-String | Write-Output
if ($ValidateOnly) {
Write-Output 'Validation mode only. Post-promotion baseline was not applied.'
return
}
Ensure-UpnSuffix
Ensure-DnsForwarders
Ensure-DnsZone -NetworkId $script:ServerReverseNetworkId
Ensure-DnsZone -NetworkId $script:WorkstationReverseNetworkId
Ensure-DhcpAuthorization
Ensure-DhcpScope
Ensure-OuHierarchy
Write-Output 'Post-promotion baseline completed successfully.'
Configure-HQ-DC01-RecoveryTime.ps1
[CmdletBinding()]
param(
[string]$DomainFqdn = 'corp.gntech.me',
[string]$BackupDriveLetter = 'E',
[int]$BackupDiskNumber = 1,
[string[]]$NtpPeers = @('time.cloudflare.com,0x8','time.google.com,0x8'),
[switch]$StartSystemStateBackup,
[switch]$ValidateOnly
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Assert-Elevated {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this script from an elevated PowerShell session.'
}
}
function Get-RecoveryState {
$feature = Get-ADOptionalFeature -Identity 'Recycle Bin Feature'
$backupFeature = Get-WindowsFeature Windows-Server-Backup
$backupVolume = Get-Volume -ErrorAction Stop | Where-Object DriveLetter -eq $BackupDriveLetter
$backupDisk = Get-Disk -Number $BackupDiskNumber -ErrorAction SilentlyContinue
[pscustomobject]@{
Domain = (Get-ADDomain).DNSRoot
PdcEmulator = (Get-ADDomain).PDCEmulator
RecycleBinEnabled = ($feature.EnabledScopes.Count -gt 0)
BackupFeature = $backupFeature.InstallState
BackupDiskNumber = if ($backupDisk) { $backupDisk.Number } else { $null }
BackupDiskStyle = if ($backupDisk) { $backupDisk.PartitionStyle } else { 'Missing' }
BackupDiskIsBoot = if ($backupDisk) { $backupDisk.IsBoot } else { $null }
BackupDiskIsSystem = if ($backupDisk) { $backupDisk.IsSystem } else { $null }
BackupVolume = if ($backupVolume) { "$($backupVolume.DriveLetter):" } else { 'Missing' }
BackupVolumeLabel = if ($backupVolume) { $backupVolume.FileSystemLabel } else { '' }
TimeSource = ((w32tm /query /source) | Out-String).Trim()
W32TimeStatus = (Get-Service W32Time).Status
}
}
function Assert-ExistingBackupTargetSafe {
$volume = Get-Volume -ErrorAction Stop | Where-Object DriveLetter -eq $BackupDriveLetter
if (-not $volume) { return }
$partition = Get-Partition -DriveLetter $BackupDriveLetter -ErrorAction Stop
$disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop
if ($partition.DiskNumber -ne $BackupDiskNumber) {
throw "Drive ${BackupDriveLetter}: is on Disk $($partition.DiskNumber), not requested Disk $BackupDiskNumber."
}
if ($disk.IsBoot -or $disk.IsSystem) {
throw "Drive ${BackupDriveLetter}: is backed by a boot or system disk. Refusing to continue."
}
if ($volume.FileSystemLabel -ne 'DCBackup') {
throw "Drive ${BackupDriveLetter}: has label '$($volume.FileSystemLabel)', not 'DCBackup'. Refusing to continue."
}
}
Assert-Elevated
Import-Module ActiveDirectory
Import-Module ServerManager
$domain = Get-ADDomain
if ($domain.DNSRoot -ne $DomainFqdn) {
throw "Connected domain '$($domain.DNSRoot)' does not match requested domain '$DomainFqdn'."
}
if ($domain.PDCEmulator -ne "$env:COMPUTERNAME.$DomainFqdn") {
throw "Run the time configuration on the PDC Emulator '$($domain.PDCEmulator)'."
}
if ($BackupDriveLetter.Length -ne 1 -or $BackupDriveLetter -notmatch '^[A-Za-z]$') {
throw 'BackupDriveLetter must be one alphabetic character.'
}
Assert-ExistingBackupTargetSafe
Write-Output '=== Current State ==='
Get-RecoveryState | Format-List | Out-String | Write-Output
if ($ValidateOnly) {
Write-Output 'Validation mode only. No recovery or time configuration was changed.'
return
}
$recycleBin = Get-ADOptionalFeature -Identity 'Recycle Bin Feature'
if ($recycleBin.EnabledScopes.Count -eq 0) {
Enable-ADOptionalFeature -Identity 'Recycle Bin Feature' -Scope ForestOrConfigurationSet -Target $DomainFqdn -Confirm:$false
}
if (-not (Get-WindowsFeature Windows-Server-Backup).Installed) {
Install-WindowsFeature Windows-Server-Backup | Out-Null
}
$backupVolume = Get-Volume -ErrorAction Stop | Where-Object DriveLetter -eq $BackupDriveLetter
if (-not $backupVolume) {
$backupDisk = Get-Disk -Number $BackupDiskNumber -ErrorAction Stop
if ($backupDisk.IsBoot -or $backupDisk.IsSystem) {
throw "Disk $BackupDiskNumber is a boot or system disk. Refusing to modify it."
}
if ($backupDisk.PartitionStyle -ne 'RAW') {
throw "Disk $BackupDiskNumber is '$($backupDisk.PartitionStyle)', not RAW. Refusing to modify it."
}
$partition = $backupDisk |
Initialize-Disk -PartitionStyle GPT -PassThru |
New-Partition -DriveLetter $BackupDriveLetter -UseMaximumSize
Format-Volume -Partition $partition -FileSystem NTFS -NewFileSystemLabel 'DCBackup' -Confirm:$false | Out-Null
}
$peerList = $NtpPeers -join ' '
w32tm /config /manualpeerlist:"$peerList" /syncfromflags:manual /reliable:yes /update | Out-Null
Restart-Service W32Time -Force
Start-Sleep -Seconds 5
w32tm /resync /rediscover | Out-Null
if ($StartSystemStateBackup) {
wbadmin start systemstatebackup -backupTarget:"${BackupDriveLetter}:" -quiet
if ($LASTEXITCODE -ne 0) { throw "wbadmin failed with exit code $LASTEXITCODE." }
}
Write-Output '=== Result ==='
Get-RecoveryState | Format-List | Out-String | Write-Output
if ($StartSystemStateBackup) {
Write-Output '=== Backup Versions ==='
wbadmin get versions
}
Configure-HQ-ADSites.ps1
[CmdletBinding()]
param(
[ValidatePattern('^[A-Za-z0-9][A-Za-z0-9 _-]{0,63}$')]
[string]$SiteName = 'HQ',
[string]$Location = 'Headquarters',
[string[]]$Subnets = @(
'172.20.10.0/24',
'172.20.20.0/24',
'172.20.30.0/24',
'172.20.40.0/24',
'172.20.50.0/24',
'172.20.60.0/24',
'172.20.70.0/24',
'172.20.80.0/24',
'172.20.90.0/24',
'172.20.100.0/24',
'172.20.110.0/24'
),
[switch]$ValidateOnly
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Assert-Elevated {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this script from an elevated PowerShell session.'
}
}
function Get-SiteState {
Write-Output '=== Sites ==='
Get-ADReplicationSite -Filter * | Sort-Object Name | ForEach-Object {
Write-Output "Site: $($_.Name) / DN=$($_.DistinguishedName)"
}
Write-Output '=== Subnets ==='
Get-ADReplicationSubnet -Filter * | Sort-Object Name | ForEach-Object {
Write-Output "Subnet: $($_.Name) / Site=$($_.Site) / Location=$($_.Location)"
}
Write-Output '=== Local Site Discovery ==='
nltest /dsgetsite
}
Assert-Elevated
Import-Module ActiveDirectory
$domain = Get-ADDomain
$configDn = "CN=Configuration,$($domain.DistinguishedName)"
$defaultSiteDn = "CN=Default-First-Site-Name,CN=Sites,$configDn"
Get-SiteState
if ($ValidateOnly) {
Write-Output 'Validation mode only. No site or subnet configuration was changed.'
return
}
$site = Get-ADReplicationSite -Filter "Name -eq '$SiteName'" -ErrorAction SilentlyContinue
if (-not $site) {
if (Get-ADObject -Identity $defaultSiteDn -ErrorAction SilentlyContinue) {
Rename-ADObject -Identity $defaultSiteDn -NewName $SiteName
}
else {
New-ADReplicationSite -Name $SiteName -Description 'Primary headquarters site' | Out-Null
}
}
foreach ($subnetName in $Subnets) {
$subnet = Get-ADReplicationSubnet -Filter "Name -eq '$subnetName'" -ErrorAction SilentlyContinue
if ($subnet) {
Set-ADReplicationSubnet -Identity $subnet.DistinguishedName -Site $SiteName -Location $Location
}
else {
New-ADReplicationSubnet -Name $subnetName -Site $SiteName -Location $Location | Out-Null
}
}
Write-Output '=== Result ==='
Get-SiteState
$serverSearchBase = "CN=Servers,CN=$SiteName,CN=Sites,$configDn"
$server = Get-ADObject -LDAPFilter "(cn=$env:COMPUTERNAME)" -SearchBase $serverSearchBase -ErrorAction SilentlyContinue
if (-not $server) { throw "The server object for '$env:COMPUTERNAME' was not found beneath site '$SiteName'." }
Configure-HQ-DC01-UserGPOBaseline.ps1
param(
[string]$UsersOuDn,
[string]$UserBaselineGpoName,
[switch]$ValidateOnly
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Read-RequiredValue {
param(
[string]$Prompt,
[string]$DefaultValue
)
$fullPrompt = $Prompt
if ($DefaultValue) { $fullPrompt = "$Prompt [$DefaultValue]" }
while ($true) {
$value = Read-Host -Prompt $fullPrompt
if (-not [string]::IsNullOrWhiteSpace($value)) { return $value.Trim() }
if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) { return $DefaultValue }
}
}
function Resolve-Inputs {
$script:UsersOuDn = if ([string]::IsNullOrWhiteSpace($UsersOuDn)) { Read-RequiredValue 'Users OU DN' 'OU=Users,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $UsersOuDn.Trim() }
$script:UserBaselineGpoName = if ([string]::IsNullOrWhiteSpace($UserBaselineGpoName)) { Read-RequiredValue 'User baseline GPO name' 'GPO-Users-Baseline' } else { $UserBaselineGpoName.Trim() }
}
function Ensure-Module {
param([string]$Name)
Import-Module $Name
}
function Ensure-Gpo {
param(
[string]$Name,
[string]$Comment
)
$gpo = Get-GPO -Name $Name -ErrorAction SilentlyContinue
if (-not $gpo) {
$gpo = New-GPO -Name $Name -Comment $Comment
}
return $gpo
}
function Ensure-GpoLink {
param(
[string]$Name,
[string]$Target
)
$existingLink = @((Get-GPInheritance -Target $Target).GpoLinks | Where-Object DisplayName -eq $Name)
if ($existingLink.Count -eq 0) {
New-GPLink -Name $Name -Target $Target -LinkEnabled Yes | Out-Null
}
}
function Set-UserRegistryValue {
param(
[string]$Key,
[string]$ValueName,
[string]$Type,
[object]$Value
)
Set-GPRegistryValue `
-Name $script:UserBaselineGpoName `
-Key $Key `
-ValueName $ValueName `
-Type $Type `
-Value $Value | Out-Null
}
Resolve-Inputs
Ensure-Module -Name GroupPolicy
$summary = [pscustomobject]@{
UsersOuDn = $script:UsersOuDn
UserBaselineGpoName = $script:UserBaselineGpoName
ValidateOnly = [bool]$ValidateOnly
}
$summary | Format-List | Out-String | Write-Output
if ($ValidateOnly) {
Write-Output 'Validation mode only. User baseline GPO was not applied.'
return
}
Ensure-Gpo -Name $script:UserBaselineGpoName -Comment 'Baseline for standard user settings' | Out-Null
Ensure-GpoLink -Name $script:UserBaselineGpoName -Target $script:UsersOuDn
# Show file extensions in Explorer.
Set-UserRegistryValue `
-Key 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' `
-ValueName 'HideFileExt' `
-Type DWord `
-Value 0
# Do not hide hidden files for standard users yet; keep baseline low-risk.
# Remove the Windows consumer "News and interests" style taskbar feed.
Set-UserRegistryValue `
-Key 'HKCU\Software\Microsoft\Windows\CurrentVersion\Feeds' `
-ValueName 'ShellFeedsTaskbarViewMode' `
-Type DWord `
-Value 2
# Block Control Panel and Settings access as a visible, user-scoped baseline.
Set-UserRegistryValue `
-Key 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer' `
-ValueName 'NoControlPanel' `
-Type DWord `
-Value 1
Write-Output 'User baseline GPO completed successfully.'
Configure-HQ-WorkstationHardeningGPO.ps1
[CmdletBinding()]
param(
[string]$WorkstationsOuDn = 'OU=Workstations,OU=GNTECH,DC=corp,DC=gntech,DC=me',
[string]$GpoName = 'GPO-Workstations-Hardening',
[string]$TranscriptionPath = 'C:\ProgramData\PSLogs',
[switch]$ValidateOnly
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Assert-Elevated {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this script from an elevated PowerShell session.'
}
}
function Ensure-GpoLink {
$links = @((Get-GPInheritance -Target $WorkstationsOuDn).GpoLinks | Where-Object DisplayName -eq $GpoName)
if ($links.Count -eq 0) {
New-GPLink -Name $GpoName -Target $WorkstationsOuDn -LinkEnabled Yes | Out-Null
return 'Created'
}
if (-not $links[0].Enabled) {
Set-GPLink -Name $GpoName -Target $WorkstationsOuDn -LinkEnabled Yes | Out-Null
return 'Enabled'
}
return 'Compliant'
}
Assert-Elevated
Import-Module ActiveDirectory
Import-Module GroupPolicy
Get-ADOrganizationalUnit -Identity $WorkstationsOuDn -ErrorAction Stop | Out-Null
$settings = @(
@{ Key='HKLM\Software\Policies\Microsoft\Windows NT\DNSClient'; Name='EnableMulticast'; Type='DWord'; Value=0 },
@{ Key='HKLM\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging'; Name='EnableModuleLogging'; Type='DWord'; Value=1 },
@{ Key='HKLM\Software\Policies\Microsoft\Windows\PowerShell\Transcription'; Name='EnableTranscripting'; Type='DWord'; Value=1 },
@{ Key='HKLM\Software\Policies\Microsoft\Windows\PowerShell\Transcription'; Name='EnableInvocationHeader'; Type='DWord'; Value=1 },
@{ Key='HKLM\Software\Policies\Microsoft\Windows\PowerShell\Transcription'; Name='OutputDirectory'; Type='String'; Value=$TranscriptionPath },
@{ Key='HKLM\Software\Policies\Microsoft\Windows\System'; Name='EnableSmartScreen'; Type='DWord'; Value=1 },
@{ Key='HKLM\Software\Policies\Microsoft\Windows\System'; Name='ShellSmartScreenLevel'; Type='String'; Value='Warn' },
@{ Key='HKLM\Software\Policies\Microsoft\Windows Defender'; Name='DisableAntiSpyware'; Type='DWord'; Value=0 },
@{ Key='HKLM\Software\Policies\Microsoft\Windows Defender\Real-Time Protection'; Name='DisableRealtimeMonitoring'; Type='DWord'; Value=0 },
@{ Key='HKLM\Software\Policies\Microsoft\Windows Defender\Spynet'; Name='SpynetReporting'; Type='DWord'; Value=2 },
@{ Key='HKLM\Software\Policies\Microsoft\Windows Defender\Spynet'; Name='SubmitSamplesConsent'; Type='DWord'; Value=1 }
)
$gpo = Get-GPO -Name $GpoName -ErrorAction SilentlyContinue
if (-not $gpo -and $ValidateOnly) {
Write-Output "GPO: Missing / Name=$GpoName"
Write-Output 'Validation mode only. No GPO changes were applied.'
return
}
if (-not $gpo) {
$gpo = New-GPO -Name $GpoName -Comment 'Validated workstation hardening controls'
}
if ($ValidateOnly) {
$link = @((Get-GPInheritance -Target $WorkstationsOuDn).GpoLinks | Where-Object DisplayName -eq $GpoName)
Write-Output "GPO: Present / Id=$($gpo.Id)"
Write-Output "Link: $(if ($link.Count -gt 0 -and $link[0].Enabled) { 'Enabled' } else { 'MissingOrDisabled' })"
foreach ($setting in $settings) {
$current = Get-GPRegistryValue -Name $GpoName -Key $setting.Key -ValueName $setting.Name -ErrorAction SilentlyContinue
[pscustomobject]@{
Key = $setting.Key
ValueName = $setting.Name
Expected = $setting.Value
Current = if ($current) { $current.Value } else { $null }
Compliant = [bool]($current -and $current.Value -eq $setting.Value)
}
}
Write-Output 'Validation mode only. No GPO changes were applied.'
return
}
$linkAction = Ensure-GpoLink
foreach ($setting in $settings) {
Set-GPRegistryValue -Name $GpoName -Key $setting.Key -ValueName $setting.Name -Type $setting.Type -Value $setting.Value | Out-Null
}
Write-Output "GPO: $GpoName"
Write-Output "Link: $linkAction"
Write-Output "SettingsApplied: $($settings.Count)"
Write-Output 'Workstation hardening GPO completed successfully.'
Configure-HQ-FS01-Shares.ps1
param(
[string]$ShareRoot,
[string]$DomainNetbios,
[switch]$ValidateOnly
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Assert-Elevated {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this script from an elevated PowerShell session.'
}
}
function Read-RequiredValue {
param([string]$Prompt,[string]$DefaultValue)
$fullPrompt = $Prompt
if ($DefaultValue) { $fullPrompt = "$Prompt [$DefaultValue]" }
while ($true) {
$value = Read-Host -Prompt $fullPrompt
if (-not [string]::IsNullOrWhiteSpace($value)) { return $value.Trim() }
if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) { return $DefaultValue }
}
}
function Resolve-Inputs {
$script:ShareRoot = if ([string]::IsNullOrWhiteSpace($ShareRoot)) { Read-RequiredValue 'Share root path' 'C:\Shares' } else { $ShareRoot.Trim() }
$script:DomainNetbios = if ([string]::IsNullOrWhiteSpace($DomainNetbios)) { Read-RequiredValue 'Domain NetBIOS name' 'GNTECH' } else { $DomainNetbios.Trim() }
}
function Ensure-Directory {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path)) {
if (-not $ValidateOnly) { New-Item -Path $Path -ItemType Directory -Force | Out-Null }
if ($ValidateOnly) { return 'Planned' }
return 'Created'
}
'Exists'
}
function Set-FolderAcl {
param([string]$Path,[array]$Rules)
$acl = New-Object System.Security.AccessControl.DirectorySecurity
$acl.SetAccessRuleProtection($true, $false)
$inheritanceFlags = [System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'
$propagationFlags = [System.Security.AccessControl.PropagationFlags]::None
foreach ($rule in $Rules) {
$fsRights = [System.Security.AccessControl.FileSystemRights]$rule.Rights
$accessType = [System.Security.AccessControl.AccessControlType]::Allow
$aclRule = New-Object System.Security.AccessControl.FileSystemAccessRule($rule.Identity,$fsRights,$inheritanceFlags,$propagationFlags,$accessType)
$acl.AddAccessRule($aclRule) | Out-Null
}
if (-not $ValidateOnly) { Set-Acl -Path $Path -AclObject $acl }
if ($ValidateOnly) { 'Planned' } else { 'Applied' }
}
function Ensure-SmbShareState {
param([string]$Name,[string]$Path,[string]$Description,[string[]]$FullAccess,[string[]]$ChangeAccess,[string[]]$ReadAccess,[string]$FolderEnumerationMode = 'AccessBased')
$existing = Get-SmbShare -Name $Name -ErrorAction SilentlyContinue
if ($existing -and $existing.Path -ne $Path) {
throw "SMB share '$Name' exists at '$($existing.Path)' instead of '$Path'. Correct it manually before rerunning."
}
if (-not $ValidateOnly) {
if (-not $existing) {
$splat = @{ Name = $Name; Path = $Path; Description = $Description; FolderEnumerationMode = $FolderEnumerationMode }
if ($FullAccess) { $splat.FullAccess = $FullAccess }
if ($ChangeAccess) { $splat.ChangeAccess = $ChangeAccess }
if ($ReadAccess) { $splat.ReadAccess = $ReadAccess }
New-SmbShare @splat | Out-Null
}
else {
Set-SmbShare -Name $Name -Description $Description -FolderEnumerationMode $FolderEnumerationMode -Force | Out-Null
$desiredAccess = @()
$desiredAccess += @($FullAccess | ForEach-Object { @{ Account = $_; Right = 'Full' } })
$desiredAccess += @($ChangeAccess | ForEach-Object { @{ Account = $_; Right = 'Change' } })
$desiredAccess += @($ReadAccess | ForEach-Object { @{ Account = $_; Right = 'Read' } })
foreach ($entry in $desiredAccess) {
Grant-SmbShareAccess -Name $Name -AccountName $entry.Account -AccessRight $entry.Right -Force | Out-Null
}
}
}
if ($ValidateOnly) { if ($existing) { 'Existing path validated' } else { 'Planned' } }
elseif ($existing) { 'Reconciled' } else { 'Created' }
}
function Assert-AccountResolvable {
param([string[]]$Identity)
foreach ($item in $Identity) {
try { ([System.Security.Principal.NTAccount]$item).Translate([System.Security.Principal.SecurityIdentifier]) | Out-Null }
catch { throw "Required account or group '$item' cannot be resolved on this server." }
}
}
Resolve-Inputs
Assert-Elevated
$departmentsRoot = Join-Path $script:ShareRoot 'Departments'
$publicRoot = Join-Path $script:ShareRoot 'Public'
$backupsRoot = Join-Path $script:ShareRoot 'Backups'
$itRoot = Join-Path $departmentsRoot 'IT'
$financeRoot = Join-Path $departmentsRoot 'Finance'
$domainUsers = "$($script:DomainNetbios)\Domain Users"
$domainAdmins = "$($script:DomainNetbios)\Domain Admins"
$builtinAdmins = 'BUILTIN\Administrators'
$systemAccount = 'NT AUTHORITY\SYSTEM'
Assert-AccountResolvable -Identity @(
$domainUsers,$domainAdmins,$builtinAdmins,$systemAccount,
"$($script:DomainNetbios)\DL-HQ-FS01-Departments-IT-RW",
"$($script:DomainNetbios)\DL-HQ-FS01-Departments-Finance-RW",
"$($script:DomainNetbios)\DL-HQ-FS01-Public-RW",
"$($script:DomainNetbios)\DL-HQ-FS01-Backups-RW"
)
$feature = Get-WindowsFeature FS-FileServer
if (-not $feature.Installed -and -not $ValidateOnly) { Install-WindowsFeature FS-FileServer | Out-Null }
Write-Output "FS-FileServer: $(if ($feature.Installed) { 'Installed' } elseif ($ValidateOnly) { 'Missing' } else { 'Installed now' })"
$folders = @($script:ShareRoot,$departmentsRoot,$publicRoot,$backupsRoot,$itRoot,$financeRoot)
$folderResults = foreach ($folder in $folders) {
[pscustomobject]@{ Path = $folder; Action = (Ensure-Directory -Path $folder) }
}
$folderResults | Format-Table -AutoSize | Out-String | Write-Output
Set-FolderAcl -Path $departmentsRoot -Rules @(
@{ Identity = $systemAccount; Rights = 'FullControl' },
@{ Identity = $builtinAdmins; Rights = 'FullControl' },
@{ Identity = $domainAdmins; Rights = 'FullControl' },
@{ Identity = $domainUsers; Rights = 'ReadAndExecute, Synchronize' }
)
Set-FolderAcl -Path $itRoot -Rules @(
@{ Identity = $systemAccount; Rights = 'FullControl' },
@{ Identity = $builtinAdmins; Rights = 'FullControl' },
@{ Identity = $domainAdmins; Rights = 'FullControl' },
@{ Identity = "$($script:DomainNetbios)\DL-HQ-FS01-Departments-IT-RW"; Rights = 'Modify, Synchronize' }
)
Set-FolderAcl -Path $financeRoot -Rules @(
@{ Identity = $systemAccount; Rights = 'FullControl' },
@{ Identity = $builtinAdmins; Rights = 'FullControl' },
@{ Identity = $domainAdmins; Rights = 'FullControl' },
@{ Identity = "$($script:DomainNetbios)\DL-HQ-FS01-Departments-Finance-RW"; Rights = 'Modify, Synchronize' }
)
Set-FolderAcl -Path $publicRoot -Rules @(
@{ Identity = $systemAccount; Rights = 'FullControl' },
@{ Identity = $builtinAdmins; Rights = 'FullControl' },
@{ Identity = $domainAdmins; Rights = 'FullControl' },
@{ Identity = $domainUsers; Rights = 'ReadAndExecute, Synchronize' },
@{ Identity = "$($script:DomainNetbios)\DL-HQ-FS01-Public-RW"; Rights = 'Modify, Synchronize' }
)
Set-FolderAcl -Path $backupsRoot -Rules @(
@{ Identity = $systemAccount; Rights = 'FullControl' },
@{ Identity = $builtinAdmins; Rights = 'FullControl' },
@{ Identity = $domainAdmins; Rights = 'FullControl' },
@{ Identity = "$($script:DomainNetbios)\DL-HQ-FS01-Backups-RW"; Rights = 'Modify, Synchronize' }
)
$shareResults = @(
[pscustomobject]@{
Name = 'Departments'
Action = (Ensure-SmbShareState -Name 'Departments' -Path $departmentsRoot -Description 'Departmental data root' -FullAccess @($domainAdmins) -ReadAccess @($domainUsers))
},
[pscustomobject]@{
Name = 'Public'
Action = (Ensure-SmbShareState -Name 'Public' -Path $publicRoot -Description 'Broad collaboration share' -FullAccess @($domainAdmins) -ChangeAccess @("$($script:DomainNetbios)\DL-HQ-FS01-Public-RW") -ReadAccess @($domainUsers))
},
[pscustomobject]@{
Name = 'Backups'
Action = (Ensure-SmbShareState -Name 'Backups' -Path $backupsRoot -Description 'Backup staging share' -FullAccess @($domainAdmins) -ChangeAccess @("$($script:DomainNetbios)\DL-HQ-FS01-Backups-RW"))
}
)
$shareResults | Format-Table -AutoSize | Out-String | Write-Output
if ($ValidateOnly) {
Write-Output 'Validation mode only. HQ-FS01 share configuration was not applied.'
} else {
Write-Output 'HQ-FS01 share configuration completed successfully.'
}
Create-HQ-FS01-Groups.ps1
param(
[string]$GroupsOuDn,
[switch]$ValidateOnly
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Read-RequiredValue {
param(
[string]$Prompt,
[string]$DefaultValue
)
$fullPrompt = $Prompt
if ($DefaultValue) { $fullPrompt = "$Prompt [$DefaultValue]" }
while ($true) {
$value = Read-Host -Prompt $fullPrompt
if (-not [string]::IsNullOrWhiteSpace($value)) { return $value.Trim() }
if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) { return $DefaultValue }
}
}
function Resolve-Inputs {
$script:GroupsOuDn = if ([string]::IsNullOrWhiteSpace($GroupsOuDn)) {
Read-RequiredValue 'Groups OU DN' 'OU=Groups,OU=GNTECH,DC=corp,DC=gntech,DC=me'
} else {
$GroupsOuDn.Trim()
}
}
function Ensure-Module {
param([string]$Name)
Import-Module $Name
}
function Ensure-Group {
param(
[string]$Name,
[string]$Description
)
$existing = Get-ADGroup -LDAPFilter "(cn=$Name)" -SearchBase $script:GroupsOuDn -ErrorAction SilentlyContinue
if ($existing) {
return [pscustomobject]@{ Name = $Name; Action = 'Exists' }
}
if (-not $ValidateOnly) {
New-ADGroup `
-Name $Name `
-SamAccountName $Name `
-GroupScope DomainLocal `
-GroupCategory Security `
-Path $script:GroupsOuDn `
-Description $Description | Out-Null
}
[pscustomobject]@{ Name = $Name; Action = $(if ($ValidateOnly) { 'Planned' } else { 'Created' }) }
}
Resolve-Inputs
Ensure-Module -Name ActiveDirectory
$definitions = @(
@{ Name = 'DL-HQ-FS01-Public-RW'; Description = 'Modify access to HQ-FS01 Public share' },
@{ Name = 'DL-HQ-FS01-Departments-IT-RW'; Description = 'Modify access to HQ-FS01 Departments IT folder' },
@{ Name = 'DL-HQ-FS01-Departments-Finance-RW'; Description = 'Modify access to HQ-FS01 Departments Finance folder' },
@{ Name = 'DL-HQ-FS01-Backups-RW'; Description = 'Modify access to HQ-FS01 Backups share' }
)
$summary = $definitions | ForEach-Object {
[pscustomobject]@{
Name = $_.Name
Description = $_.Description
Path = $script:GroupsOuDn
}
}
$summary | Format-Table -AutoSize | Out-String | Write-Output
$results = foreach ($definition in $definitions) {
Ensure-Group -Name $definition.Name -Description $definition.Description
}
$results | Format-Table -AutoSize | Out-String | Write-Output
if ($ValidateOnly) {
Write-Output 'Validation mode only. HQ-FS01 groups were not created.'
} else {
Write-Output 'HQ-FS01 groups completed successfully.'
}
Create-HQ-ServiceAccounts.ps1
param(
[string]$DomainDn,
[string]$ServiceAccountsOuDn,
[string]$ServiceUpnSuffix,
[hashtable]$AccountPasswords,
[switch]$PasswordNeverExpires,
[switch]$ValidateOnly
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Assert-Elevated {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this script from an elevated PowerShell session.'
}
}
function Read-RequiredValue {
param(
[string]$Prompt,
[string]$DefaultValue
)
$fullPrompt = $Prompt
if ($DefaultValue) {
$fullPrompt = "$Prompt [$DefaultValue]"
}
while ($true) {
$value = Read-Host -Prompt $fullPrompt
if (-not [string]::IsNullOrWhiteSpace($value)) {
return $value.Trim()
}
if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) {
return $DefaultValue
}
}
}
function Read-RequiredSecret {
param([string]$Prompt)
try {
return Read-Host -Prompt $Prompt -AsSecureString
}
catch {
throw "A secure value for '$Prompt' is required."
}
}
function Resolve-Inputs {
$script:DomainDn = if ([string]::IsNullOrWhiteSpace($DomainDn)) {
Read-RequiredValue -Prompt 'Domain DN' -DefaultValue 'DC=corp,DC=gntech,DC=me'
}
else {
$DomainDn.Trim()
}
$script:ServiceAccountsOuDn = if ([string]::IsNullOrWhiteSpace($ServiceAccountsOuDn)) {
Read-RequiredValue -Prompt 'Service accounts OU DN' -DefaultValue 'OU=Tier1,OU=ServiceAccounts,OU=GNTECH,DC=corp,DC=gntech,DC=me'
}
else {
$ServiceAccountsOuDn.Trim()
}
$script:ServiceUpnSuffix = if ([string]::IsNullOrWhiteSpace($ServiceUpnSuffix)) {
''
}
else {
$ServiceUpnSuffix.Trim()
}
}
function Ensure-Module {
param([string]$Name)
Import-Module $Name
}
function Assert-AdTargets {
$domain = Get-ADDomain -ErrorAction Stop
if ($domain.DistinguishedName -ne $script:DomainDn) {
throw "Connected domain '$($domain.DistinguishedName)' does not match requested domain '$($script:DomainDn)'."
}
Get-ADOrganizationalUnit -Identity $script:ServiceAccountsOuDn -ErrorAction Stop | Out-Null
}
function Get-ServiceAccountUpn {
param([string]$SamAccountName)
if ([string]::IsNullOrWhiteSpace($script:ServiceUpnSuffix)) {
return $null
}
return "$SamAccountName@$($script:ServiceUpnSuffix)"
}
function Ensure-ServiceAccount {
param(
[hashtable]$Definition,
[securestring]$Password
)
$existing = Get-ADUser -Filter "SamAccountName -eq '$($Definition.SamAccountName)'" -Properties UserPrincipalName,PasswordNeverExpires,Description -ErrorAction SilentlyContinue
if ($existing) {
if ($existing.DistinguishedName -notlike "*,$($script:ServiceAccountsOuDn)") {
throw "Account '$($Definition.SamAccountName)' exists outside the target OU: $($existing.DistinguishedName)"
}
$changes = @{}
$expectedUpn = Get-ServiceAccountUpn -SamAccountName $Definition.SamAccountName
if ($expectedUpn -and $existing.UserPrincipalName -ne $expectedUpn) { $changes.UserPrincipalName = $expectedUpn }
if ($existing.Description -ne $Definition.Description) { $changes.Description = $Definition.Description }
if ($PasswordNeverExpires -and -not $existing.PasswordNeverExpires) { $changes.PasswordNeverExpires = $true }
if ($changes.Count -gt 0 -and -not $ValidateOnly) { Set-ADUser -Identity $existing @changes }
return [pscustomobject]@{
SamAccountName = $Definition.SamAccountName
Action = if ($changes.Count -eq 0) { 'Compliant' } elseif ($ValidateOnly) { 'Drift' } else { 'Updated' }
Details = if ($changes.Count -eq 0) { '' } else { ($changes.Keys -join ',') }
}
}
if ($ValidateOnly) {
return [pscustomobject]@{
SamAccountName = $Definition.SamAccountName
Action = 'Missing'
Details = ''
}
}
$parameters = @{
Name = $Definition.Name
DisplayName = $Definition.DisplayName
SamAccountName = $Definition.SamAccountName
Path = $script:ServiceAccountsOuDn
AccountPassword = $Password
Enabled = $true
ChangePasswordAtLogon = $false
Description = $Definition.Description
}
$upn = Get-ServiceAccountUpn -SamAccountName $Definition.SamAccountName
if ($upn) {
$parameters['UserPrincipalName'] = $upn
}
if ($PasswordNeverExpires) {
$parameters['PasswordNeverExpires'] = $true
}
New-ADUser @parameters
return [pscustomobject]@{
SamAccountName = $Definition.SamAccountName
Action = 'Created'
Details = ''
}
}
Resolve-Inputs
Assert-Elevated
Ensure-Module -Name ActiveDirectory
Assert-AdTargets
$definitions = @(
@{
Name = 'svc.join.hq'
DisplayName = 'svc.join.hq'
SamAccountName = 'svc.join.hq'
Description = 'Delegated domain join service account for workstation and server onboarding.'
},
@{
Name = 'svc.backup.hq'
DisplayName = 'svc.backup.hq'
SamAccountName = 'svc.backup.hq'
Description = 'Backup service account for approved backup tooling and targets.'
},
@{
Name = 'svc.monitor.hq'
DisplayName = 'svc.monitor.hq'
SamAccountName = 'svc.monitor.hq'
Description = 'Monitoring and inventory service account for approved observability tooling.'
},
@{
Name = 'svc.deploy.hq'
DisplayName = 'svc.deploy.hq'
SamAccountName = 'svc.deploy.hq'
Description = 'Deployment automation service account for scripted server and workstation configuration.'
}
)
$summary = $definitions | ForEach-Object {
[pscustomobject]@{
Name = $_.Name
SamAccountName = $_.SamAccountName
UserPrincipalName = Get-ServiceAccountUpn -SamAccountName $_.SamAccountName
Path = $script:ServiceAccountsOuDn
Description = $_.Description
}
}
$summary | Format-Table -AutoSize | Out-String | Write-Output
if ($ValidateOnly) {
$results = foreach ($definition in $definitions) {
Ensure-ServiceAccount -Definition $definition -Password $null
}
$results | Format-Table -AutoSize | Out-String | Write-Output
Write-Output 'Validation mode only. Service accounts were not created or changed.'
return
}
$passwords = @{}
foreach ($definition in $definitions) {
$existing = Get-ADUser -Filter "SamAccountName -eq '$($definition.SamAccountName)'" -ErrorAction SilentlyContinue
if (-not $existing) {
if ($AccountPasswords -and $AccountPasswords.ContainsKey($definition.SamAccountName)) {
$providedPassword = $AccountPasswords[$definition.SamAccountName]
if ($providedPassword -isnot [securestring]) {
throw "AccountPasswords entry for '$($definition.SamAccountName)' must be a SecureString."
}
$passwords[$definition.SamAccountName] = $providedPassword
}
else {
$passwords[$definition.SamAccountName] = Read-RequiredSecret -Prompt "Password for $($definition.SamAccountName)"
}
}
}
$results = foreach ($definition in $definitions) {
Ensure-ServiceAccount -Definition $definition -Password $passwords[$definition.SamAccountName]
}
$results | Format-Table -AutoSize | Out-String | Write-Output
Write-Output 'Service account creation completed successfully.'
Create-HQ-TestUsers.ps1
param(
[string]$DomainDn,
[string]$UsersOuDn,
[string]$AdminsOuDn,
[string]$UserUpnSuffix,
[string]$AdminUpnSuffix,
[string]$PublicWriteGroupSamAccountName,
[switch]$ValidateOnly
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Read-RequiredValue {
param(
[string]$Prompt,
[string]$DefaultValue
)
$fullPrompt = $Prompt
if ($DefaultValue) { $fullPrompt = "$Prompt [$DefaultValue]" }
while ($true) {
$value = Read-Host -Prompt $fullPrompt
if (-not [string]::IsNullOrWhiteSpace($value)) { return $value.Trim() }
if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) { return $DefaultValue }
}
}
function Read-RequiredSecret {
param([string]$Prompt)
try {
return Read-Host -Prompt $Prompt -AsSecureString
}
catch {
throw "A secure value for '$Prompt' is required when running non-interactively."
}
}
function Resolve-Inputs {
$script:DomainDn = if ([string]::IsNullOrWhiteSpace($DomainDn)) { Read-RequiredValue 'Domain DN' 'DC=corp,DC=gntech,DC=me' } else { $DomainDn.Trim() }
$script:UsersOuDn = if ([string]::IsNullOrWhiteSpace($UsersOuDn)) { Read-RequiredValue 'Users OU DN' 'OU=Users,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $UsersOuDn.Trim() }
$script:AdminsOuDn = if ([string]::IsNullOrWhiteSpace($AdminsOuDn)) { Read-RequiredValue 'Admins OU DN' 'OU=Admins,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $AdminsOuDn.Trim() }
$script:UserUpnSuffix = if ([string]::IsNullOrWhiteSpace($UserUpnSuffix)) { Read-RequiredValue 'Standard user UPN suffix' 'gntech.me' } else { $UserUpnSuffix.Trim() }
$script:AdminUpnSuffix = if ([string]::IsNullOrWhiteSpace($AdminUpnSuffix)) { Read-RequiredValue 'Admin user UPN suffix' 'gntech.me' } else { $AdminUpnSuffix.Trim() }
$script:PublicWriteGroupSamAccountName = if ([string]::IsNullOrWhiteSpace($PublicWriteGroupSamAccountName)) { Read-RequiredValue 'Public write group SamAccountName' 'DL-HQ-FS01-Public-RW' } else { $PublicWriteGroupSamAccountName.Trim() }
if (-not $ValidateOnly) {
$script:DefaultUserPassword = Read-RequiredSecret -Prompt 'Standard user password'
$script:DefaultAdminPassword = Read-RequiredSecret -Prompt 'Admin user password'
}
}
function Ensure-Module {
param([string]$Name)
Import-Module $Name
}
function Ensure-TestUser {
param(
[string]$Name,
[string]$GivenName,
[string]$Surname,
[string]$DisplayName,
[string]$UserPrincipalName,
[string]$SamAccountName,
[string]$EmployeeId,
[string]$Path,
[securestring]$Password
)
$existing = Get-ADUser -Filter "SamAccountName -eq '$SamAccountName'" -ErrorAction SilentlyContinue
if (-not $existing) {
New-ADUser `
-Name $Name `
-GivenName $GivenName `
-Surname $Surname `
-DisplayName $DisplayName `
-UserPrincipalName $UserPrincipalName `
-SamAccountName $SamAccountName `
-EmployeeID $EmployeeId `
-Path $Path `
-AccountPassword $Password `
-Enabled $true `
-ChangePasswordAtLogon $false
}
}
function Ensure-GroupMembership {
param(
[string]$GroupSamAccountName,
[string]$MemberSamAccountName
)
$group = Get-ADGroup -Filter "SamAccountName -eq '$GroupSamAccountName'" -ErrorAction SilentlyContinue
if (-not $group) {
throw "The group '$GroupSamAccountName' was not found."
}
$member = Get-ADUser -Filter "SamAccountName -eq '$MemberSamAccountName'" -ErrorAction SilentlyContinue
if (-not $member) {
throw "The user '$MemberSamAccountName' was not found."
}
$isMember = Get-ADGroupMember -Identity $group.DistinguishedName -Recursive | Where-Object SamAccountName -eq $MemberSamAccountName
if ($isMember) {
return [pscustomobject]@{
GroupSamAccountName = $GroupSamAccountName
MemberSamAccountName = $MemberSamAccountName
Action = 'Exists'
}
}
if (-not $ValidateOnly) {
Add-ADGroupMember -Identity $group.DistinguishedName -Members $member.DistinguishedName
}
[pscustomobject]@{
GroupSamAccountName = $GroupSamAccountName
MemberSamAccountName = $MemberSamAccountName
Action = $(if ($ValidateOnly) { 'Planned' } else { 'Added' })
}
}
Resolve-Inputs
Ensure-Module -Name ActiveDirectory
$definitions = @(
@{
Name = 'Miguel Perez'
GivenName = 'Miguel'
Surname = 'Perez'
DisplayName = 'Miguel Perez'
UserPrincipalName = "miguel.perez@$($script:UserUpnSuffix)"
SamAccountName = 'miguel.perez'
EmployeeId = 'GNT-000123'
Path = $script:UsersOuDn
IsAdmin = $false
},
@{
Name = 'Ana Garcia'
GivenName = 'Ana'
Surname = 'Garcia'
DisplayName = 'Ana Garcia'
UserPrincipalName = "ana.garcia@$($script:UserUpnSuffix)"
SamAccountName = 'ana.garcia'
EmployeeId = 'GNT-000124'
Path = $script:UsersOuDn
IsAdmin = $false
},
@{
Name = 'Admin Miguel Perez'
GivenName = 'Miguel'
Surname = 'Perez'
DisplayName = 'Admin Miguel Perez'
UserPrincipalName = "adm.miguel.perez@$($script:AdminUpnSuffix)"
SamAccountName = 'adm.miguel.perez'
EmployeeId = 'GNT-A000123'
Path = $script:AdminsOuDn
IsAdmin = $true
},
@{
Name = 'Admin Gerlin Nolasco'
GivenName = 'Gerlin'
Surname = 'Nolasco'
DisplayName = 'Admin Gerlin Nolasco'
UserPrincipalName = "adm.gerlin.nolasco@$($script:AdminUpnSuffix)"
SamAccountName = 'adm.gerlin.nolasco'
EmployeeId = 'GNT-A000124'
Path = $script:AdminsOuDn
IsAdmin = $true
}
)
$summary = $definitions | ForEach-Object {
[pscustomobject]@{
Name = $_.Name
UserPrincipalName = $_.UserPrincipalName
SamAccountName = $_.SamAccountName
EmployeeId = $_.EmployeeId
Path = $_.Path
}
}
$summary | Format-Table -AutoSize | Out-String | Write-Output
[pscustomobject]@{
PublicWriteGroupSamAccountName = $script:PublicWriteGroupSamAccountName
PublicWriteMemberSamAccountName = 'miguel.perez'
PublicDeniedMemberSamAccountName = 'ana.garcia'
} | Format-List | Out-String | Write-Output
if ($ValidateOnly) {
Write-Output 'Validation mode only. Test users and group memberships were not created.'
return
}
foreach ($definition in $definitions) {
$password = if ($definition.IsAdmin) { $script:DefaultAdminPassword } else { $script:DefaultUserPassword }
Ensure-TestUser `
-Name $definition.Name `
-GivenName $definition.GivenName `
-Surname $definition.Surname `
-DisplayName $definition.DisplayName `
-UserPrincipalName $definition.UserPrincipalName `
-SamAccountName $definition.SamAccountName `
-EmployeeId $definition.EmployeeId `
-Path $definition.Path `
-Password $password
}
$membershipResult = Ensure-GroupMembership `
-GroupSamAccountName $script:PublicWriteGroupSamAccountName `
-MemberSamAccountName 'miguel.perez'
$membershipResult | Format-Table -AutoSize | Out-String | Write-Output
Write-Output 'Test users completed successfully.'
Deploy-HQ-DC01-Stage1.ps1
param(
[string]$TargetName,
[string]$TimeZone,
[string]$IPv4,
[Nullable[int]]$PrefixLength,
[string]$Gateway,
[string[]]$DnsServers,
[switch]$RestartIfNeeded
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Read-RequiredValue {
param(
[string]$Prompt,
[string]$DefaultValue
)
$fullPrompt = $Prompt
if ($DefaultValue) {
$fullPrompt = "$Prompt [$DefaultValue]"
}
while ($true) {
$value = Read-Host -Prompt $fullPrompt
if (-not [string]::IsNullOrWhiteSpace($value)) {
return $value.Trim()
}
if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) {
return $DefaultValue
}
}
}
function Read-RequiredInt {
param(
[string]$Prompt,
[int]$DefaultValue
)
while ($true) {
$value = Read-RequiredValue -Prompt $Prompt -DefaultValue $DefaultValue
$parsed = 0
if ([int]::TryParse($value, [ref]$parsed)) {
return $parsed
}
}
}
function Read-DnsServerList {
param(
[string[]]$DefaultServers
)
while ($true) {
$defaultValue = $DefaultServers -join ','
$value = Read-RequiredValue -Prompt 'DNS servers (comma-separated)' -DefaultValue $defaultValue
$servers = @(
$value.Split(',') |
ForEach-Object { $_.Trim() } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
if ($servers.Count -gt 0) {
return $servers
}
}
}
function Resolve-DeploymentInputs {
$script:TargetName = if ([string]::IsNullOrWhiteSpace($TargetName)) {
Read-RequiredValue -Prompt 'Target hostname' -DefaultValue 'HQ-DC01'
} else {
$TargetName.Trim()
}
$script:TimeZone = if ([string]::IsNullOrWhiteSpace($TimeZone)) {
Read-RequiredValue -Prompt 'Windows time zone ID' -DefaultValue 'SA Western Standard Time'
} else {
$TimeZone.Trim()
}
$script:IPv4 = if ([string]::IsNullOrWhiteSpace($IPv4)) {
Read-RequiredValue -Prompt 'IPv4 address' -DefaultValue '172.20.20.11'
} else {
$IPv4.Trim()
}
$script:PrefixLength = if ($null -eq $PrefixLength) {
Read-RequiredInt -Prompt 'IPv4 prefix length' -DefaultValue 24
} else {
[int]$PrefixLength
}
$script:Gateway = if ([string]::IsNullOrWhiteSpace($Gateway)) {
Read-RequiredValue -Prompt 'Default gateway' -DefaultValue '172.20.20.1'
} else {
$Gateway.Trim()
}
$script:DnsServers = if ($DnsServers.Count -eq 0) {
Read-DnsServerList -DefaultServers @('127.0.0.1', '1.1.1.1')
} else {
@(
$DnsServers |
ForEach-Object { $_.Trim() } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
}
}
function Get-PrimaryAdapter {
$adapter = Get-NetAdapter |
Where-Object Status -eq 'Up' |
Sort-Object ifIndex |
Select-Object -First 1
if (-not $adapter) {
throw 'No active network adapter was found.'
}
return $adapter
}
function Set-ExactDnsServers {
param(
[int]$InterfaceIndex,
[string[]]$DesiredServers
)
$current = (Get-DnsClientServerAddress -InterfaceIndex $InterfaceIndex -AddressFamily IPv4).ServerAddresses
if (@($current) -join ',' -ne @($DesiredServers) -join ',') {
Set-DnsClientServerAddress -InterfaceIndex $InterfaceIndex -ServerAddresses $DesiredServers
}
}
function Set-ExactIPv4Configuration {
param(
[int]$InterfaceIndex,
[string]$DesiredIPv4,
[int]$DesiredPrefixLength,
[string]$DesiredGateway
)
$existing = Get-NetIPAddress -InterfaceIndex $InterfaceIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue
foreach ($address in $existing) {
if ($address.IPAddress -eq $DesiredIPv4 -and $address.PrefixLength -eq $DesiredPrefixLength) {
continue
}
if ($address.IPAddress -like '169.254.*') {
continue
}
Remove-NetIPAddress `
-InterfaceIndex $InterfaceIndex `
-AddressFamily IPv4 `
-IPAddress $address.IPAddress `
-Confirm:$false `
-ErrorAction SilentlyContinue
}
$desiredAddress = Get-NetIPAddress `
-InterfaceIndex $InterfaceIndex `
-AddressFamily IPv4 `
-ErrorAction SilentlyContinue |
Where-Object {
$_.IPAddress -eq $DesiredIPv4 -and
$_.PrefixLength -eq $DesiredPrefixLength
} |
Select-Object -First 1
if (-not $desiredAddress) {
New-NetIPAddress `
-InterfaceIndex $InterfaceIndex `
-IPAddress $DesiredIPv4 `
-PrefixLength $DesiredPrefixLength
}
$defaultRoutes = Get-NetRoute `
-InterfaceIndex $InterfaceIndex `
-DestinationPrefix '0.0.0.0/0' `
-AddressFamily IPv4 `
-ErrorAction SilentlyContinue
foreach ($route in $defaultRoutes) {
if ($route.NextHop -ne $DesiredGateway) {
Remove-NetRoute `
-InterfaceIndex $InterfaceIndex `
-DestinationPrefix '0.0.0.0/0' `
-NextHop $route.NextHop `
-Confirm:$false `
-ErrorAction SilentlyContinue
}
}
$desiredRoute = Get-NetRoute `
-InterfaceIndex $InterfaceIndex `
-DestinationPrefix '0.0.0.0/0' `
-AddressFamily IPv4 `
-ErrorAction SilentlyContinue |
Where-Object NextHop -eq $DesiredGateway |
Select-Object -First 1
if (-not $desiredRoute) {
New-NetRoute `
-InterfaceIndex $InterfaceIndex `
-DestinationPrefix '0.0.0.0/0' `
-NextHop $DesiredGateway `
-RouteMetric 256 | Out-Null
}
}
function Install-RequiredFeatures {
$requiredFeatures = 'AD-Domain-Services', 'DNS', 'DHCP'
$missingFeatures = Get-WindowsFeature $requiredFeatures |
Where-Object InstallState -ne 'Installed' |
Select-Object -ExpandProperty Name
if ($missingFeatures) {
Install-WindowsFeature -Name $missingFeatures -IncludeManagementTools | Out-Null
}
}
Resolve-DeploymentInputs
$adapter = Get-PrimaryAdapter
$restartRequired = $false
if ((Get-TimeZone).Id -ne $TimeZone) {
Set-TimeZone -Id $TimeZone
}
Set-ExactIPv4Configuration `
-InterfaceIndex $adapter.ifIndex `
-DesiredIPv4 $IPv4 `
-DesiredPrefixLength $PrefixLength `
-DesiredGateway $Gateway
Set-ExactDnsServers `
-InterfaceIndex $adapter.ifIndex `
-DesiredServers $DnsServers
if ($env:COMPUTERNAME -ne $TargetName) {
Rename-Computer -NewName $TargetName -Force
$restartRequired = $true
}
Install-RequiredFeatures
$summary = [pscustomobject]@{
Hostname = $env:COMPUTERNAME
TargetName = $TargetName
Adapter = $adapter.Name
IPv4 = $IPv4
PrefixLength = $PrefixLength
Gateway = $Gateway
DnsServers = ($DnsServers -join ', ')
RestartRequired = $restartRequired
}
$summary | Format-List | Out-String | Write-Output
if ($restartRequired -and $RestartIfNeeded) {
Write-Output 'Restarting to complete hostname change.'
Restart-Computer -Force
}
Deploy-HQ-FS01-Stage1.ps1
param(
[string]$TargetName,
[string]$IPv4,
[Nullable[int]]$PrefixLength,
[string]$Gateway,
[string[]]$DnsServers,
[string]$DomainName,
[string]$OuPath,
[string]$JoinCredentialUser,
[switch]$RestartIfNeeded
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Assert-Elevated {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this script from an elevated PowerShell session.'
}
}
function Read-RequiredValue {
param([string]$Prompt,[string]$DefaultValue)
$fullPrompt = $Prompt
if ($DefaultValue) { $fullPrompt = "$Prompt [$DefaultValue]" }
while ($true) {
$value = Read-Host -Prompt $fullPrompt
if (-not [string]::IsNullOrWhiteSpace($value)) { return $value.Trim() }
if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) { return $DefaultValue }
}
}
function Read-RequiredInt {
param([string]$Prompt,[int]$DefaultValue)
while ($true) {
$value = Read-RequiredValue -Prompt $Prompt -DefaultValue $DefaultValue
$parsed = 0
if ([int]::TryParse($value, [ref]$parsed)) { return $parsed }
}
}
function Read-DnsServerList {
param([string[]]$DefaultServers)
while ($true) {
$value = Read-RequiredValue -Prompt 'DNS servers (comma-separated)' -DefaultValue ($DefaultServers -join ',')
$servers = @($value.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ })
if ($servers.Count -gt 0) { return $servers }
}
}
function Resolve-Inputs {
$script:TargetName = if ([string]::IsNullOrWhiteSpace($TargetName)) { Read-RequiredValue 'Target hostname' 'HQ-FS01' } else { $TargetName.Trim() }
$script:IPv4 = if ([string]::IsNullOrWhiteSpace($IPv4)) { Read-RequiredValue 'IPv4 address' '172.20.20.21' } else { $IPv4.Trim() }
$script:PrefixLength = if ($null -eq $PrefixLength) { Read-RequiredInt 'IPv4 prefix length' 24 } else { [int]$PrefixLength }
$script:Gateway = if ([string]::IsNullOrWhiteSpace($Gateway)) { Read-RequiredValue 'Default gateway' '172.20.20.1' } else { $Gateway.Trim() }
$script:DnsServers = if ($DnsServers.Count -eq 0) { Read-DnsServerList -DefaultServers @('172.20.20.11') } else { @($DnsServers | ForEach-Object { $_.Trim() } | Where-Object { $_ }) }
$script:DomainName = if ([string]::IsNullOrWhiteSpace($DomainName)) { Read-RequiredValue 'Domain FQDN' 'corp.gntech.me' } else { $DomainName.Trim() }
$script:OuPath = if ([string]::IsNullOrWhiteSpace($OuPath)) { Read-RequiredValue 'Target server OU DN' 'OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $OuPath.Trim() }
$script:JoinCredentialUser = if ([string]::IsNullOrWhiteSpace($JoinCredentialUser)) { Read-RequiredValue 'Domain join user' 'corp.gntech.me\\svc.join.hq' } else { $JoinCredentialUser.Trim() }
}
function Get-PrimaryAdapter {
$adapter = Get-NetAdapter | Where-Object Status -eq 'Up' | Sort-Object ifIndex | Select-Object -First 1
if (-not $adapter) { throw 'No active network adapter was found.' }
return $adapter
}
function Test-DomainReadiness {
param([int]$InterfaceIndex)
$dnsServers = (Get-DnsClientServerAddress -InterfaceIndex $InterfaceIndex -AddressFamily IPv4).ServerAddresses
if (-not $dnsServers -or $dnsServers.Count -eq 0) {
throw 'No IPv4 DNS servers are configured on the active adapter.'
}
try {
Resolve-DnsName $script:DomainName -ErrorAction Stop | Out-Null
}
catch {
throw "DNS validation failed for domain '$($script:DomainName)'."
}
}
function Set-ExactDnsServers {
param([int]$InterfaceIndex,[string[]]$DesiredServers)
$current = (Get-DnsClientServerAddress -InterfaceIndex $InterfaceIndex -AddressFamily IPv4).ServerAddresses
if (@($current) -join ',' -ne @($DesiredServers) -join ',') {
Set-DnsClientServerAddress -InterfaceIndex $InterfaceIndex -ServerAddresses $DesiredServers
}
}
function Set-ExactIPv4Configuration {
param([int]$InterfaceIndex,[string]$DesiredIPv4,[int]$DesiredPrefixLength,[string]$DesiredGateway)
$existing = Get-NetIPAddress -InterfaceIndex $InterfaceIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue
foreach ($address in $existing) {
if ($address.IPAddress -eq $DesiredIPv4 -and $address.PrefixLength -eq $DesiredPrefixLength) { continue }
if ($address.IPAddress -like '169.254.*') { continue }
Remove-NetIPAddress -InterfaceIndex $InterfaceIndex -AddressFamily IPv4 -IPAddress $address.IPAddress -Confirm:$false -ErrorAction SilentlyContinue
}
$desiredAddress = Get-NetIPAddress -InterfaceIndex $InterfaceIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.IPAddress -eq $DesiredIPv4 -and $_.PrefixLength -eq $DesiredPrefixLength } | Select-Object -First 1
if (-not $desiredAddress) {
New-NetIPAddress -InterfaceIndex $InterfaceIndex -IPAddress $DesiredIPv4 -PrefixLength $DesiredPrefixLength | Out-Null
}
$defaultRoutes = Get-NetRoute -InterfaceIndex $InterfaceIndex -DestinationPrefix '0.0.0.0/0' -AddressFamily IPv4 -ErrorAction SilentlyContinue
foreach ($route in $defaultRoutes) {
if ($route.NextHop -ne $DesiredGateway) {
Remove-NetRoute -InterfaceIndex $InterfaceIndex -DestinationPrefix '0.0.0.0/0' -NextHop $route.NextHop -Confirm:$false -ErrorAction SilentlyContinue
}
}
$desiredRoute = Get-NetRoute -InterfaceIndex $InterfaceIndex -DestinationPrefix '0.0.0.0/0' -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object NextHop -eq $DesiredGateway | Select-Object -First 1
if (-not $desiredRoute) {
New-NetRoute -InterfaceIndex $InterfaceIndex -DestinationPrefix '0.0.0.0/0' -NextHop $DesiredGateway -RouteMetric 256 | Out-Null
}
}
Resolve-Inputs
Assert-Elevated
$adapter = Get-PrimaryAdapter
Set-ExactIPv4Configuration -InterfaceIndex $adapter.ifIndex -DesiredIPv4 $script:IPv4 -DesiredPrefixLength $script:PrefixLength -DesiredGateway $script:Gateway
Set-ExactDnsServers -InterfaceIndex $adapter.ifIndex -DesiredServers $script:DnsServers
Set-DnsClient -InterfaceIndex $adapter.ifIndex -ConnectionSpecificSuffix $script:DomainName -RegisterThisConnectionsAddress $true -UseSuffixWhenRegistering $true
Test-DomainReadiness -InterfaceIndex $adapter.ifIndex
$computerSystem = Get-CimInstance Win32_ComputerSystem
$renameRequired = $env:COMPUTERNAME -ne $script:TargetName
$joinRequired = -not $computerSystem.PartOfDomain
$restartRequired = $false
if ($renameRequired -or $joinRequired) {
$credential = Get-Credential -UserName $script:JoinCredentialUser -Message 'Enter domain join credential for HQ-FS01'
if ($renameRequired) {
Add-Computer -DomainName $script:DomainName -Credential $credential -OUPath $script:OuPath -NewName $script:TargetName -Force
$restartRequired = $true
Write-Output 'The server was renamed and joined to the domain.'
} else {
Add-Computer -DomainName $script:DomainName -Credential $credential -OUPath $script:OuPath -Force
$restartRequired = $true
Write-Output 'The server was joined to the domain.'
}
} else {
Write-Output 'No rename or domain join action is required.'
}
Write-Output "TargetName: $($script:TargetName)"
Write-Output "IPv4: $($script:IPv4)/$($script:PrefixLength)"
Write-Output "Gateway: $($script:Gateway)"
Write-Output "DnsServers: $($script:DnsServers -join ', ')"
Write-Output "DomainValidation: OK"
if ($restartRequired -and $RestartIfNeeded) {
Restart-Computer -Force
} elseif ($restartRequired) {
Write-Output 'A restart is required to complete HQ-FS01 stage 1.'
}
Deploy-HQ-MGMT01-Stage1.ps1
param(
[string]$TargetName,
[string]$DomainName,
[string]$OuPath,
[string]$JoinCredentialUser,
[string]$ExpectedDnsServer,
[string]$ExpectedGateway,
[switch]$EnableRdp,
[switch]$SkipDomainJoin,
[switch]$RestartIfNeeded
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Assert-Elevated {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this script from an elevated PowerShell session.'
}
}
function Read-RequiredValue {
param(
[string]$Prompt,
[string]$DefaultValue
)
$fullPrompt = $Prompt
if ($DefaultValue) { $fullPrompt = "$Prompt [$DefaultValue]" }
while ($true) {
$value = Read-Host -Prompt $fullPrompt
if (-not [string]::IsNullOrWhiteSpace($value)) { return $value.Trim() }
if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) { return $DefaultValue }
}
}
function Resolve-Inputs {
$script:TargetName = if ([string]::IsNullOrWhiteSpace($TargetName)) { Read-RequiredValue 'Target hostname' 'HQ-MGMT01' } else { $TargetName.Trim() }
$script:DomainName = if ([string]::IsNullOrWhiteSpace($DomainName)) { Read-RequiredValue 'Domain FQDN' 'corp.gntech.me' } else { $DomainName.Trim() }
$script:ExpectedDnsServer = if ([string]::IsNullOrWhiteSpace($ExpectedDnsServer)) { Read-RequiredValue 'Expected DNS server IPv4' '172.20.20.11' } else { $ExpectedDnsServer.Trim() }
$script:ExpectedGateway = if ([string]::IsNullOrWhiteSpace($ExpectedGateway)) { Read-RequiredValue 'Expected default gateway IPv4' '172.20.20.1' } else { $ExpectedGateway.Trim() }
}
function Get-PrimaryAdapter {
$adapter = Get-NetAdapter |
Where-Object Status -eq 'Up' |
Sort-Object ifIndex |
Select-Object -First 1
if (-not $adapter) { throw 'No active network adapter was found.' }
return $adapter
}
function Test-FirstBootState {
$setupState = Get-ItemProperty 'HKLM:\SYSTEM\Setup'
$imageState = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\State'
if ($setupState.OOBEInProgress -ne 0) { throw 'OOBE is still in progress. Stop before joining the domain.' }
if ($setupState.SystemSetupInProgress -ne 0) { throw 'System setup is still in progress. Stop before joining the domain.' }
if ($imageState.ImageState -ne 'IMAGE_STATE_COMPLETE') { throw "Unexpected image state '$($imageState.ImageState)'." }
}
function Test-NetworkReadiness {
param([Microsoft.Management.Infrastructure.CimInstance]$Adapter)
$ipConfig = Get-NetIPConfiguration -InterfaceIndex $Adapter.ifIndex
$ipv4Address = $ipConfig.IPv4Address | Select-Object -First 1
$gateways = @($ipConfig.IPv4DefaultGateway | ForEach-Object NextHop | Where-Object { $_ })
$dnsServers = (Get-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -AddressFamily IPv4).ServerAddresses
if (-not $ipv4Address) { throw 'No IPv4 address is assigned on the active adapter.' }
if ($ExpectedGateway -notin $gateways) { throw "Expected gateway $ExpectedGateway was not found." }
if ($ExpectedDnsServer -notin $dnsServers) { throw "Expected DNS server $ExpectedDnsServer was not found." }
Resolve-DnsName $script:DomainName -ErrorAction Stop | Out-Null
Write-Output "ActiveAdapter: $($Adapter.Name)"
Write-Output "IPv4: $($ipv4Address.IPAddress)"
Write-Output "Gateway: $($gateways -join ', ')"
Write-Output "DnsServers: $($dnsServers -join ', ')"
}
function Enable-RemoteDesktopAccess {
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name 'fDenyTSConnections' -Value 0
Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' | Out-Null
Write-Output 'RemoteDesktop: Enabled'
}
function Join-ManagementServer {
if ($SkipDomainJoin) {
return [pscustomobject]@{
RestartRequired = $false
Message = 'DomainJoin: Skipped'
}
}
$computerSystem = Get-CimInstance Win32_ComputerSystem
$renameRequired = $env:COMPUTERNAME -ne $script:TargetName
$joinRequired = -not $computerSystem.PartOfDomain
if (-not $renameRequired -and -not $joinRequired) {
return [pscustomobject]@{
RestartRequired = $false
Message = 'No rename or domain join action is required.'
}
}
$script:OuPath = if ([string]::IsNullOrWhiteSpace($OuPath)) { Read-RequiredValue 'Target server OU DN' 'OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me' } else { $OuPath.Trim() }
$script:JoinCredentialUser = if ([string]::IsNullOrWhiteSpace($JoinCredentialUser)) { Read-RequiredValue 'Domain join user' 'corp.gntech.me\svc.join.hq' } else { $JoinCredentialUser.Trim() }
$credential = Get-Credential -UserName $script:JoinCredentialUser -Message 'Enter domain join credential for HQ-MGMT01'
if ($renameRequired) {
Add-Computer -DomainName $script:DomainName -Credential $credential -OUPath $script:OuPath -NewName $script:TargetName -Force
return [pscustomobject]@{
RestartRequired = $true
Message = 'The management server was renamed and joined to the domain.'
}
}
Add-Computer -DomainName $script:DomainName -Credential $credential -OUPath $script:OuPath -Force
return [pscustomobject]@{
RestartRequired = $true
Message = 'The management server was joined to the domain.'
}
}
Resolve-Inputs
Assert-Elevated
Test-FirstBootState
$adapter = Get-PrimaryAdapter
Test-NetworkReadiness -Adapter $adapter
if ($EnableRdp) {
Enable-RemoteDesktopAccess
}
$joinResult = Join-ManagementServer
$restartRequired = [bool]$joinResult.RestartRequired
$computerSystem = Get-CimInstance Win32_ComputerSystem
Write-Output "TargetName: $($script:TargetName)"
Write-Output "DomainName: $($script:DomainName)"
Write-Output "CurrentComputerName: $($computerSystem.Name)"
Write-Output "CurrentDomain: $($computerSystem.Domain)"
Write-Output "PartOfDomain: $($computerSystem.PartOfDomain)"
if (-not $SkipDomainJoin) {
Write-Output "OuPath: $($script:OuPath)"
}
Write-Output $joinResult.Message
Write-Output "RestartRequired: $restartRequired"
Write-Output 'ManagementServerStage1: Complete'
if ($restartRequired -and $RestartIfNeeded) {
Restart-Computer -Force
} elseif ($restartRequired) {
Write-Output 'A restart is required to complete HQ-MGMT01 domain onboarding.'
}
Grant-HQ-JoinDelegation.ps1
param(
[string]$JoinAccountSam = 'svc.join.hq',
[string[]]$TargetOuDns,
[switch]$ValidateOnly
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Assert-Elevated {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this script from an elevated PowerShell session.'
}
}
function Read-RequiredValue {
param(
[string]$Prompt,
[string]$DefaultValue
)
$fullPrompt = $Prompt
if ($DefaultValue) {
$fullPrompt = "$Prompt [$DefaultValue]"
}
while ($true) {
$value = Read-Host -Prompt $fullPrompt
if (-not [string]::IsNullOrWhiteSpace($value)) {
return $value.Trim()
}
if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) {
return $DefaultValue
}
}
}
function Read-StringList {
param(
[string]$Prompt,
[string[]]$DefaultValues
)
while ($true) {
$defaultValue = $DefaultValues -join ','
$value = Read-RequiredValue -Prompt $Prompt -DefaultValue $defaultValue
$items = @(
$value.Split(',') |
ForEach-Object { $_.Trim() } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
if ($items.Count -gt 0) {
return $items
}
}
}
function Resolve-Inputs {
$script:JoinAccountSam = if ([string]::IsNullOrWhiteSpace($JoinAccountSam)) {
Read-RequiredValue -Prompt 'Join account SamAccountName' -DefaultValue 'svc.join.hq'
}
else {
$JoinAccountSam.Trim()
}
$script:TargetOuDns = if ($TargetOuDns.Count -eq 0) {
Read-StringList -Prompt 'Target OU distinguished names (comma-separated)' -DefaultValues @(
'OU=Workstations,OU=GNTECH,DC=corp,DC=gntech,DC=me',
'OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me'
)
}
else {
@($TargetOuDns | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
}
function Ensure-Module {
param([string]$Name)
Import-Module $Name
}
function Assert-JoinAccountExists {
param([string]$SamAccountName)
$account = Get-ADUser -Filter "SamAccountName -eq '$SamAccountName'" -Properties SID -ErrorAction SilentlyContinue
if (-not $account) {
throw "The join account '$SamAccountName' was not found in Active Directory."
}
return $account
}
function Assert-OuExists {
param([string]$DistinguishedName)
$ou = Get-ADOrganizationalUnit -Identity $DistinguishedName -ErrorAction SilentlyContinue
if (-not $ou) {
throw "The OU '$DistinguishedName' was not found."
}
return $ou
}
function Grant-JoinAcl {
param(
[string]$TargetOuDn,
[System.Security.Principal.SecurityIdentifier]$Sid
)
$computerClassGuid = [Guid]'bf967a86-0de6-11d0-a285-00aa003049e2'
$emptyGuid = [Guid]::Empty
$aclPath = "AD:$TargetOuDn"
$acl = Get-Acl -Path $aclPath
$rules = @(
(New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$Sid,
[System.DirectoryServices.ActiveDirectoryRights]::CreateChild,
[System.Security.AccessControl.AccessControlType]::Allow,
$computerClassGuid,
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
)),
(New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$Sid,
[System.DirectoryServices.ActiveDirectoryRights]::DeleteChild,
[System.Security.AccessControl.AccessControlType]::Allow,
$computerClassGuid,
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
)),
(New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$Sid,
[System.DirectoryServices.ActiveDirectoryRights]::GenericAll,
[System.Security.AccessControl.AccessControlType]::Allow,
$emptyGuid,
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::Descendents,
$computerClassGuid
))
)
$results = foreach ($rule in $rules) {
$present = @($acl.Access | Where-Object {
$_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -eq $Sid.Value -and
$_.AccessControlType -eq $rule.AccessControlType -and
($_.ActiveDirectoryRights -band $rule.ActiveDirectoryRights) -eq $rule.ActiveDirectoryRights -and
$_.ObjectType -eq $rule.ObjectType -and
$_.InheritanceType -eq $rule.InheritanceType -and
$_.InheritedObjectType -eq $rule.InheritedObjectType
}).Count -gt 0
if (-not $present -and -not $ValidateOnly) { $acl.AddAccessRule($rule) | Out-Null }
[pscustomobject]@{
TargetOuDn = $TargetOuDn
IdentitySid = $Sid.Value
Rights = $rule.ActiveDirectoryRights
InheritanceType = $rule.InheritanceType
Action = if ($present) { 'Compliant' } elseif ($ValidateOnly) { 'Missing' } else { 'Granted' }
}
}
if (-not $ValidateOnly -and @($results | Where-Object Action -eq 'Granted').Count -gt 0) {
Set-Acl -Path $aclPath -AclObject $acl
}
return $results
}
Resolve-Inputs
Assert-Elevated
Ensure-Module -Name ActiveDirectory
$joinAccount = Assert-JoinAccountExists -SamAccountName $script:JoinAccountSam
$summary = [pscustomobject]@{
JoinAccountSamAccountName = $joinAccount.SamAccountName
JoinAccountDn = $joinAccount.DistinguishedName
TargetOus = ($script:TargetOuDns -join ' | ')
ValidateOnly = [bool]$ValidateOnly
}
$summary | Format-List | Out-String | Write-Output
foreach ($targetOuDn in $script:TargetOuDns) {
Assert-OuExists -DistinguishedName $targetOuDn | Out-Null
Grant-JoinAcl -TargetOuDn $targetOuDn -Sid $joinAccount.SID |
Format-Table -AutoSize | Out-String | Write-Output
}
if ($ValidateOnly) {
Write-Output 'Validation mode only. No delegation changes were applied.'
}
else {
Write-Output 'Join delegation completed successfully.'
}
Join-HQ-Workstation.ps1
param(
[string]$TargetName,
[string]$DomainName,
[string]$OuPath,
[string]$DomainControllerFqdn,
[string]$ExpectedDnsServer,
[string]$ExpectedGateway,
[string]$LegacyBuildGateway = '172.20.110.1',
[string]$InternetTestHost,
[pscredential]$DomainCredential,
[switch]$RemoveLegacyBuildGateway,
[switch]$RestartIfNeeded,
[switch]$SkipInternetTest
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Read-RequiredValue {
param(
[string]$Prompt,
[string]$DefaultValue
)
$fullPrompt = $Prompt
if ($DefaultValue) {
$fullPrompt = "$Prompt [$DefaultValue]"
}
while ($true) {
$value = Read-Host -Prompt $fullPrompt
if (-not [string]::IsNullOrWhiteSpace($value)) {
return $value.Trim()
}
if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) {
return $DefaultValue
}
}
}
function Resolve-Inputs {
$script:TargetName = if ([string]::IsNullOrWhiteSpace($TargetName)) {
Read-RequiredValue -Prompt 'Target workstation name' -DefaultValue $env:COMPUTERNAME
}
else {
$TargetName.Trim()
}
$script:DomainName = if ([string]::IsNullOrWhiteSpace($DomainName)) {
Read-RequiredValue -Prompt 'Domain FQDN' -DefaultValue 'corp.gntech.me'
}
else {
$DomainName.Trim()
}
$script:OuPath = if ([string]::IsNullOrWhiteSpace($OuPath)) {
Read-RequiredValue -Prompt 'Target OU distinguished name' -DefaultValue 'OU=Workstations,OU=GNTECH,DC=corp,DC=gntech,DC=me'
}
else {
$OuPath.Trim()
}
$script:DomainControllerFqdn = if ([string]::IsNullOrWhiteSpace($DomainControllerFqdn)) {
Read-RequiredValue -Prompt 'Domain controller FQDN for validation' -DefaultValue 'hq-dc01.corp.gntech.me'
}
else {
$DomainControllerFqdn.Trim()
}
$script:ExpectedDnsServer = if ([string]::IsNullOrWhiteSpace($ExpectedDnsServer)) {
Read-RequiredValue -Prompt 'Expected DNS server IPv4' -DefaultValue '172.20.20.11'
}
else {
$ExpectedDnsServer.Trim()
}
$script:ExpectedGateway = if ([string]::IsNullOrWhiteSpace($ExpectedGateway)) {
Read-RequiredValue -Prompt 'Expected default gateway IPv4' -DefaultValue '172.20.30.1'
}
else {
$ExpectedGateway.Trim()
}
$script:InternetTestHost = if ([string]::IsNullOrWhiteSpace($InternetTestHost)) {
Read-RequiredValue -Prompt 'Internet validation host' -DefaultValue 'github.com'
}
else {
$InternetTestHost.Trim()
}
}
function Assert-Elevated {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this script from an elevated PowerShell session.'
}
}
function Get-PrimaryAdapter {
$adapter = Get-NetAdapter |
Where-Object Status -eq 'Up' |
Sort-Object ifIndex |
Select-Object -First 1
if (-not $adapter) {
throw 'No active network adapter was found.'
}
return $adapter
}
function Test-FirstBootState {
$setupState = Get-ItemProperty 'HKLM:\SYSTEM\Setup'
$imageState = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\State'
if ($setupState.OOBEInProgress -ne 0) {
throw 'OOBE is still in progress. Stop and fix the source template before domain join.'
}
if ($setupState.SystemSetupInProgress -ne 0) {
throw 'System setup is still in progress. Stop and fix the source template before domain join.'
}
if ($imageState.ImageState -ne 'IMAGE_STATE_COMPLETE') {
throw "Unexpected image state '$($imageState.ImageState)'. Stop and fix the source template before domain join."
}
}
function Test-NetworkReadiness {
param(
[Microsoft.Management.Infrastructure.CimInstance]$Adapter
)
$ipConfig = Get-NetIPConfiguration -InterfaceIndex $Adapter.ifIndex
$ipv4Address = $ipConfig.IPv4Address | Select-Object -First 1
$defaultGateways = @($ipConfig.IPv4DefaultGateway)
$dnsServers = (Get-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -AddressFamily IPv4).ServerAddresses
if (-not $ipv4Address) {
throw 'No IPv4 address is assigned on the active adapter.'
}
if (-not $ipv4Address.PrefixOrigin -or $ipv4Address.PrefixOrigin -ne 'Dhcp') {
throw 'The active adapter is not using DHCP for IPv4. Workstation onboarding requires DHCP.'
}
if ($defaultGateways.Count -eq 0) {
throw 'No default gateway is present on the active adapter.'
}
$gatewayAddresses = @($defaultGateways | ForEach-Object NextHop | Where-Object { $_ })
if ($ExpectedGateway -notin $gatewayAddresses) {
throw "Expected gateway $ExpectedGateway was not found on the active adapter."
}
$unexpectedGateways = @($gatewayAddresses | Where-Object { $_ -ne $ExpectedGateway })
if ($unexpectedGateways.Count -gt 0) {
throw "Unexpected default gateway(s) detected: $($unexpectedGateways -join ', '). Correct the template or use the explicit legacy-build cleanup switch."
}
if (-not $dnsServers -or $dnsServers.Count -eq 0) {
throw 'No IPv4 DNS servers are configured on the active adapter.'
}
if ($ExpectedDnsServer -notin $dnsServers) {
throw "Expected DNS server $ExpectedDnsServer was not found on the active adapter."
}
Resolve-DnsName $DomainControllerFqdn | Out-Null
if (-not $SkipInternetTest) {
$internetTest = Test-NetConnection $InternetTestHost -Port 443 -WarningAction SilentlyContinue
if (-not $internetTest.TcpTestSucceeded) {
throw "Internet validation failed for $InternetTestHost on TCP 443."
}
}
Write-Output "ActiveAdapter: $($Adapter.Name)"
Write-Output "IPv4: $($ipv4Address.IPAddress)"
Write-Output "Gateway: $($gatewayAddresses -join ', ')"
Write-Output "DnsServers: $($dnsServers -join ', ')"
}
function Get-DomainJoinCredential {
if ($DomainCredential) { return $DomainCredential }
$defaultUser = "Administrator@$($script:DomainName)"
try {
return Get-Credential -UserName $defaultUser -Message 'Enter domain join credential'
}
catch {
throw 'A valid domain credential is required to join the workstation.'
}
}
function Remove-LegacyBuildRoute {
param([Microsoft.Management.Infrastructure.CimInstance]$Adapter)
if (-not $RemoveLegacyBuildGateway) { return }
if ([string]::IsNullOrWhiteSpace($LegacyBuildGateway)) {
throw 'LegacyBuildGateway is required when RemoveLegacyBuildGateway is used.'
}
$interface = Get-NetIPInterface -InterfaceIndex $Adapter.ifIndex -AddressFamily IPv4
if ($interface.Dhcp -ne 'Enabled') {
throw 'Legacy build-route cleanup is allowed only on a DHCP-enabled adapter.'
}
$legacyRoutes = @(Get-NetRoute `
-PolicyStore PersistentStore `
-InterfaceIndex $Adapter.ifIndex `
-DestinationPrefix '0.0.0.0/0' `
-ErrorAction SilentlyContinue |
Where-Object NextHop -eq $LegacyBuildGateway)
foreach ($route in $legacyRoutes) {
Remove-NetRoute `
-PolicyStore PersistentStore `
-InterfaceIndex $Adapter.ifIndex `
-DestinationPrefix $route.DestinationPrefix `
-NextHop $route.NextHop `
-Confirm:$false
}
$activeLegacyRoutes = @(Get-NetRoute `
-PolicyStore ActiveStore `
-InterfaceIndex $Adapter.ifIndex `
-DestinationPrefix '0.0.0.0/0' `
-ErrorAction SilentlyContinue |
Where-Object NextHop -eq $LegacyBuildGateway)
foreach ($route in $activeLegacyRoutes) {
Remove-NetRoute `
-PolicyStore ActiveStore `
-InterfaceIndex $Adapter.ifIndex `
-DestinationPrefix $route.DestinationPrefix `
-NextHop $route.NextHop `
-Confirm:$false
}
Write-Output "LegacyBuildPersistentRoutesRemoved: $($legacyRoutes.Count)"
Write-Output "LegacyBuildActiveRoutesRemoved: $($activeLegacyRoutes.Count)"
}
Resolve-Inputs
Assert-Elevated
Test-FirstBootState
$adapter = Get-PrimaryAdapter
Remove-LegacyBuildRoute -Adapter $adapter
Test-NetworkReadiness -Adapter $adapter
$computerSystem = Get-CimInstance Win32_ComputerSystem
$renameRequired = $computerSystem.Name -ne $TargetName
$joinRequired = -not $computerSystem.PartOfDomain
$restartRequired = $false
Write-Output "CurrentComputerName: $($computerSystem.Name)"
Write-Output "TargetComputerName: $TargetName"
Write-Output "PartOfDomain: $($computerSystem.PartOfDomain)"
if (-not $renameRequired -and -not $joinRequired) {
Write-Output 'No rename or domain join action is required.'
return
}
$credential = Get-DomainJoinCredential
if ($renameRequired -and $joinRequired) {
Add-Computer `
-DomainName $DomainName `
-Credential $credential `
-OUPath $OuPath `
-NewName $TargetName `
-Force
$restartRequired = $true
Write-Output 'The workstation was renamed and joined to the domain.'
}
elseif ($joinRequired) {
Add-Computer `
-DomainName $DomainName `
-Credential $credential `
-OUPath $OuPath `
-Force
$restartRequired = $true
Write-Output 'The workstation was joined to the domain.'
}
elseif ($renameRequired) {
Rename-Computer -NewName $TargetName -DomainCredential $credential -Force
$restartRequired = $true
Write-Output 'The workstation was renamed.'
}
if ($restartRequired -and $RestartIfNeeded) {
Restart-Computer -Force
}
elseif ($restartRequired) {
Write-Output 'A restart is required to complete the workstation onboarding.'
}
Promote-HQ-DC01-Forest.ps1
param(
[string]$DomainName,
[string]$NetBIOSName,
[switch]$InstallDns = $true,
[switch]$NoRebootOnCompletion,
[switch]$ValidateOnly
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Read-RequiredValue {
param(
[string]$Prompt,
[string]$DefaultValue
)
$fullPrompt = $Prompt
if ($DefaultValue) {
$fullPrompt = "$Prompt [$DefaultValue]"
}
while ($true) {
$value = Read-Host -Prompt $fullPrompt
if (-not [string]::IsNullOrWhiteSpace($value)) {
return $value.Trim()
}
if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) {
return $DefaultValue
}
}
}
function Read-RequiredSecret {
param(
[string]$Prompt
)
try {
return (Read-Host -Prompt $Prompt -AsSecureString)
}
catch {
throw "A secure value for '$Prompt' is required when running non-interactively."
}
}
function Convert-SecureStringToLength {
param(
[Security.SecureString]$SecureValue
)
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureValue)
try {
return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr).Length
}
finally {
if ($bstr -ne [IntPtr]::Zero) {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
}
}
}
function Resolve-Inputs {
$script:DomainName = if ([string]::IsNullOrWhiteSpace($DomainName)) {
Read-RequiredValue -Prompt 'Forest/domain FQDN' -DefaultValue 'corp.gntech.me'
}
else {
$DomainName.Trim()
}
$script:NetBIOSName = if ([string]::IsNullOrWhiteSpace($NetBIOSName)) {
Read-RequiredValue -Prompt 'NetBIOS name' -DefaultValue 'GNTECH'
}
else {
$NetBIOSName.Trim()
}
if (-not $ValidateOnly) {
$script:LocalAdministratorPassword = Read-RequiredSecret `
-Prompt 'Local Administrator password'
$script:SafeModeAdministratorPassword = Read-RequiredSecret `
-Prompt 'DSRM password'
}
}
function Test-Prerequisites {
$computerSystem = Get-CimInstance Win32_ComputerSystem
if ($computerSystem.PartOfDomain) {
throw "Server is already joined to domain '$($computerSystem.Domain)'."
}
$addsFeature = Get-WindowsFeature AD-Domain-Services
if ($addsFeature.InstallState -ne 'Installed') {
throw 'AD-Domain-Services feature is not installed.'
}
$localAdministrator = Get-LocalUser | Where-Object SID -like 'S-1-5-21-*-500' | Select-Object -First 1
if (-not $localAdministrator) {
throw 'Built-in local Administrator account with RID 500 was not found.'
}
if (-not $ValidateOnly) {
$localAdminLength = Convert-SecureStringToLength -SecureValue $script:LocalAdministratorPassword
$safeModeLength = Convert-SecureStringToLength -SecureValue $script:SafeModeAdministratorPassword
if ($localAdminLength -lt 12) {
throw 'Local Administrator password must be at least 12 characters.'
}
if ($safeModeLength -lt 12) {
throw 'DSRM password must be at least 12 characters.'
}
}
[pscustomobject]@{
Hostname = $env:COMPUTERNAME
DomainName = $script:DomainName
NetBIOSName = $script:NetBIOSName
PartOfDomain = $computerSystem.PartOfDomain
AddsInstalled = $addsFeature.InstallState
InstallDns = [bool]$InstallDns
NoRebootOnCompletion = [bool]$NoRebootOnCompletion
ValidateOnly = [bool]$ValidateOnly
}
}
Resolve-Inputs
$summary = Test-Prerequisites
$summary | Format-List | Out-String | Write-Output
if ($ValidateOnly) {
Write-Output 'Validation mode only. Promotion was not started.'
return
}
$localAdministrator = Get-LocalUser | Where-Object SID -like 'S-1-5-21-*-500' | Select-Object -First 1
$localAdministrator | Set-LocalUser -Password $script:LocalAdministratorPassword
$promotionParams = @{
DomainName = $script:DomainName
DomainNetbiosName = $script:NetBIOSName
SafeModeAdministratorPassword = $script:SafeModeAdministratorPassword
InstallDns = [bool]$InstallDns
Force = $true
NoRebootOnCompletion = [bool]$NoRebootOnCompletion
}
Install-ADDSForest @promotionParams
Validate-HQ-DC01.ps1
param(
[string]$ExpectedHostname,
[string]$ExpectedDomainFqdn,
[string]$ExpectedNetBIOSName,
[string]$ExpectedUpnSuffix,
[string]$ExpectedDcIp,
[string[]]$ExpectedDnsForwarders,
[string[]]$ExpectedDnsZones,
[string]$ExpectedDhcpAuthorizedDnsName,
[string]$ExpectedDhcpAuthorizedIp,
[string]$ExpectedDhcpScopeId,
[string]$ExpectedEnterpriseRootOuDn,
[string]$OuTemplateCsvPath,
[string[]]$ExpectedOuDns
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Read-RequiredValue {
param(
[string]$Prompt,
[string]$DefaultValue
)
$fullPrompt = $Prompt
if ($DefaultValue) {
$fullPrompt = "$Prompt [$DefaultValue]"
}
while ($true) {
$value = Read-Host -Prompt $fullPrompt
if (-not [string]::IsNullOrWhiteSpace($value)) {
return $value.Trim()
}
if (-not [string]::IsNullOrWhiteSpace($DefaultValue)) {
return $DefaultValue
}
}
}
function Read-StringList {
param(
[string]$Prompt,
[string[]]$DefaultValues
)
while ($true) {
$defaultValue = $DefaultValues -join ','
$value = Read-RequiredValue -Prompt $Prompt -DefaultValue $defaultValue
$items = @(
$value.Split(',') |
ForEach-Object { $_.Trim() } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
if ($items.Count -gt 0) {
return $items
}
}
}
function Resolve-Inputs {
$script:ExpectedHostname = if ([string]::IsNullOrWhiteSpace($ExpectedHostname)) {
Read-RequiredValue -Prompt 'Expected hostname' -DefaultValue 'HQ-DC01'
} else { $ExpectedHostname.Trim() }
$script:ExpectedDomainFqdn = if ([string]::IsNullOrWhiteSpace($ExpectedDomainFqdn)) {
Read-RequiredValue -Prompt 'Expected domain FQDN' -DefaultValue 'corp.gntech.me'
} else { $ExpectedDomainFqdn.Trim() }
$script:ExpectedNetBIOSName = if ([string]::IsNullOrWhiteSpace($ExpectedNetBIOSName)) {
Read-RequiredValue -Prompt 'Expected NetBIOS name' -DefaultValue 'GNTECH'
} else { $ExpectedNetBIOSName.Trim() }
$script:ExpectedUpnSuffix = if ([string]::IsNullOrWhiteSpace($ExpectedUpnSuffix)) {
Read-RequiredValue -Prompt 'Expected UPN suffix' -DefaultValue 'gntech.me'
} else { $ExpectedUpnSuffix.Trim() }
$script:ExpectedDcIp = if ([string]::IsNullOrWhiteSpace($ExpectedDcIp)) {
Read-RequiredValue -Prompt 'Expected DC IPv4' -DefaultValue '172.20.20.11'
} else { $ExpectedDcIp.Trim() }
$script:ExpectedDnsForwarders = if ($ExpectedDnsForwarders.Count -eq 0) {
Read-StringList -Prompt 'Expected DNS forwarders (comma-separated)' -DefaultValues @('1.1.1.1','9.9.9.9')
} else {
@($ExpectedDnsForwarders | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
$script:ExpectedDnsZones = if ($ExpectedDnsZones.Count -eq 0) {
Read-StringList -Prompt 'Expected DNS zones (comma-separated)' -DefaultValues @('corp.gntech.me','20.20.172.in-addr.arpa','30.20.172.in-addr.arpa')
} else {
@($ExpectedDnsZones | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
$script:ExpectedDhcpAuthorizedDnsName = if ([string]::IsNullOrWhiteSpace($ExpectedDhcpAuthorizedDnsName)) {
Read-RequiredValue -Prompt 'Expected DHCP authorized DNS name' -DefaultValue 'hq-dc01.corp.gntech.me'
} else { $ExpectedDhcpAuthorizedDnsName.Trim() }
$script:ExpectedDhcpAuthorizedIp = if ([string]::IsNullOrWhiteSpace($ExpectedDhcpAuthorizedIp)) {
Read-RequiredValue -Prompt 'Expected DHCP authorized IP' -DefaultValue '172.20.20.11'
} else { $ExpectedDhcpAuthorizedIp.Trim() }
$script:ExpectedDhcpScopeId = if ([string]::IsNullOrWhiteSpace($ExpectedDhcpScopeId)) {
Read-RequiredValue -Prompt 'Expected DHCP scope ID' -DefaultValue '172.20.30.0'
} else { $ExpectedDhcpScopeId.Trim() }
$script:ExpectedEnterpriseRootOuDn = if ([string]::IsNullOrWhiteSpace($ExpectedEnterpriseRootOuDn)) {
Read-RequiredValue -Prompt 'Expected enterprise root OU DN' -DefaultValue 'OU=GNTECH,DC=corp,DC=gntech,DC=me'
} else { $ExpectedEnterpriseRootOuDn.Trim() }
$script:OuTemplateCsvPath = if ([string]::IsNullOrWhiteSpace($OuTemplateCsvPath)) {
$scriptDirectory = if (-not [string]::IsNullOrWhiteSpace($PSScriptRoot)) {
$PSScriptRoot
} else {
(Get-Location).Path
}
$defaultCsv = Join-Path -Path $scriptDirectory -ChildPath 'ou-baseline.csv'
if (Test-Path -LiteralPath $defaultCsv) { $defaultCsv } else { '' }
} else { $OuTemplateCsvPath.Trim() }
$script:ExpectedOuDns = if ($ExpectedOuDns.Count -eq 0 -and [string]::IsNullOrWhiteSpace($script:OuTemplateCsvPath)) {
@(
'OU=Tier0,OU=GNTECH,DC=corp,DC=gntech,DC=me',
'OU=Tier1,OU=GNTECH,DC=corp,DC=gntech,DC=me',
'OU=Tier2,OU=GNTECH,DC=corp,DC=gntech,DC=me',
'OU=Users,OU=GNTECH,DC=corp,DC=gntech,DC=me',
'OU=Workstations,OU=GNTECH,DC=corp,DC=gntech,DC=me',
'OU=Servers,OU=GNTECH,DC=corp,DC=gntech,DC=me',
'OU=Groups,OU=GNTECH,DC=corp,DC=gntech,DC=me',
'OU=ServiceAccounts,OU=GNTECH,DC=corp,DC=gntech,DC=me',
'OU=Admins,OU=GNTECH,DC=corp,DC=gntech,DC=me',
'OU=Sites,OU=GNTECH,DC=corp,DC=gntech,DC=me'
)
} else {
@($ExpectedOuDns | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
}
function Ensure-Module {
param([string]$Name)
Import-Module $Name
}
function Get-ForwarderStrings {
@(Get-DnsServerForwarder -ErrorAction SilentlyContinue | ForEach-Object {
if ($_.IPAddress -is [array]) {
$_.IPAddress | ForEach-Object { $_.IPAddressToString }
}
elseif ($_.IPAddress) {
$_.IPAddress.IPAddressToString
}
})
}
function Get-ExpectedOuDnsFromCsv {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path)) {
throw "OU template CSV '$Path' was not found."
}
@(Import-Csv -LiteralPath $Path | Where-Object {
$_.Enabled -match '^(?i:true|1|yes)$'
} | ForEach-Object {
"OU=$($_.Name),$($_.ParentDn)"
})
}
Resolve-Inputs
Ensure-Module -Name ActiveDirectory
Ensure-Module -Name DhcpServer
Ensure-Module -Name DnsServer
if (-not [string]::IsNullOrWhiteSpace($script:OuTemplateCsvPath)) {
$script:ExpectedOuDns = @(Get-ExpectedOuDnsFromCsv -Path $script:OuTemplateCsvPath | Where-Object { $_ -ne $script:ExpectedEnterpriseRootOuDn })
}
$forest = Get-ADForest
$domain = Get-ADDomain
$computerSystem = Get-CimInstance Win32_ComputerSystem
$features = Get-WindowsFeature AD-Domain-Services, DNS, DHCP
$services = Get-Service NTDS, DNS, DHCPServer
$forwarders = Get-ForwarderStrings
$zones = @(Get-DnsServerZone -ErrorAction SilentlyContinue | Where-Object ZoneName -in $script:ExpectedDnsZones)
$authorizedDhcp = @(Get-DhcpServerInDC -ErrorAction SilentlyContinue)
$scope = Get-DhcpServerv4Scope -ScopeId $script:ExpectedDhcpScopeId -ErrorAction SilentlyContinue
$scopeOptions = @(Get-DhcpServerv4OptionValue -ScopeId $script:ExpectedDhcpScopeId -ErrorAction SilentlyContinue)
$rootOu = Get-ADOrganizationalUnit -LDAPFilter "(distinguishedName=$($script:ExpectedEnterpriseRootOuDn))" -ErrorAction SilentlyContinue
$expectedOusFound = @()
foreach ($ouDn in $script:ExpectedOuDns) {
$found = Get-ADOrganizationalUnit -LDAPFilter "(distinguishedName=$ouDn)" -ErrorAction SilentlyContinue
if ($found) {
$expectedOusFound += $ouDn
}
}
Write-Output '=== Identity ==='
Write-Output "Hostname: $env:COMPUTERNAME"
Write-Output "PartOfDomain: $($computerSystem.PartOfDomain)"
Write-Output "ComputerSystemDomain: $($computerSystem.Domain)"
Write-Output "ForestRootDomain: $($forest.RootDomain)"
Write-Output "ForestMode: $($forest.ForestMode)"
Write-Output "UPNSuffixes: $($forest.UPNSuffixes -join ', ')"
Write-Output "DomainDNSRoot: $($domain.DNSRoot)"
Write-Output "DomainNetBIOSName: $($domain.NetBIOSName)"
Write-Output "DomainMode: $($domain.DomainMode)"
Write-Output '=== Features ==='
foreach ($feature in $features) {
Write-Output "$($feature.Name): $($feature.InstallState)"
}
Write-Output '=== Services ==='
foreach ($service in $services) {
Write-Output "$($service.Name): $($service.Status)"
}
Write-Output '=== DNS ==='
if ($forwarders.Count -eq 0) {
Write-Output 'Forwarders: none found'
} else {
foreach ($forwarder in $forwarders) {
Write-Output "Forwarder: $forwarder"
}
}
if ($zones.Count -eq 0) {
Write-Output 'Zones: none found'
} else {
foreach ($zone in $zones) {
Write-Output "Zone: $($zone.ZoneName) / Type=$($zone.ZoneType) / DsIntegrated=$($zone.IsDsIntegrated)"
}
}
Write-Output '=== DHCP ==='
if ($authorizedDhcp.Count -eq 0) {
Write-Output 'Authorized DHCP servers: none found'
} else {
foreach ($entry in $authorizedDhcp) {
Write-Output "Authorized DHCP: $($entry.DnsName) / $($entry.IPAddress.IPAddressToString)"
}
}
if (-not $scope) {
Write-Output "Scope $($script:ExpectedDhcpScopeId): missing"
} else {
Write-Output "Scope: $($scope.ScopeId.IPAddressToString) / Name=$($scope.Name) / State=$($scope.State) / Start=$($scope.StartRange.IPAddressToString) / End=$($scope.EndRange.IPAddressToString)"
}
if ($scopeOptions.Count -eq 0) {
Write-Output "DHCP options for scope $($script:ExpectedDhcpScopeId): none found"
} else {
foreach ($option in $scopeOptions) {
$valueText = if ($option.Value -is [array]) { $option.Value -join ', ' } else { $option.Value }
Write-Output "DHCP Option $($option.OptionId): $($option.Name) = $valueText"
}
}
Write-Output '=== OU Structure ==='
if (-not $rootOu) {
Write-Output "Enterprise root OU missing: $($script:ExpectedEnterpriseRootOuDn)"
} else {
Write-Output "Enterprise root OU: $($rootOu.DistinguishedName)"
}
foreach ($ouDn in $script:ExpectedOuDns) {
if ($expectedOusFound -contains $ouDn) {
Write-Output "OU Present: $ouDn"
} else {
Write-Output "OU Missing: $ouDn"
}
}
Validate-WS2025-GoldenImage.ps1
[CmdletBinding()]
param(
[string]$ExpectedCaptionPattern = '*Windows Server 2025 Standard*',
[string]$ExpectedPowerShellVersion = '7.6.3'
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$failures = [System.Collections.Generic.List[string]]::new()
Write-Output '=== Operating system ==='
$operatingSystem = Get-CimInstance Win32_OperatingSystem
Write-Output "Caption: $($operatingSystem.Caption)"
Write-Output "Version: $($operatingSystem.Version)"
Write-Output "Build: $($operatingSystem.BuildNumber)"
if ($operatingSystem.Caption -notlike $ExpectedCaptionPattern) {
$failures.Add("Unexpected Windows edition: $($operatingSystem.Caption)")
}
Write-Output '=== QEMU Guest Agent ==='
$qemuAgent = Get-Service -Name 'QEMU-GA' -ErrorAction Stop
Write-Output "QEMU-GA: $($qemuAgent.Status) / $($qemuAgent.StartType)"
if ($qemuAgent.Status -ne 'Running') { $failures.Add('QEMU-GA is not running.') }
if ($qemuAgent.StartType -ne 'Automatic') { $failures.Add('QEMU-GA is not automatic.') }
Write-Output '=== OpenSSH ==='
$sshd = Get-Service -Name 'sshd' -ErrorAction SilentlyContinue
if ($sshd) {
Write-Output "sshd: $($sshd.Status) / $($sshd.StartType)"
if ($sshd.Status -ne 'Stopped') { $failures.Add('sshd is not stopped.') }
if ($sshd.StartType -ne 'Disabled') { $failures.Add('sshd is not disabled.') }
} else {
Write-Output 'sshd: Not installed'
}
Write-Output '=== PowerShell 7 ==='
$pwshPath = 'C:\Program Files\PowerShell\7\pwsh.exe'
if (-not (Test-Path $pwshPath)) {
$failures.Add('PowerShell 7 executable is missing.')
} else {
$pwshVersion = & $pwshPath -NoProfile -Command '$PSVersionTable.PSVersion.ToString()'
Write-Output "PowerShell 7 version: $pwshVersion"
if ($pwshVersion -ne $ExpectedPowerShellVersion) {
$failures.Add("Unexpected PowerShell 7 version: $pwshVersion")
}
}
Write-Output '=== Problem devices ==='
$pnpJob = Start-Job -ScriptBlock {
@(Get-PnpDevice -PresentOnly | Where-Object Status -ne 'OK')
}
if (-not (Wait-Job -Job $pnpJob -Timeout 60)) {
Stop-Job -Job $pnpJob
Remove-Job -Job $pnpJob -Force
$failures.Add('PnP device enumeration exceeded 60 seconds.')
$problemDevices = @()
} else {
$problemDevices = @(Receive-Job -Job $pnpJob)
Remove-Job -Job $pnpJob -Force
}
if ($problemDevices.Count -gt 0) {
$problemDevices | Select-Object Class, FriendlyName, Status, Problem |
Format-Table -AutoSize | Out-String | Write-Output
$failures.Add("$($problemDevices.Count) problem device(s) detected.")
} else {
Write-Output 'No problem devices detected.'
}
Write-Output '=== Forbidden roles ==='
$forbiddenRoles = @(
Get-WindowsFeature AD-Domain-Services, DNS, DHCP, Web-Server, FS-FileServer, Hyper-V |
Where-Object InstallState -eq 'Installed'
)
if ($forbiddenRoles.Count -gt 0) {
$forbiddenRoles | Select-Object Name, DisplayName |
Format-Table -AutoSize | Out-String | Write-Output
$failures.Add('One or more forbidden roles are installed.')
} else {
Write-Output 'No forbidden roles detected.'
}
Write-Output '=== Domain membership ==='
$computerSystem = Get-CimInstance Win32_ComputerSystem
Write-Output "PartOfDomain: $($computerSystem.PartOfDomain)"
Write-Output "Domain or workgroup: $($computerSystem.Domain)"
if ($computerSystem.PartOfDomain) { $failures.Add('Golden image clone is joined to a domain.') }
Write-Output '=== Network state ==='
$manualAddresses = @(
Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object { $_.InterfaceAlias -ne 'Loopback Pseudo-Interface 1' -and $_.PrefixOrigin -eq 'Manual' }
)
$persistentDefaultRoutes = @(
Get-NetRoute -PolicyStore PersistentStore -AddressFamily IPv4 -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue
)
Write-Output "Manual IPv4 addresses: $($manualAddresses.Count)"
Write-Output "Persistent default routes: $($persistentDefaultRoutes.Count)"
if ($manualAddresses.Count -gt 0) { $failures.Add('A manual IPv4 address remains in the image clone.') }
if ($persistentDefaultRoutes.Count -gt 0) { $failures.Add('A persistent default route remains in the image clone.') }
Write-Output '=== Pending reboot ==='
$pendingReboot = (
(Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') -or
(Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') -or
(Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations')
)
Write-Output "Pending reboot: $pendingReboot"
if ($pendingReboot) { $failures.Add('A reboot is pending.') }
Write-Output '=== Result ==='
if ($failures.Count -gt 0) {
$failures | ForEach-Object { Write-Error $_ }
exit 1
}
Write-Output 'Golden image validation passed.'
exit 0