Files
Invoke-OpenSSHConfiguration/Invoke-OpenSSHConfiguration.ps1
T
GraceSolutions 30baf5e70e Fix: Read session user certs from registry, not process identity
- Cert:\CurrentUser resolves to PROCESS identity, not session user
- Now reads directly from HKEY_USERS\{SID}\Software\Microsoft\SystemCertificates\
- Parses certificate blobs from Root and CA store registry keys
- Only attempts if CurrentUserSession is available and hive is loaded
- Skips invalid certificate blobs gracefully
2026-05-04 10:52:17 -04:00

1038 lines
74 KiB
PowerShell

#Requires -Version 5
<#
.SYNOPSIS
A short synopsis of of what the script does
.DESCRIPTION
A meaningful description of what the script does
.PARAMETER TaskSequenceVariables
One or more task sequence variable(s) to retrieve during task sequence execution.
If this parameter is not specified, all task sequence variable(s) will be stored into the variable 'TSVariableTable'.
Any task sequence variables that are new or have been updated will be saved back to the task sequence engine for futher usage.
$TSVariable.MyCustomVariableName = "MyCustomVariableValue"
$TSVariable.Make = "MyDeviceModel"
.PARAMETER LogDir
A valid folder path. If the folder does not exist, it will be created. This parameter can also be specified by the alias "LogPath".
.PARAMETER ContinueOnError
Ignore failures.
.EXAMPLE
Use this command to execute a VBSCript that will launch this powershell script automatically with the specified parameters. This is useful to avoid powershell execution complexities.
cscript.exe /nologo "%FolderPathContainingScript%\%ScriptName%.vbs" /SwitchParameter /ScriptParameter:"%ScriptParameterValue%" /ScriptParameterArray:"%ScriptParameterValue1%,%ScriptParameterValue2%"
wscript.exe /nologo "%FolderPathContainingScript%\%ScriptName%.vbs" /SwitchParameter /ScriptParameter:"%ScriptParameterValue%" /ScriptParameterArray:"%ScriptParameterValue1%,%ScriptParameterValue2%"
.EXAMPLE
powershell.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -File "%FolderPathContainingScript%\%ScriptName%.ps1" -SwitchParameter -ScriptParameter "%ScriptParameterValue%"
.EXAMPLE
powershell.exe -ExecutionPolicy Bypass -NonInteractive -NoProfile -NoLogo -WindowStyle Hidden -Command "& '%FolderPathContainingScript%\%ScriptName%.ps1' -ScriptParameter1 '%ScriptParameter1Value%' -ScriptParameter2 %ScriptParameter2Value% -SwitchParameter"
.NOTES
Any useful tidbits
.LINK
A useful link
#>
[CmdletBinding(SupportsShouldProcess=$True)]
Param
(
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('M')]
[ValidateSet('Client', 'Server')]
[String]$Mode,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('PKL')]
[String[]]$PublicKeyList,
[Parameter(Mandatory=$False)]
[Switch]$GenerateKeyPair,
[Parameter(Mandatory=$False)]
[ValidateSet('rsa', 'ecdsa', 'ed25519')]
[String]$KeyType = 'ed25519',
[Parameter(Mandatory=$False)]
[ValidateSet(256, 384, 521, 2048, 3072, 4096)]
[Int]$KeyBits,
[Parameter(Mandatory=$False)]
[Switch]$EnablePubkeyAuthentication,
[Parameter(Mandatory=$False)]
[Switch]$DisablePasswordAuthentication,
[Parameter(Mandatory=$False)]
[Switch]$EnableCertificateAuthentication,
[Parameter(Mandatory=$False)]
[Switch]$TrustWindowsCertificateAuthorities,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[String]$CAInclusionExpression,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[String]$CAExclusionExpression,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[String]$SFTPSubsystemPath,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Int]$SSHPort = 22,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('LogDir', 'LogPath')]
[System.IO.DirectoryInfo]$LogDirectory,
[Parameter(Mandatory=$False)]
[Switch]$ContinueOnError
)
Function Test-ProcessElevationStatus
{
$Identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$Principal = New-Object -TypeName 'System.Security.Principal.WindowsPrincipal' -ArgumentList ($Identity)
$Result = $Principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
Write-Output -InputObject ($Result)
}
Switch (Test-ProcessElevationStatus)
{
Default
{
Try
{
#region Define Default Action Preferences
$Script:InformationPreference = 'Continue'
$Script:DebugPreference = 'SilentlyContinue'
$Script:ErrorActionPreference = 'Stop'
$Script:VerbosePreference = 'SilentlyContinue'
$Script:WarningPreference = 'Continue'
$Script:ConfirmPreference = 'None'
$Script:WhatIfPreference = $False
#endregion
#region Set the default exit code for the script (By default, the script will exit with an exit code of 0)
[System.Environment]::ExitCode = 0
#endregion
#region Initialize Toolkit (This operation loads functions, modules, and variables into the current session, so if you do not see a variable defined below, it is because it is defined in the Toolkit)
Try
{
[System.IO.FileInfo]$ToolkitScriptPath = "$([System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Definition))\Toolkit\Toolkit.ps1"
. "$($ToolkitScriptPath.FullName)" -CallingScriptInvocationInfo ($MyInvocation) -CallingScriptParameterSetName ($PSCmdlet.ParameterSetName)
}
Catch
{
[System.Environment]::ExitCode = 6000
Throw
}
#endregion
#region Set default parameter values
Switch ($True)
{
{([System.String]::IsNullOrEmpty($Mode) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($Mode) -eq $True)}
{
}
}
#endregion
#region Perform Script Actions
$WindowsCapabilityInclusionExpression = 'OpenSSH.*'
$WriteLogMessage.Invoke(0, @("Attempting to retreive the list of Windows capabilities matching expression `"$($WindowsCapabilityInclusionExpression)`". Please Wait..."))
$WindowsCapabilityList = Get-WindowsCapability -Online -Name ($WindowsCapabilityInclusionExpression)
$WindowsCapabilityListCount = ($WindowsCapabilityList | Measure-Object).Count
$WriteLogMessage.Invoke(0, @("Located $($WindowsCapabilityListCount) Windows capabilities matching the specified expression."))
Switch ($WindowsCapabilityListCount -gt 0)
{
{($_ -eq $True)}
{
$WindowsCapabilityListCounter = 1
For ($WindowsCapabilityListIndex = 0; $WindowsCapabilityListIndex -lt $WindowsCapabilityListCount; $WindowsCapabilityListIndex++)
{
$WindowsCapabilityListItem = $WindowsCapabilityList[$WindowsCapabilityListIndex]
$WriteLogMessage.Invoke(0, @("Attempting to process windows capability $($WindowsCapabilityListCounter) of $($WindowsCapabilityListCount). Please Wait..."))
$WriteLogMessage.Invoke(0, @("DisplayName: $($WindowsCapabilityListItem.DisplayName)", "Name: $($WindowsCapabilityListItem.Name)", "State: $($WindowsCapabilityListItem.State)", "Description: $($WindowsCapabilityListItem.Description)"))
Switch ($WindowsCapabilityListItem.State -iin @('Installed'))
{
{($_ -eq $True)}
{
Try
{
$WriteLogMessage.Invoke(0, @("Attempting to remove windows capability $($WindowsCapabilityListCounter) of $($WindowsCapabilityListCount). Please Wait..."))
$Null = Remove-WindowsCapability -Online -Name ($WindowsCapabilityListItem.Name)
$WriteLogMessage.Invoke(0, @("Successfully removed windows capability $($WindowsCapabilityListCounter) of $($WindowsCapabilityListCount). Please Wait..."))
}
Catch
{
$WriteLogMessage.Invoke(2, @("Failed to remove windows capability $($WindowsCapabilityListCounter) of $($WindowsCapabilityListCount)."))
}
Finally
{
}
}
Default
{
$WriteLogMessage.Invoke(0, @("Skipping operations on windows capability $($WindowsCapabilityListCounter) of $($WindowsCapabilityListCount). Please Wait..."))
}
}
$WindowsCapabilityListCounter++
}
}
}
$GetInstalledSoftwareList = {Get-InstalledSoftware -FilterExpression {($_.DisplayName -imatch '(.*Open.*SSH.*)') -and ($_.DisplayName -inotmatch '(^.{0,0}$)')} -AllProperties -AllUserProfiles}
$InstalledSoftwareList = $GetInstalledSoftwareList.Invoke()
$InstalledSoftwareListCount = ($InstalledSoftwareList | Measure-Object).Count
Switch ($InstalledSoftwareListCount -eq 0)
{
{($_ -eq $True)}
{
#Determine the OS architecture for asset filtering (Win32, Win64, ARM64)
$OSArchitectureMap = @{
'AMD64' = 'Win64'
'x86' = 'Win32'
'ARM64' = 'ARM64'
'ARM' = 'ARM'
}
$ProcessorArchitecture = [System.Environment]::GetEnvironmentVariable('PROCESSOR_ARCHITECTURE')
$OSArchitectureFilter = $OSArchitectureMap[$ProcessorArchitecture]
#Build regex pattern to match MSI for the current architecture (e.g., "OpenSSH-Win64.*\.msi$")
$AssetPattern = "OpenSSH-$($OSArchitectureFilter).*\.msi$"
$GetGitRepositoryReleaseParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$GetGitRepositoryReleaseParameters.BaseURI = 'https://api.github.com'
$GetGitRepositoryReleaseParameters.RepositoryOwner = "PowerShell"
$GetGitRepositoryReleaseParameters.RepositoryName = "Win32-OpenSSH"
$GetGitRepositoryReleaseParameters.ReleaseTag = "latest"
$GetGitRepositoryReleaseParameters.AssetInclusionExpression = $AssetPattern
$GetGitRepositoryReleaseParameters.DestinationDirectory = [System.IO.Path]::Combine($Env:Windir, 'Temp', $CallingScriptPath.BaseName, 'Downloads')
$GetGitRepositoryReleaseParameters.Download = $True
$GetGitRepositoryReleaseParameters.Force = $False
$GetGitRepositoryReleaseParameters.ContinueOnError = $False
$GetGitRepositoryReleaseParameters.Verbose = $True
$GetGitRepositoryReleaseResult = Get-GitRepositoryRelease @GetGitRepositoryReleaseParameters
$GitRepositoryRelease = $GetGitRepositoryReleaseResult | Select-Object -First 1
$GitRepositoryReleaseAssetList = $GitRepositoryRelease.ReleaseInfo.AssetList | Where-Object {($_.DestinationPath.Extension -iin @('.msi')) -and ($_.DestinationPath.Exists -eq $True)}
$GitRepositoryReleaseAssetListCount = ($GitRepositoryReleaseAssetList | Measure-Object).Count
$GitRepositoryReleaseAssetListCounter = 1
For ($GitRepositoryReleaseAssetListIndex = 0; $GitRepositoryReleaseAssetListIndex -lt $GitRepositoryReleaseAssetListCount; $GitRepositoryReleaseAssetListIndex++)
{
$GitRepositoryReleaseAssetListItem = $GitRepositoryReleaseAssetList[$GitRepositoryReleaseAssetListIndex]
$ExecutionLogPath = [System.IO.FileInfo][System.IO.Path]::Combine($LogDirectory.FullName, "$($GitRepositoryReleaseAssetListItem.DestinationPath.BaseName).log")
$StartProcessWithOutputParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$StartProcessWithOutputParameters.FilePath = 'msiexec.exe'
$StartProcessWithOutputParameters.ArgumentList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$StartProcessWithOutputParameters.ArgumentList.Add('/i')
$StartProcessWithOutputParameters.ArgumentList.Add("`"$($GitRepositoryReleaseAssetListItem.DestinationPath.FullName)`"")
Switch ($True)
{
{([System.String]::IsNullOrEmpty($Mode) -eq $False) -and ([System.String]::IsNullOrWhiteSpace($Mode) -eq $False)}
{
$StartProcessWithOutputParameters.ArgumentList.Add("ADDLOCAL=$($Mode)")
}
}
$StartProcessWithOutputParameters.ArgumentList.Add('/qn')
$StartProcessWithOutputParameters.ArgumentList.Add('/norestart')
$StartProcessWithOutputParameters.ArgumentList.Add("/l*v `"$($ExecutionLogPath.FullName)`"")
$StartProcessWithOutputParameters.AcceptableExitCodeList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$StartProcessWithOutputParameters.AcceptableExitCodeList.Add('0')
$StartProcessWithOutputParameters.AcceptableExitCodeList.Add('3010')
$StartProcessWithOutputParameters.CreateNoWindow = $True
$StartProcessWithOutputParameters.ExecutionTimeout = [System.Timespan]::FromMinutes(15)
$StartProcessWithOutputParameters.ExecutionTimeoutInterval = [System.Timespan]::FromSeconds(5)
$StartProcessWithOutputParameters.SecureArgumentList = $False
$StartProcessWithOutputParameters.LogOutput = $True
$StartProcessWithOutputParameters.ContinueOnError = $False
$StartProcessWithOutputParameters.Verbose = $True
$StartProcessWithOutputParameters.ErrorAction = [System.Management.Automation.Actionpreference]::Stop
$MSIProductInfo = Get-MSIPropertyList -Path ($GitRepositoryReleaseAssetListItem.DestinationPath.FullName)
$WriteLogMessage.Invoke(0, @("Attempting to install release binary $($GitRepositoryReleaseAssetListCounter) of $($GitRepositoryReleaseAssetListCount). Please Wait...", "Manufacturer: $($MSIProductInfo.Manufacturer)", "ProductName: $($MSIProductInfo.ProductName)", "ProductVersion: $($MSIProductInfo.ProductVersion)", "Product Language: $($MSIProductInfo.ProductLanguage)"))
$StartProcessWithOutputResult = Start-ProcessWithOutput @StartProcessWithOutputParameters
$GitRepositoryReleaseAssetListCounter++
}
}
}
$InstalledSoftwareList = $GetInstalledSoftwareList.Invoke()
$InstalledSoftwareListCount = ($InstalledSoftwareList | Measure-Object).Count
Switch ($InstalledSoftwareListCount -gt 0)
{
{($_ -eq $True)}
{
#region Define OpenSSH Paths and Settings
[System.IO.DirectoryInfo]$OpenSSHProgramDataDirectory = [System.IO.Path]::Combine($Env:ProgramData, 'ssh')
[System.IO.FileInfo]$SSHDConfigPath = [System.IO.Path]::Combine($OpenSSHProgramDataDirectory.FullName, 'sshd_config')
[System.IO.FileInfo]$AdministratorsAuthorizedKeysPath = [System.IO.Path]::Combine($OpenSSHProgramDataDirectory.FullName, 'administrators_authorized_keys')
[System.Text.Encoding]$OpenSSHFileEncoding = [System.Text.Encoding]::ASCII
#endregion
#region Generate Key Pair
Switch ($GenerateKeyPair.IsPresent)
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Attempting to generate SSH key pair. Please Wait..."))
[System.IO.DirectoryInfo]$SSHKeyDirectory = [System.IO.Path]::Combine($OpenSSHProgramDataDirectory.FullName, 'keys')
Switch ($SSHKeyDirectory.Exists)
{
{($_ -eq $False)}
{
$WriteLogMessage.Invoke(0, @("Attempting to create directory `"$($SSHKeyDirectory.FullName)`". Please Wait..."))
$Null = [System.IO.Directory]::CreateDirectory($SSHKeyDirectory.FullName)
}
}
$KeyFileName = "ssh_host_$($KeyType)_key"
[System.IO.FileInfo]$PrivateKeyPath = [System.IO.Path]::Combine($SSHKeyDirectory.FullName, $KeyFileName)
[System.IO.FileInfo]$PublicKeyPath = [System.IO.Path]::Combine($SSHKeyDirectory.FullName, "$($KeyFileName).pub")
Switch ($PrivateKeyPath.Exists)
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("SSH key pair already exists at `"$($PrivateKeyPath.FullName)`". Skipping generation."))
}
Default
{
$SSHKeygenArgumentList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$SSHKeygenArgumentList.Add('-t')
$SSHKeygenArgumentList.Add($KeyType)
Switch (($Null -ine $KeyBits) -and ($KeyBits -gt 0))
{
{($_ -eq $True)}
{
$SSHKeygenArgumentList.Add('-b')
$SSHKeygenArgumentList.Add($KeyBits.ToString())
}
}
$SSHKeygenArgumentList.Add('-f')
$SSHKeygenArgumentList.Add("`"$($PrivateKeyPath.FullName)`"")
$SSHKeygenArgumentList.Add('-N')
$SSHKeygenArgumentList.Add('""')
$SSHKeygenArgumentList.Add('-C')
$SSHKeygenArgumentList.Add("`"$($Env:COMPUTERNAME)`"")
$StartProcessWithOutputParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$StartProcessWithOutputParameters.FilePath = 'ssh-keygen.exe'
$StartProcessWithOutputParameters.ArgumentList = $SSHKeygenArgumentList
$StartProcessWithOutputParameters.AcceptableExitCodeList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$StartProcessWithOutputParameters.AcceptableExitCodeList.Add('0')
$StartProcessWithOutputParameters.CreateNoWindow = $True
$StartProcessWithOutputParameters.ExecutionTimeout = [System.Timespan]::FromMinutes(2)
$StartProcessWithOutputParameters.ExecutionTimeoutInterval = [System.Timespan]::FromSeconds(5)
$StartProcessWithOutputParameters.LogOutput = $True
$StartProcessWithOutputParameters.ContinueOnError = $False
$StartProcessWithOutputParameters.Verbose = $True
$WriteLogMessage.Invoke(0, @("Generating $($KeyType) key pair at `"$($PrivateKeyPath.FullName)`". Please Wait..."))
$StartProcessWithOutputResult = Start-ProcessWithOutput @StartProcessWithOutputParameters
$WriteLogMessage.Invoke(0, @("SSH key pair generated successfully."))
}
}
}
}
#endregion
#region Add Public Keys
Switch (($Null -ine $PublicKeyList) -and ($PublicKeyList.Count -gt 0))
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Attempting to add $($PublicKeyList.Count) public key(s) to authorized_keys. Please Wait..."))
$ExistingAuthorizedKeys = New-Object -TypeName 'System.Collections.Generic.HashSet[System.String]'
Switch ($AdministratorsAuthorizedKeysPath.Exists)
{
{($_ -eq $True)}
{
$ExistingContent = [System.IO.File]::ReadAllLines($AdministratorsAuthorizedKeysPath.FullName)
ForEach ($Line In $ExistingContent)
{
Switch (([System.String]::IsNullOrEmpty($Line) -eq $False) -and ([System.String]::IsNullOrWhiteSpace($Line) -eq $False))
{
{($_ -eq $True)}
{
$Null = $ExistingAuthorizedKeys.Add($Line.Trim())
}
}
}
}
}
$KeysToAdd = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$PublicKeyListCounter = 1
ForEach ($PublicKey In $PublicKeyList)
{
$PublicKeyTrimmed = $PublicKey.Trim()
Switch ($ExistingAuthorizedKeys.Contains($PublicKeyTrimmed))
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Public key $($PublicKeyListCounter) of $($PublicKeyList.Count) already exists. Skipping."))
}
Default
{
$WriteLogMessage.Invoke(0, @("Adding public key $($PublicKeyListCounter) of $($PublicKeyList.Count)."))
$KeysToAdd.Add($PublicKeyTrimmed)
}
}
$PublicKeyListCounter++
}
Switch ($KeysToAdd.Count -gt 0)
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Appending $($KeysToAdd.Count) new public key(s) to `"$($AdministratorsAuthorizedKeysPath.FullName)`". Please Wait..."))
$Null = [System.IO.File]::AppendAllLines($AdministratorsAuthorizedKeysPath.FullName, $KeysToAdd, $OpenSSHFileEncoding)
$WriteLogMessage.Invoke(0, @("Public keys added successfully."))
}
}
}
}
#endregion
#region Export Windows CA Certificates to PEM
Switch ($TrustWindowsCertificateAuthorities.IsPresent)
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Attempting to export Windows CA certificates to PEM format. Please Wait..."))
$CertificateExpirationThreshold = Get-Date
#Include LocalMachine CA stores
$CertificateStorePathList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$CertificateStorePathList.Add('Cert:\LocalMachine\Root')
$CertificateStorePathList.Add('Cert:\LocalMachine\CA')
#Include CurrentUser CA stores from session user (not process user)
#Note: Cert:\CurrentUser resolves to PROCESS identity, not session user
#To access session user's certs, we read directly from HKEY_USERS\{SID}
$UserCertificates = New-Object -TypeName 'System.Collections.Generic.List[System.Security.Cryptography.X509Certificates.X509Certificate2]'
Switch (($Null -ine $CurrentUserSession) -and ([System.String]::IsNullOrEmpty($CurrentUserSession.SID) -eq $False))
{
{($_ -eq $True)}
{
#Check if user's registry hive is loaded under HKEY_USERS
$UserRegistryBasePath = "Registry::HKEY_USERS\$($CurrentUserSession.SID)"
Switch (Test-Path -Path $UserRegistryBasePath -ErrorAction SilentlyContinue)
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Accessing certificate store for session user: $($CurrentUserSession.NTAccount) [SID: $($CurrentUserSession.SID)]"))
#Read certificates directly from user's registry hive
$UserCertStoreNames = @('Root', 'CA')
ForEach ($StoreName In $UserCertStoreNames)
{
$UserCertRegistryPath = "$($UserRegistryBasePath)\Software\Microsoft\SystemCertificates\$($StoreName)\Certificates"
Switch (Test-Path -Path $UserCertRegistryPath -ErrorAction SilentlyContinue)
{
{($_ -eq $True)}
{
$CertSubKeys = Get-ChildItem -Path $UserCertRegistryPath -ErrorAction SilentlyContinue
ForEach ($CertSubKey In $CertSubKeys)
{
Try
{
$CertBlob = Get-ItemProperty -Path $CertSubKey.PSPath -Name 'Blob' -ErrorAction SilentlyContinue
Switch ($Null -ine $CertBlob.Blob)
{
{($_ -eq $True)}
{
$X509Cert = New-Object -TypeName 'System.Security.Cryptography.X509Certificates.X509Certificate2' -ArgumentList @(,[System.Byte[]]$CertBlob.Blob)
$UserCertificates.Add($X509Cert)
}
}
}
Catch
{
#Skip invalid certificate blobs
}
}
}
}
}
$WriteLogMessage.Invoke(0, @("Found $($UserCertificates.Count) certificate(s) in session user's store."))
}
Default
{
$WriteLogMessage.Invoke(1, @("User registry hive not loaded for SID: $($CurrentUserSession.SID). Skipping user certificates."))
}
}
}
}
$CACertificates = New-Object -TypeName 'System.Collections.Generic.List[System.Security.Cryptography.X509Certificates.X509Certificate2]'
#Add certificates from LocalMachine stores
$LocalMachineCerts = Get-ChildItem -Path $CertificateStorePathList -ErrorAction SilentlyContinue | Where-Object {($_.NotAfter -gt $CertificateExpirationThreshold) -and ($_.Extensions | Where-Object {($_.Oid.FriendlyName -iin @('Basic Constraints')) -and ($_.CertificateAuthority -eq $True)})}
Switch ($Null -ine $LocalMachineCerts)
{
{($_ -eq $True)}
{
$CACertificates.AddRange([System.Security.Cryptography.X509Certificates.X509Certificate2[]]$LocalMachineCerts)
}
}
#Add user certificates (filter for CA certs)
Switch ($UserCertificates.Count -gt 0)
{
{($_ -eq $True)}
{
$FilteredUserCerts = $UserCertificates | Where-Object {($_.NotAfter -gt $CertificateExpirationThreshold) -and ($_.Extensions | Where-Object {($_.Oid.FriendlyName -iin @('Basic Constraints')) -and ($_.CertificateAuthority -eq $True)})}
Switch ($Null -ine $FilteredUserCerts)
{
{($_ -eq $True)}
{
$CACertificates.AddRange([System.Security.Cryptography.X509Certificates.X509Certificate2[]]$FilteredUserCerts)
}
}
}
}
#Dedupe by thumbprint
$CACertificates = $CACertificates | Sort-Object -Property @('Thumbprint') -Unique
#region Apply CA Inclusion Filter
Switch (([System.String]::IsNullOrEmpty($CAInclusionExpression) -eq $False) -and ([System.String]::IsNullOrWhiteSpace($CAInclusionExpression) -eq $False))
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Applying CA inclusion filter: `"$($CAInclusionExpression)`""))
$CACertificates = $CACertificates | Where-Object {($_.Subject -imatch $CAInclusionExpression) -or ($_.Thumbprint -imatch $CAInclusionExpression)}
}
}
#endregion
#region Apply CA Exclusion Filter
Switch (([System.String]::IsNullOrEmpty($CAExclusionExpression) -eq $False) -and ([System.String]::IsNullOrWhiteSpace($CAExclusionExpression) -eq $False))
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Applying CA exclusion filter: `"$($CAExclusionExpression)`""))
$CACertificates = $CACertificates | Where-Object {($_.Subject -inotmatch $CAExclusionExpression) -and ($_.Thumbprint -inotmatch $CAExclusionExpression)}
}
}
#endregion
$CACertificatesCount = ($CACertificates | Measure-Object).Count
$WriteLogMessage.Invoke(0, @("Found $($CACertificatesCount) CA certificate(s) matching criteria."))
Switch ($CACertificatesCount -gt 0)
{
{($_ -eq $True)}
{
$ConvertFromCertificateToPEMParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$ConvertFromCertificateToPEMParameters.CertificateList = $CACertificates
$ConvertFromCertificateToPEMParameters.ExportRootDirectory = $OpenSSHProgramDataDirectory.FullName
$ConvertFromCertificateToPEMParameters.CreateCABundle = $True
$ConvertFromCertificateToPEMParameters.CABundleFileName = 'OpenSSH-TrustedCertificateAuthorities-LocalMachine.pem'
$ConvertFromCertificateToPEMParameters.ContinueOnError = $False
$ConvertFromCertificateToPEMParameters.Verbose = $True
$ConvertFromCertificateToPEMResult = ConvertFrom-CertificateToPEM @ConvertFromCertificateToPEMParameters
$CABundleResult = $ConvertFromCertificateToPEMResult | Where-Object {($_.Type -ieq 'CABundle')}
Switch ($Null -ine $CABundleResult)
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("CA bundle exported successfully to `"$($CABundleResult.Path)`" with $($CABundleResult.CertificateCount) certificate(s)."))
}
}
}
}
}
}
#endregion
#region Configure sshd_config
Switch ($SSHDConfigPath.Exists)
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Attempting to configure sshd_config at `"$($SSHDConfigPath.FullName)`". Please Wait..."))
#region Backup Existing Configuration
$SSHDConfigBackupPath = [System.IO.FileInfo][System.IO.Path]::Combine($OpenSSHProgramDataDirectory.FullName, "sshd_config.backup.$($GetCurrentDateTimeFileFormat.Invoke())")
$WriteLogMessage.Invoke(0, @("Creating backup of sshd_config to `"$($SSHDConfigBackupPath.FullName)`". Please Wait..."))
$Null = [System.IO.File]::Copy($SSHDConfigPath.FullName, $SSHDConfigBackupPath.FullName, $True)
$WriteLogMessage.Invoke(0, @("Backup created successfully."))
#region Cleanup Old Backups (Keep Last 3)
$BackupFilePattern = 'sshd_config.backup.*'
$BackupsToKeep = 3
$ExistingBackups = Get-ChildItem -Path $OpenSSHProgramDataDirectory.FullName -Filter $BackupFilePattern -File -ErrorAction SilentlyContinue | Sort-Object -Property @('LastWriteTime') -Descending
Switch (($ExistingBackups | Measure-Object).Count -gt $BackupsToKeep)
{
{($_ -eq $True)}
{
$BackupsToRemove = $ExistingBackups | Select-Object -Skip $BackupsToKeep
ForEach ($BackupToRemove In $BackupsToRemove)
{
$WriteLogMessage.Invoke(0, @("Removing old backup `"$($BackupToRemove.Name)`"."))
$Null = [System.IO.File]::Delete($BackupToRemove.FullName)
}
}
}
#endregion
#endregion
$SSHDConfigContent = [System.IO.File]::ReadAllText($SSHDConfigPath.FullName)
$SSHDConfigModified = $False
#region Enable PubkeyAuthentication
Switch ($EnablePubkeyAuthentication.IsPresent -eq $True)
{
{($_ -eq $True)}
{
$PubkeyAuthPattern = '(?m)^#?\s*PubkeyAuthentication\s+.*$'
$PubkeyAuthReplacement = 'PubkeyAuthentication yes'
Switch ($SSHDConfigContent -imatch $PubkeyAuthPattern)
{
{($_ -eq $True)}
{
$SSHDConfigContent = [System.Text.RegularExpressions.Regex]::Replace($SSHDConfigContent, $PubkeyAuthPattern, $PubkeyAuthReplacement)
$SSHDConfigModified = $True
$WriteLogMessage.Invoke(0, @("Enabled PubkeyAuthentication."))
}
Default
{
$SSHDConfigContent = $SSHDConfigContent + "`r`n$($PubkeyAuthReplacement)"
$SSHDConfigModified = $True
$WriteLogMessage.Invoke(0, @("Added PubkeyAuthentication setting."))
}
}
}
}
#endregion
#region Disable PasswordAuthentication
Switch ($DisablePasswordAuthentication.IsPresent -eq $True)
{
{($_ -eq $True)}
{
$PasswordAuthPattern = '(?m)^#?\s*PasswordAuthentication\s+.*$'
$PasswordAuthReplacement = 'PasswordAuthentication no'
Switch ($SSHDConfigContent -imatch $PasswordAuthPattern)
{
{($_ -eq $True)}
{
$SSHDConfigContent = [System.Text.RegularExpressions.Regex]::Replace($SSHDConfigContent, $PasswordAuthPattern, $PasswordAuthReplacement)
$SSHDConfigModified = $True
$WriteLogMessage.Invoke(0, @("Disabled PasswordAuthentication."))
}
Default
{
$SSHDConfigContent = $SSHDConfigContent + "`r`n$($PasswordAuthReplacement)"
$SSHDConfigModified = $True
$WriteLogMessage.Invoke(0, @("Added PasswordAuthentication setting (disabled)."))
}
}
}
}
#endregion
#region Configure SFTP Subsystem (Default)
$SFTPPath = $SFTPSubsystemPath
Switch (([System.String]::IsNullOrEmpty($SFTPPath) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($SFTPPath) -eq $True))
{
{($_ -eq $True)}
{
$SFTPPath = 'sftp-server.exe'
}
}
$SubsystemPattern = '(?m)^#?\s*Subsystem\s+sftp\s+.*$'
$SubsystemReplacement = "Subsystem sftp $($SFTPPath)"
Switch ($SSHDConfigContent -imatch $SubsystemPattern)
{
{($_ -eq $True)}
{
$CurrentSubsystemMatch = [System.Text.RegularExpressions.Regex]::Match($SSHDConfigContent, $SubsystemPattern)
Switch ($CurrentSubsystemMatch.Value -ine $SubsystemReplacement)
{
{($_ -eq $True)}
{
$SSHDConfigContent = [System.Text.RegularExpressions.Regex]::Replace($SSHDConfigContent, $SubsystemPattern, $SubsystemReplacement)
$SSHDConfigModified = $True
$WriteLogMessage.Invoke(0, @("Configured SFTP subsystem: $($SFTPPath)"))
}
}
}
Default
{
$SSHDConfigContent = $SSHDConfigContent + "`r`n$($SubsystemReplacement)"
$SSHDConfigModified = $True
$WriteLogMessage.Invoke(0, @("Added SFTP subsystem configuration."))
}
}
#endregion
#region Configure TrustedUserCAKeys for Certificate Authentication
Switch (($EnableCertificateAuthentication.IsPresent -eq $True) -and ($TrustWindowsCertificateAuthorities.IsPresent -eq $True))
{
{($_ -eq $True)}
{
$CABundleFileName = 'OpenSSH-TrustedCertificateAuthorities-LocalMachine.pem'
[System.IO.FileInfo]$CABundlePath = [System.IO.Path]::Combine($OpenSSHProgramDataDirectory.FullName, $CABundleFileName)
#Use __PROGRAMDATA__ variable for sshd_config (portable path)
$CABundleConfigPath = "__PROGRAMDATA__/ssh/$($CABundleFileName)"
Switch ($CABundlePath.Exists)
{
{($_ -eq $True)}
{
$TrustedCAKeysPattern = '(?m)^#?\s*TrustedUserCAKeys\s+.*$'
$TrustedCAKeysReplacement = "TrustedUserCAKeys $($CABundleConfigPath)"
Switch ($SSHDConfigContent -imatch $TrustedCAKeysPattern)
{
{($_ -eq $True)}
{
$SSHDConfigContent = [System.Text.RegularExpressions.Regex]::Replace($SSHDConfigContent, $TrustedCAKeysPattern, $TrustedCAKeysReplacement)
$SSHDConfigModified = $True
$WriteLogMessage.Invoke(0, @("Configured TrustedUserCAKeys to `"$($CABundleConfigPath)`"."))
}
Default
{
$SSHDConfigContent = $SSHDConfigContent + "`r`n$($TrustedCAKeysReplacement)"
$SSHDConfigModified = $True
$WriteLogMessage.Invoke(0, @("Added TrustedUserCAKeys setting."))
}
}
}
}
}
}
#endregion
#region Write sshd_config if Modified
Switch ($SSHDConfigModified -eq $True)
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Writing updated sshd_config. Please Wait..."))
[System.IO.File]::WriteAllText($SSHDConfigPath.FullName, $SSHDConfigContent, $OpenSSHFileEncoding)
$WriteLogMessage.Invoke(0, @("sshd_config updated successfully."))
#region Validate Configuration
$WriteLogMessage.Invoke(0, @("Validating sshd_config with sshd -t. Please Wait..."))
$SSHDExePath = [System.IO.FileInfo][System.IO.Path]::Combine($Env:ProgramFiles, 'OpenSSH', 'sshd.exe')
Switch ($SSHDExePath.Exists -eq $True)
{
{($_ -eq $True)}
{
$ValidateConfigParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$ValidateConfigParameters.FilePath = $SSHDExePath.FullName
$ValidateConfigParameters.ArgumentList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$ValidateConfigParameters.ArgumentList.Add('-t')
$ValidateConfigParameters.AcceptableExitCodeList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$ValidateConfigParameters.AcceptableExitCodeList.Add('0')
$ValidateConfigParameters.CreateNoWindow = $True
$ValidateConfigParameters.ExecutionTimeout = [System.Timespan]::FromSeconds(30)
$ValidateConfigParameters.ExecutionTimeoutInterval = [System.Timespan]::FromSeconds(5)
$ValidateConfigParameters.LogOutput = $True
$ValidateConfigParameters.ContinueOnError = $True
$ValidateConfigParameters.Verbose = $True
$ValidateConfigResult = Start-ProcessWithOutput @ValidateConfigParameters
Switch ($ValidateConfigResult.ExitCode -eq 0)
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("sshd_config validation passed."))
$WriteLogMessage.Invoke(0, @("Attempting to restart sshd service to apply configuration changes. Please Wait..."))
$Null = Restart-Service -Name 'sshd' -Force -ErrorAction SilentlyContinue
$WriteLogMessage.Invoke(0, @("sshd service restarted."))
}
Default
{
$WriteLogMessage.Invoke(2, @("sshd_config validation failed. Restoring backup..."))
$Null = [System.IO.File]::Copy($SSHDConfigBackupPath.FullName, $SSHDConfigPath.FullName, $True)
$WriteLogMessage.Invoke(0, @("Backup restored. Configuration changes reverted."))
}
}
}
Default
{
$WriteLogMessage.Invoke(1, @("sshd.exe not found at `"$($SSHDExePath.FullName)`". Skipping validation."))
$WriteLogMessage.Invoke(0, @("Attempting to restart sshd service to apply configuration changes. Please Wait..."))
$Null = Restart-Service -Name 'sshd' -Force -ErrorAction SilentlyContinue
$WriteLogMessage.Invoke(0, @("sshd service restarted."))
}
}
#endregion
}
Default
{
$WriteLogMessage.Invoke(0, @("No changes required to sshd_config."))
}
}
#endregion
}
}
#endregion
#region Service Management (Automatic)
$SSHDService = Get-Service -Name 'sshd' -ErrorAction SilentlyContinue
Switch ($Null -ine $SSHDService)
{
{($_ -eq $True)}
{
#region Configure Automatic Startup
Switch ($SSHDService.StartType -ine 'Automatic')
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Configuring sshd service to start automatically. Please Wait..."))
$Null = Set-Service -Name 'sshd' -StartupType Automatic -ErrorAction SilentlyContinue
$WriteLogMessage.Invoke(0, @("sshd service configured for automatic startup."))
}
}
#endregion
#region Start Service
Switch ($SSHDService.Status -ine 'Running')
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Starting sshd service. Please Wait..."))
$Null = Start-Service -Name 'sshd' -ErrorAction SilentlyContinue
$WriteLogMessage.Invoke(0, @("sshd service started."))
}
}
#endregion
}
}
#endregion
#region Windows Firewall Configuration (Automatic)
$FirewallRuleName = 'OpenSSH-Server-In-TCP'
$FirewallProfiles = @('Domain', 'Private', 'Public')
ForEach ($FirewallProfile In $FirewallProfiles)
{
$ExistingRule = Get-NetFirewallRule -Name "$($FirewallRuleName)-$($FirewallProfile)" -ErrorAction SilentlyContinue
Switch ($Null -eq $ExistingRule)
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Creating Windows Firewall rule for profile `"$($FirewallProfile)`". Please Wait..."))
$FirewallRuleParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$FirewallRuleParameters.Add('Name', "$($FirewallRuleName)-$($FirewallProfile)")
$FirewallRuleParameters.Add('DisplayName', "OpenSSH SSH Server ($($FirewallProfile))")
$FirewallRuleParameters.Add('Description', "Inbound rule for OpenSSH SSH Server on TCP port $($SSHPort) for $($FirewallProfile) profile")
$FirewallRuleParameters.Add('Enabled', 'True')
$FirewallRuleParameters.Add('Direction', 'Inbound')
$FirewallRuleParameters.Add('Protocol', 'TCP')
$FirewallRuleParameters.Add('LocalPort', $SSHPort)
$FirewallRuleParameters.Add('Profile', $FirewallProfile)
$FirewallRuleParameters.Add('Action', 'Allow')
$FirewallRuleParameters.Add('ErrorAction', 'SilentlyContinue')
$Null = New-NetFirewallRule @FirewallRuleParameters
$WriteLogMessage.Invoke(0, @("Windows Firewall rule created for profile `"$($FirewallProfile)`"."))
}
Default
{
Switch (($ExistingRule.Enabled -eq $False) -or ($ExistingRule.LocalPort -ne $SSHPort))
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Updating Windows Firewall rule for profile `"$($FirewallProfile)`". Please Wait..."))
$Null = Set-NetFirewallRule -Name "$($FirewallRuleName)-$($FirewallProfile)" -Enabled 'True' -LocalPort $SSHPort -ErrorAction SilentlyContinue
$WriteLogMessage.Invoke(0, @("Windows Firewall rule updated for profile `"$($FirewallProfile)`"."))
}
}
}
}
}
#endregion
}
}
#endregion
}
Catch
{
#region Perform error handling actions
$ErrorHandlingDefinition.Invoke($Error[0], 2, $ContinueOnError.IsPresent)
#endregion
}
Finally
{
#region Perform finalization actions
$FinalizationActions.Invoke()
#endregion
}
}
{($_ -eq $False)}
{
[System.IO.FileInfo]$ScriptPath = "$($MyInvocation.MyCommand.Definition)"
#$CurrentExecutionPolicy = Get-ExecutionPolicy -Scope Process
$CurrentExecutionPolicy = 'Bypass'
$ArgumentList = New-Object -TypeName 'System.Collections.Generic.List[String]'
$ArgumentList.Add("-ExecutionPolicy $($CurrentExecutionPolicy)")
$ArgumentList.Add('-NonInteractive')
$ArgumentList.Add('-NoProfile')
$ArgumentList.Add('-NoLogo')
$ArgumentList.Add('-NoExit')
$ArgumentList.Add('-Command')
$ArgumentList.Add("`"`& {. `"$($ScriptPath.FullName)`"")
$MyInvocation.UnboundArguments.GetEnumerator() | ForEach-Object {$ArgumentList.Add("-$($_.Key) @($($_.Value | ForEach-Object {`"$($_)`"}))")}
$PSBoundParameters.GetEnumerator() | ForEach-Object {$ArgumentList.Add("-$($_.Key) @($($_.Value | ForEach-Object {`"$($_)`"}))")}
$ArgumentListTargetIndex = $ArgumentList.Count - 1
$ArgumentListIndexItem = $ArgumentList[$ArgumentListTargetIndex]
$Null = $ArgumentList.RemoveAt($ArgumentListTargetIndex)
$Null = $ArgumentList.Insert($ArgumentListTargetIndex, ($ArgumentListIndexItem + ';'))
$ArgumentList.Add("[System.Environment]::Exit((`$LASTEXITCODE -Bor [Int](-Not `$? -And -Not `$LASTEXITCODE)))}`"")
$ScriptInterpreterList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$ScriptInterpreterList.Add('powershell.exe')
$ScriptInterpreterList.Add('pwsh.exe')
:ScriptInterpreterListLoop ForEach ($ScriptInterpreter In $ScriptInterpreterList)
{
$ScriptInterpreterObject = Try {Get-Command -Name ($ScriptInterpreter) -ErrorAction SilentlyContinue} Catch {$Null}
Switch ($Null -ine $ScriptInterpreterObject)
{
{($_ -eq $True)}
{
$Null = Start-Process -FilePath ($ScriptInterpreterObject.Path) -WorkingDirectory "$($Env:Temp.TrimEnd('\'))" -ArgumentList ($ArgumentList.ToArray()) -WindowStyle Normal -Verb RunAs -PassThru
Break ScriptInterpreterListLoop
}
}
}
}
}