My PS scripts for installation automation

I built a set of scripts to automate renewal and installation of certificates. I decided to post my work in progress, hoping I might be able to help some people…

Disclaimer: I’m by no means an expert in PowerShell… :slight_smile:

Some of the features I needed for installing cettificates are :

  • Replacing certificates in running systems should be done during quiet hours (e.g. > 2 am).
  • Some certificates need to installed on more than 1 machine.
  • Some certificates need to installed on more than 1 machine which are dependent.

Requirements:

The scripts all run from a single computer, using remote powershell (or ssh) sessions. So make sure remoting works (winrm /quickconfig).

Configure Trustedhosts to include any servers you want to connect with:

Set-Item WSMan:\localhost\Client\TrustedHosts -Value …

  • If CredSSP is required (e.g. for adfs) enable credential delegation to those machines:

Enable-WSManCredSSP -Role Client -DelegateComputer … -force

The scripts use 2 modules from the PSGallery: CredentialManager and Posh-SSH. Install these prior to using/editing the scripts.

Install-Module -Name CredentialManager
Install-Module -Name Posh-SSH

The scrips are separated in 2 parts.

  • Certificate renewal: The first script (CTW-CopyCert2Queue.ps1) is run as a post script after certificate renewal. It copies the certificate to a set location with a predefined format, moves old certificates to an archive folder and sends an e-mail with some details about the new certificate.

  • Certificate installation: The second script (SchedTask-ProcessCertificates.ps1) is run by a scheduled task. This script will run all certificate install scripts in a sub folder. Within these certificate install scripts all handling of a specific certificate is done.

Scheduled task:

Create a scheduled task to run at a set interval, e.g. once a week on sunday @ 4 AM. Run the task using a regular (local user). Have the task start SchedTask-ProcessCertificates.ps1.

Credentials:

Log in as the regular (local) user used for the scheduled task. Start Credential Manager and add the required credentials of the server to connect to as generic credentials. The “internet or network address” is the name used to reference the credentials in the scripts.

Folder structure:

C:\
- CTW\
  - PFX\            # New certificates are stored here
    - _archive\     # New certificates are stored here
- Scripts\          # Main scripts
  - _scripts\       # Certificate install scripts
    - _files\       # Includes and openssl

The _files folder contains the include file(s) and the openssl files (openssl.exe, openssl.cfg, libcrypto-1_1-x64.dll and libssl-1_1-x64.dll).

Certificate Naming:

Use the fqdn (I use ‘wc’ for * in case of a wildcard certificate) as the name of the managed certificate in Certify SSL Manager. This name is used by the scripts.

1 Like

C:\CTW\Scripts\CTW-CopyCert2Queue.ps1

# ##################################################################################
#
# CTW-CopyCert2Queue.ps1
# 
# ##################################################################################
#
# Get newly created certificate, rename and copy to queue for further processing.
# Generates filename as "<item name>_yyyymmdd-yyyymmdd.pfx" where item name is the
# name as defined in CTW. Creates xml file with new and previous thumbprint.
# Moves previous certificate files to archive location.
#
# ##################################################################################

Param($Result)

# ----------------------------------------------------------------------------------
# Global vars
# ----------------------------------------------------------------------------------

$CertificateStorePath = "C:\CTW\PFX"
$CertificateArchivePath = "C:\CTW\PFX\_archive"

$MailServer = 'smtp.domain.com'
$MailFrom = '[email protected]'
$MailTo = '[email protected]'

# ----------------------------------------------------------------------------------
# Handle certificate
# ----------------------------------------------------------------------------------

If ($Result.IsSuccess) {

    $CertificateName = $Result.ManagedItem.Name
    $CertificateDateStart = $Result.ManagedItem.DateStart
    $CertificateDateEnd = $Result.ManagedItem.DateExpiry
    $CertificatePath = $Result.ManagedItem.CertificatePath
    $CertificateThumbPrint = $Result.ManagedItem.CertificateThumbPrintprintHash
    $CertificatePreviousThumbPrint = $Result.ManagedItem.CertificatePreviousThumbprintHash

    # Generate dated filename
    $CertificateNewFileName = $CertificateName + "_" + $CertificateDateStart.ToString("yyyyMMdd") + "-" + $CertificateDateEnd.ToString("yyyyMMdd")

    # move old certificates to archive
    Move-Item -Path ("$CertificateStorePath\$CertificateName*.*") -Destination $CertificateArchivePath -Force

    # Copy pfx to queue
    Copy-Item -Path $CertificatePath -Destination ("$CertificateStorePath\$CertificateNewFileName.pfx") -Force

    # Write certificate info to file
    $output = @"
<?xml version="1.0"?>
<certs>
    <cert name=`"$CertificateName`">
        <start>$($CertificateDateStart.ToString("yyyyMMdd"))</start>
        <end>$($CertificateDateEnd.ToString("yyyyMMdd"))</end>
        <thumb>$CertificateThumbPrint</thumb>
        <thumbprevious>$CertificatePreviousThumbPrint</thumbprevious>
    </cert>
</certs>
"@
    $output | Out-File -FilePath "$CertificateStorePath\$CertificateNewFileName.xml"

    # Send e-mail notification - new certificate from Lets Encrypt
    $ResultMsg = $Result.Message
    $emailSubject = "Renewal of $CertificateName succeeded"
    $emailBody = @"
<p>Renewal of $CertificateName succeeded.</p>
<table>
<tr><td>Start time:</td><td>$($CertificateDateStart.ToString("dd/MM/yyyy HH:mm"))</td></tr>
<tr><td>End time:</td><td>$($CertificateDateEnd.ToString("dd/MM/yyyy HH:mm"))</td></tr>
<tr><td>Filepath:</td><td>$CertificateStorePath\$CertificateNewFileName.pfx</td></tr>
<tr><td>Thumbprint:</td><td>$CertificateThumbPrint</td></tr>
</table>
<p>Message: $ResultMsg</p><br />
"@
    Send-MailMessage -From $MailFrom -To $MailTo -Subject $emailSubject -Body $emailBody -BodyAsHtml -SmtpServer $MailServer

} else {

    # Send e-mail of renewal failure
    $CertificateName = $Result.ManagedItem.Name
    $ResultMsg = $Result.Message
    $emailSubject = "Renewal of $CertificateName failed"
    $emailBody = "<br /><p>Failed renewal of $CertificateName</p><p>Message: $ResultMsg</p><br />"
    Send-MailMessage -From $MailFrom -To $MailTo -Subject $emailSubject -Body $emailBody -BodyAsHtml -SmtpServer $MailServer

}

C:\CTW\Scripts\SchedTask-ProcessCertificates.ps1

# ##################################################################################
#
# SchedTask-ProcessCertificates.ps1
#
# ##################################################################################
#
# Description:
#
#   Executes all ps1 scripts in the CertificateScripts folder. 
#
# Install:
#
#   Run this script from a scheduled task. Configure the task to run using a
#   regular (local) user. The certificate install scripts use credentials stored in
#   the credential store of this user account.
#
# ##################################################################################

$CertificateScripts = Get-ChildItem -Path "$PSScriptRoot\_scripts" -Filter "*.ps1" -File 
ForEach ($Script In $CertificateScripts) 
{
    &($Script.FullName)
}

C:\CTW\Scripts\_scripts\_files\_include.ps1

# ##################################################################################
#
# _include.ps1
#
# ##################################################################################
#
# Description:
#
#   Contains functions used in the certificate install scripts:
#   - Remote Powershell 
#   - Remote SSH
#   - Convert PFX to PEM
#   - Logging
#
# ##################################################################################

# ============================================================
# Init
# ============================================================

# Import required modules
#   Install-Module CredentialManager
#   Install-Module Posh-SSH
Import-Module CredentialManager
Import-Module Posh-SSH

# Define Logfile path if not set yet
If (!(Test-Path -Path Variable:LogFile)) { $LogFile = "$env:temp\certificate.log"}


# ============================================================
# Function: LogtoFile
# ============================================================

<#
.SYNOPSIS
Writes text to a log file.

.DESCRIPTION
Writes text to a log file. 
Text content is preceeded by date and time. New entries are appended if specified log file exists.

.PARAMETER LogFullPath
Full path to log file.

.PARAMETER LogText
Text to write to log file.

.INPUTS
None.

.OUTPUTS
None.

.EXAMPLE
LogToFile 'c:\temp\logfile.txt' 'Example text to write to log file.'

.EXAMPLE
LogToFile -LogFullPath 'c:\temp\logfile.txt' -LogText 'Example text to write to log file.'

.NOTES
Creates log file if it doesn't exist yet, and append any text if it does exist.
Every line is preceeded by date (yyyy-mm-dd) and time (hh:mm:ss) seperated by tab.

#>
Function LogToFile {
    Param (
        [Parameter(Mandatory=$true,Position=0)]
        [string]$LogFullPath,
        [Parameter(Mandatory=$true,Position=1)]
        [string]$LogText
    )
    $logstring = $(Get-Date -Format("yyyy-MM-dd`tHH:mm:ss")) + "`t" + $LogText
    $logstring | Out-File -FilePath $LogFullPath -Append
}


# ============================================================
# Function: OpenRemoteSession
# ============================================================

<#
.SYNOPSIS
Opens PowerShell session to remote server.

.DESCRIPTION
Opens PowerShell session to remote server.

.PARAMETER CredentialTargetName
Name of credential stored in Windows Credential store (Generic credential).

.PARAMETER Server
Server (fqdn) to connect to.

.PARAMETER CredSSP
Use CredSSP with the remote session (passes credentials to remote session, required for
certain programs/features (e.g. ADFS)).

.INPUTS
None.

.OUTPUTS
Returns a PSSession object if remote connection was setup succesfully or $null if unsuccesfull.

.EXAMPLE
OpenRemoteSession -Server 'server.domain.com' -CredentialTargetName 'ServerCredentials' -CredSSP

#>
Function OpenRemoteSession {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","CredentialTargetName")]
    Param (
        [Parameter(Mandatory=$true)]
        [string]$CredentialTargetName,
        [Parameter(Mandatory=$true)]
        [string]$Server,
        [switch]$CredSSP
    )
    $Credentials = Get-StoredCredential -Target $CredentialTargetName
    If ( $Credentials ) {
        # Open remote session. If CredSSP switch is not set, use Negotiated authentication
        If ( $CredSSP ) {
            $Session = New-PSSession -ComputerName $Server -Credential $Credentials -Authentication Credssp
        } else {
            $Session = New-PSSession -ComputerName $Server -Credential $Credentials -Authentication Negotiate
        }
        # Check if session was started, return session object if succesfull
        If ( $Session ) {
            LogToFile $LogFile "Remote PSSession to $Server created."
            Return $Session
        } Else {
            LogToFile $LogFile "Error: unable to create a remote PSSession to $Server."
            Return $null
        }
    } Else {
        LogToFile $LogFile "Credentials '$CredentialTargetName' not found."
        Return $null
    }
}


# ============================================================
# Function: CloseRemoteSession
# ============================================================

<#
.SYNOPSIS
Closes a PowerShell session to a remote server.

.DESCRIPTION
Closes a PowerShell session to a remote server.

.PARAMETER Session
PSSession object to remote session.

.INPUTS
None.

.OUTPUTS
None.

.EXAMPLE
CloseRemoteSession -Session $RemoteSession

#>
Function CloseRemoteSession {
    Param (
        [Parameter(Mandatory=$true)]
        [System.Management.Automation.Runspaces.PSSession]$Session
    )
    $Server = $Session.ComputerName
    Remove-PSSession -Session $Session
    LogToFile $LogFile "Remote PSSession to $Server closed."
}


# ============================================================
# Function: InstallCertificateRemote
# ============================================================

<#
.SYNOPSIS
Imports a certificate on the remote server.

.DESCRIPTION
Imports a certificate on the remote server.
The certificate is copied to the remote server, and then imported into the remote
machine's personal certificate store.

.PARAMETER CertificateFullPath
Full path to local certificate file.

.PARAMETER Session
PSSession object to remote session.

.INPUTS
None.

.OUTPUTS
Returns $true if succesfull, $false if failed.

.EXAMPLE
InstallCertificateRemote -CertificateFullPath 'c:\temp\certificate.pfx' -Session $RemoteSession

#>
Function InstallCertificateRemote {
    Param (
        [Parameter(Mandatory=$true)]
        [string]$CertificateFullPath,
        [Parameter(Mandatory=$true)]
        [System.Management.Automation.Runspaces.PSSession]$Session
    )
    # Get certificate filename
    $CertificateFileName = Split-Path -Path $CertificateFullPath -Leaf

    # Return temporary folder (%temp\CTW) on remote host, create folder if not exists yet
    $RemoteCommand = {
        if (!(Test-Path -Path "$env:TEMP\CTW")) {
            New-Item -Path "$env:TEMP\CTW" -ItemType Directory -Force
        }
        Return "$env:TEMP\CTW".ToString()
    }
    $TempLocation = Invoke-Command -Session $Session -ScriptBlock $RemoteCommand

    # Copy certificate to remote server
    Copy-Item $CertificateFullPath -ToSession $Session -Destination $TempLocation -Force
    LogToFile $LogFile "Copied $CertificateFileName to $($Session.ComputerName):$TempLocation"

    # Execute remote certificate import
    $RemoteCommand = {
        Import-PfxCertificate -FilePath "$Using:TempLocation\$Using:CertificateFileName" -CertStoreLocation Cert:\LocalMachine\My
    }
    $ImportedCertificate = Invoke-Command -Session $Session -ScriptBlock $RemoteCommand

    # Check if import was succesfull
    If ( $ImportedCertificate ) {
        LogToFile $LogFile "Imported $CertificateFileName in Cert:\LocalMachine\My store."
        # # Cleanup
        # $RemoteCommand = { Remove-Item -Path "$Using:TempLocation\$Using:CertificateFileName" -Force }
        # Invoke-Command -Session $Session -ScriptBlock $RemoteCommand
        # LogToFile $LogFile "Removed $CertificateFileName from $($Session.ComputerName):$TempLocation"
        Return $true
    } Else {
        LogToFile $LogFile "Certificate import failed."
        Return $false
    }
}


# ============================================================
# Function: ActivateCertificateRemote
# ============================================================

<#
.SYNOPSIS
Executes a set of command remotely.

.DESCRIPTION
Executes a set of command remotely.
The specified scriptblock is executed on the remote server. The scriptblock should
contain all command required to activate the new certificate.

.PARAMETER Session
PSSession object to remote session.

.PARAMETER RemoteCommand
Scriptblock containing the PowerShell commands to activate the new certificate.

.INPUTS
None.

.OUTPUTS
Returns the result of the scriptblock.

.EXAMPLE
ActivateCertificateRemote -Session $RemoteSession -RemoteCommand { Set-AdfsSslCertificate -Thumbprint $using:Certificate.Thumbprint }

#>
Function ActivateCertificateRemote {
    Param (
        [Parameter(Mandatory=$true)]
        [System.Management.Automation.Runspaces.PSSession]$Session,
        [Parameter(Mandatory=$true)]
        [System.Management.Automation.ScriptBlock]$RemoteCommand
    )
    $Result = Invoke-Command -Session $Session -ScriptBlock $RemoteCommand
    LogToFile $LogFile $("Executed remote commands:`r`n" + $RemoteCommand.ToString())
    LogToFile $LogFile $("Result:`r`n" + $Result)
    Return $Result
}


# ============================================================
# Function: GetLatestCertificate
# ============================================================

<#
.SYNOPSIS
Checks location for a new certificate.

.DESCRIPTION
Checks location for a new certificate.
This function will return the name, path and (previous)thumbprint of the certificate.

.PARAMETER FileFilter
Filter to select files. Syntax using basic PS filtering, e.g. * and ?

.PARAMETER CertificateStorePath
Folder containing the certificates.

.INPUTS
None.

.OUTPUTS
Returns a custom object containing name, full path, thumbprint and thumbprint of the previous certificate (if available).

.EXAMPLE
GetLatestCertificate -FileFilter 'server.domain.com_????????-????????.pfx' -CertificateStorePath 'c:\temp\pfx'

.NOTES
This function checks for all files based on the filter, but only returns the most recent file.

#>
function GetLatestCertificate {
    Param (
        [Parameter(Mandatory=$true)]
        [string]$FileFilter,
        [Parameter(Mandatory=$true)]
        [string]$CertificateStorePath
    )
    # Get the most recently created certificate for selected domain
    $CertificateFile = Get-ChildItem -Path $CertificateStorePath -Filter $FileFilter -File | Sort-Object LastWriteTime -Descending | Select-Object -first 1
    # If no certificate is available, just exit the script
    If ($CertificateFile.Count -ne 1) { 
        LogToFile $LogFile "No new certificate found."
        Return $null
    }

    # Get thumbprint of new and previous certificate
    [xml]$CertificateXML = Get-Content $($CertificateFile[0].fullname.Replace(".pfx",".xml"))
    If ($CertificateXML.certs.ChildNodes.Count -ne 1) { 
        LogToFile $LogFile "XML not found or contains 0 or >1 cert nodes."
        Return $null
    }

    Return (New-Object -TypeName PSObject -Property @{
        'Name' = $CertificateFile.Name;
        'FullPath' = $CertificateFile.FullName;
        'Domain' = $CertificateXML.certs.cert.name;
        'Thumbprint' = $CertificateXML.certs.cert.thumb;
        'PreviousThumbprint' = $CertificateXML.certs.cert.thumbprevious;
        'StartDate' = $CertificateXML.certs.cert.start;
        'EndDate' = $CertificateXML.certs.cert.end
    })
}


# ============================================================
# Function: ConvertCertificateToPEM
# ============================================================

<#
.SYNOPSIS
Returns certificate and private key in PEM format

.DESCRIPTION
Converts a pfx certificate file to seperate public certificate and private key
in PEM format.

.PARAMETER CertificateFullPath
Full path to local certificate file.

.INPUTS
None.

.OUTPUTS
Returns a custom object containing public certificate and private key in PEm format as string.

.EXAMPLE
ConvertCertificateToPEM -CertificateFullPath 'c:\temp\www.domain.com.pfx' -PrivateKeyPassword 'Password'

#>
Function ConvertCertificateToPEM {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","PrivateKeyPassword")]
    Param (
        [Parameter(Mandatory=$true)]
        [string]$CertificateFullPath
    )
    # Extract certificate & private key as PEM
    $PEMCertificate = & $PSScriptRoot\openssl.exe pkcs12 -in $CertificateFullPath -clcerts -nokeys -passin pass: | & $PSScriptRoot\openssl.exe x509
    $PEMprivateKey = & $PSScriptRoot\openssl.exe pkcs12 -in $CertificateFullPath -nocerts -passin pass: -passout pass:TempPass1234 | & $PSScriptRoot\openssl.exe rsa -passin pass:TempPass1234

    Return (New-Object -TypeName PSObject -Property @{
        'Certificate' = $PEMCertificate -join "`r`n";   # Convert array of strings to single string
        'PrivateKey' = $PEMprivateKey -join "`r`n"      # Convert array of strings to single string
    })
}


# ============================================================
# Function: OpenSSHSession
# ============================================================

<#
.SYNOPSIS
Opens SSH session to remote server.

.DESCRIPTION
Opens SSH session to remote server.

.PARAMETER CredentialTargetName
Name of credential stored in Windows Credential store (Generic credential).

.PARAMETER Server
Server (fqdn) to connect to.

.INPUTS
None.

.OUTPUTS
Returns a SSHSession object if remote connection was setup succesfully or $null if unsuccesfull.

.EXAMPLE
OpenSSHSession -Server 'server.domain.com' -CredentialTargetName 'ServerCredentials'

#>
Function OpenSSHSession {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","CredentialTargetName")]
    Param (
        [Parameter(Mandatory=$true)]
        [string]$CredentialTargetName,
        [Parameter(Mandatory=$true)]
        [string]$Server
    )
    $Credentials = Get-StoredCredential -Target $CredentialTargetName
    If ( $Credentials ) {
        $Session = New-SSHSession -ComputerName $Server -Credential $Credentials
        # Check if session was started, return session object if succesfull
        If ( $Session ) {
            LogToFile $LogFile "Remote SSH session to $Server created."
            Return $Session
        } Else {
            LogToFile $LogFile "Error: unable to create a remote SSH session to $Server."
            Return $null
        }
    } Else {
        LogToFile $LogFile "Credentials '$CredentialTargetName' not found."
        Return $null
    }
}


# ============================================================
# Function: CloseSSHSession
# ============================================================

<#
.SYNOPSIS
Closes a SSH session to a remote server.

.DESCRIPTION
Closes a SSH session to a remote server.

.PARAMETER Session
SSHSession object to remote session.

.INPUTS
None.

.OUTPUTS
None.

.EXAMPLE
CloseSSHSession -Session $SSHSession

#>
Function CloseSSHSession {
    Param (
        [Parameter(Mandatory=$true)]
        [string]$Session
    )
    $Server = $Session.Host
    Remove-SSHSession -SSHSession $Session
    LogToFile $LogFile "Remote SSH session to $Server closed."
}


# ============================================================
# Function: ActivateCertificateSSH
# ============================================================

<#
.SYNOPSIS
Executes a set of commands via SSH.

.DESCRIPTION
Executes a set of commands via SSH.
The specified array of commands is executed on the remote server.

.PARAMETER Session
SSHSession object to remote session.

.PARAMETER RemoteCommand
SArray of strings containing the SSH commands to activate the new certificate.

.INPUTS
None.

.OUTPUTS
Returns the result of the scriptblock.

.EXAMPLE
ActivateCertificateSSH -Session $SSHSession -RemoteCommand @"set servercert "mynewcertificate" "@

#>
Function ActivateCertificateSSH {
    Param (
        [Parameter(Mandatory=$true)]
        [string]$Session,
        [Parameter(Mandatory=$true)]
        [string[]]$RemoteCommand
    )
    $Result = Invoke-SSHCommand -SSHSession $Session -Command $RemoteCommand
    LogToFile $LogFile $("Executed remote commands:`r`n" + $RemoteCommand.ToString())
    LogToFile $LogFile $("Result:`r`n" + $Result.Output)
    Return $Result
}

C:\CTW\Scripts\_scripts\template-1-server.ps1_tpl

# ##################################################################################
#
# 
#
# ##################################################################################
#
# Description:
#
#   Checks pickup folder to see if a new certificate for given domain is available.
#   Copies file to remote server, imports the certificate and activates the new
#   certificate. (Optional) The old certificate is then removed from store.
#
# Prerequisites:
# 
#   It's required to add the remote servers to the TrustedHosts parameter:
#     Set-Item WSMan:\localhost\Client\TrustedHosts -Value "server1.domain.com,..."
#   note: this overwrites previous value. Read first and append, fqdn seperated
#         by comma.
#
#   When using CredSSP, add the target host to the delegates list:
#     $DelegateServers = "server1.domain.com","...","..."
#     Enable-WSManCredSSP -Role Client -DelegateComputer $DelegateServers -force
#
# ##################################################################################

# ----------------------------------------------------------------------------------
# Includes
# ----------------------------------------------------------------------------------

. "$PSScriptRoot\_files\_include.ps1"

# ----------------------------------------------------------------------------------
# User configurable variables
# ----------------------------------------------------------------------------------

# Certificate name
$CertificateName = "<name/fqdn of cert>>"
$CertificateStorePath = "<local path where certificates are stored>"

# Target server 1
$RPSCredTarget1 = "<name of credential>"         # stored credential for server
$RPSServerName1 = "<name/fqdn/ip of server>"   # server name/ip
# Remember to add code to perform on the remote server below @ <powershell code>

# ----------------------------------------------------------------------------------
# Set log
# ----------------------------------------------------------------------------------

$LogFile = "$PSScriptRoot\$($CertificateName)_$(Get-Date -Format("yyyyMMdd")).log"

LogToFile $LogFile "=== Script started ==="

# ----------------------------------------------------------------------------------
# Get certificate
# ----------------------------------------------------------------------------------

# cert.domain.com_yyyymmdd-yyyymmdd.pfx
$Certificate = GetLatestCertificate -FileFilter "$($CertificateName)_????????-????????.pfx"  -CertificateStorePath $CertificateStorePath
If (!( $Certificate )) { 
    LogToFile $LogFile "Exiting script..."
    Exit
}

LogToFile $LogFile $("Found " + $Certificate.FullPath)

# ----------------------------------------------------------------------------------
# Install certificate on remote servers
# ----------------------------------------------------------------------------------

$CertificateUpdateSuccess = $false
$RPSSession1 = OpenRemoteSession -CredentialTargetName $RPSCredTarget1 -Server $RPSServerName1 -CredSSP
If ( $RPSSession1 ) {
    # Remote session to server successfully started
    If ( InstallCertificateRemote -Session $RPSSession1 -CertificateFullPath $Certificate.FullPath ) {

        # Activate certificate on server 1, required code in scriptblock
        ActivateCertificateRemote -Session $RPSSession1 -RemoteCommand {
            <powershell code>
        }

        $CertificateUpdateSuccess = $true
    } Else {
        LogToFile $LogFile "Unable to install certificate on $RPSServerName1."
        CloseRemoteSession -Session $RPSSession1
        Exit
    }
    CloseRemoteSession -Session $RPSSession1
} Else {
    LogToFile $LogFile "Unable to open remote session to $RPSServerName1."
}


# ----------------------------------------------------------------------------------
# Rename certificate (handle cert only once)
# ----------------------------------------------------------------------------------

If ( $CertificateUpdateSuccess ) {
    Rename-Item -Path $Certificate.FullPath -NewName $Certificate.Name.Replace(".pfx",".pfx.done")
    Rename-Item -Path $Certificate.FullPath.Replace(".pfx",".xml") -NewName $Certificate.Name.Replace(".pfx",".xml.done")
    LogToFile $LogFile "Appended '.done' to certificate and xml files."
    LogToFile $LogFile "Finished."
}

C:\CTW\Scripts\_scripts\template-2-servers.ps1_tpl

# ##################################################################################
#
# 
#
# ##################################################################################
#
# Description:
#
#   Checks pickup folder to see if a new certificate for given domain is available.
#   Copies file to remote server, imports the certificate and activates the new
#   certificate. (Optional) The old certificate is then removed from store.
#
# Prerequisites:
# 
#   It's required to add the remote servers to the TrustedHosts parameter:
#     Set-Item WSMan:\localhost\Client\TrustedHosts -Value "server1.domain.com,..."
#   note: this overwrites previous value. Read first and append, fqdn seperated
#         by comma.
#
#   When using CredSSP, add the target host to the delegates list:
#     $DelegateServers = "server1.domain.com","...","..."
#     Enable-WSManCredSSP -Role Client -DelegateComputer $DelegateServers -force
#
# ##################################################################################

# ----------------------------------------------------------------------------------
# Includes
# ----------------------------------------------------------------------------------

. "$PSScriptRoot\_files\_include.ps1"

# ----------------------------------------------------------------------------------
# User configurable variables
# ----------------------------------------------------------------------------------

# Certificate name
$CertificateName = "<name/fqdn of cert>"
$CertificateStorePath = "<local path where certificates are stored>"

# Target server 1
$RPSCredTarget1 = "<name of credential>"         # stored credential for server
$RPSServerName1 = "<name/fqdn/ip of server>"   # server name/ip
# Remember to add code to perform on the remote server below @ <powershell code>

# Target server 2
$RPSCredTarget2 = "<name of credential>"          # stored credential for server
$RPSServerName2 = "<name/fqdn/ip of server>"   # server name/ip
# Remember to add code to perform on the remote server below @ <powershell code>

# ----------------------------------------------------------------------------------
# Set log
# ----------------------------------------------------------------------------------

$LogFile = "$PSScriptRoot\$($CertificateName)_$(Get-Date -Format("yyyyMMdd")).log"

LogToFile $LogFile "=== Script started ==="

# ----------------------------------------------------------------------------------
# Get certificate
# ----------------------------------------------------------------------------------

# cert.domain.com_yyyymmdd-yyyymmdd.pfx
$Certificate = GetLatestCertificate -FileFilter "$($CertificateName)_????????-????????.pfx"  -CertificateStorePath $CertificateStorePath
If (!( $Certificate )) { 
    LogToFile $LogFile "Exiting script..."
    Exit
}

LogToFile $LogFile $("Found " + $Certificate.FullPath)

# ----------------------------------------------------------------------------------
# Install certificate on remote servers
# ----------------------------------------------------------------------------------

$CertificateUpdateSuccess = $false
$RPSSession1 = OpenRemoteSession -CredentialTargetName $RPSCredTarget1 -Server $RPSServerName1 -CredSSP
If ( $RPSSession1 ) {
    $RPSSession2 = OpenRemoteSession -CredentialTargetName $RPSCredTarget2 -Server $RPSServerName2
    If ( $RPSSession2 ) {
        # Remote sessions to both servers successfully started
        If ( InstallCertificateRemote -Session $RPSSession1 -CertificateFullPath $Certificate.FullPath ) {
            If ( InstallCertificateRemote -Session $RPSSession2 -CertificateFullPath $Certificate.FullPath ) {

                # Activate certificate on server 1, required code in scriptblock
                ActivateCertificateRemote -Session $RPSSession1 -RemoteCommand {
                    <powershell code>
                }
                # Activate certificate on server 2, required code in scriptblock
                ActivateCertificateRemote -Session $RPSSession2 -RemoteCommand {
                    <powershell code>
                }
                
                $CertificateUpdateSuccess = $true
            } Else {
                LogToFile $LogFile "Unable to install certificate on $RPSServerName2."
                CloseRemoteSession -Session $RPSSession1
                CloseRemoteSession -Session $RPSSession2
                Exit
            }
        } Else {
            LogToFile $LogFile "Unable to install certificate on $RPSServerName1."
            CloseRemoteSession -Session $RPSSession1
            CloseRemoteSession -Session $RPSSession2
            Exit
        }
        CloseRemoteSession -Session $RPSSession1
        CloseRemoteSession -Session $RPSSession2
    } Else {
        LogToFile $LogFile "Unable to open remote session to $RPSServerName2."
        CloseRemoteSession -Session $RPSSession1
    }
} Else {
    LogToFile $LogFile "Unable to open remote session to $RPSServerName1."
}


# ----------------------------------------------------------------------------------
# Rename certificate (handle cert only once)
# ----------------------------------------------------------------------------------

If ( $CertificateUpdateSuccess ) {
    Rename-Item -Path $Certificate.FullPath -NewName $Certificate.Name.Replace(".pfx",".pfx.done")
    Rename-Item -Path $Certificate.FullPath.Replace(".pfx",".xml") -NewName $Certificate.Name.Replace(".pfx",".xml.done")
    LogToFile $LogFile "Appended '.done' to certificate and xml files."
    LogToFile $LogFile "Finished."
}

To use a template, copy it, rename to logical name (e.g. fqdn) with .ps1 extension.

Edit the file, change:

  • $RPSCredTarget1 and $RPSServerName1
  • $RPSCredTarget2 and $RPSServerName2 (in case of 2 dependent servers)
  • Insert the PS code to execute remote @ <powershell code>
  • Insert the PS code to execute remote @2nd <powershell code> (in case of 2 dependent servers)

Example:

For ADFS you need to update the certificate on the ADFS server itself and on the proxy server (WAP).

The first server is the ADS server itself, the second is the WAP server. Make sure the right credentials and FQDN are entered. ADFS requires CredSSP!

The first code block could be:

Set-AdfsSslCertificate -Thumbprint $using:Certificate.Thumbprint

The second code block for the proxy:

Set-WebApplicationProxySslCertificate -Thumbprint $using:Certificate.Thumbprint

$using is used because the code is executed remotely, but need to use variables defined locally.

Example:

This is an example for mail, with WAP as proxy. I named all entries in WAP starting with Exchange, so this makes the code a bit shorter :slight_smile:

Code for Exchange:

Add-PSSnapIn Microsoft.Exchange.Management.PowerShell.E2010
Enable-ExchangeCertificate -Thumbprint “$using:Certificate.Thumbprint” -Services POP,IMAP,SMTP,IIS -Force

Code for WAP:

Get-WebApplicationProxyApplication -Name “Exchange *” | Set-WebApplicationProxyApplication -ExternalCertificateThumbprint “$using:Certificate.Thumbprint”

One more. This template is for ssh with pem certificates.

Example:

CLI for FortiGate.

I added a PS line above the CLI code to define a certificate name variable to use for FortiGate:

$CertificateDisplayName = $Certificate.Domain + " " + $Certificate.EndDate

This is the CLI code for FortiGate:

config global
config certificate local
edit “$CertificateDisplayName”
set password “”
set private-key “$($CertificatePEM.PrivateKey)”
set certificate “$($CertificatePEM.Certificate)”
set comments “”
next
end
end
config vdom
edit companyvpn
config vpn ssl settings
set servercert “$CertificateDisplayName”
end
next
end

C:\CTW\Scripts\_scripts\template-2-servers.ps1_tpl

# ##################################################################################
#
#  
#
# ##################################################################################
#
# Description:
#
#   Checks pickup folder to see if a new certificate for given domain is available.
#   Convert pfx to pem format, upload the certificate using SSH and activate it.
#
# ##################################################################################

# ----------------------------------------------------------------------------------
# Includes
# ----------------------------------------------------------------------------------

. "$PSScriptRoot\_files\_include.ps1"

# ----------------------------------------------------------------------------------
# User configurable variables
# ----------------------------------------------------------------------------------

# Certificate name
$CertificateName = "<name/fqdn of cert>"
$CertificateStorePath = "<local path where certificates are stored>"

# Target server 1
$SSHCredTarget1 = "<name of credential>"         # stored credential for server
$SSHServerName1 = "<name/fqdn/ip of server>"   # server name/ip

# ----------------------------------------------------------------------------------
# Set log
# ----------------------------------------------------------------------------------

$LogFile = "$PSScriptRoot\$($CertificateName)_$(Get-Date -Format("yyyyMMdd")).log"

LogToFile $LogFile "=== Script started ==="

# ----------------------------------------------------------------------------------
# Get certificate
# ----------------------------------------------------------------------------------

# filter: cert.domain.com_yyyymmdd-yyyymmdd.pfx
$Certificate = GetLatestCertificate -FileFilter "$($CertificateName)_????????-????????.pfx"  -CertificateStorePath $CertificateStorePath
If (!( $Certificate )) { 
    LogToFile $LogFile "Exiting script..."
    Exit
}

LogToFile $LogFile $("Found " + $Certificate.FullPath)

# ----------------------------------------------------------------------------------
# Convert certificate as PEM
# ----------------------------------------------------------------------------------

$CertificatePEM =  ConvertCertificateToPEM -CertificateFullPath $Certificate.FullPath
LogToFile $LogFile $("Converted PFX to PEM format certificates")

# ----------------------------------------------------------------------------------
# Install certificate on remote servers
# ----------------------------------------------------------------------------------

$CertificateUpdateSuccess = $false
$SSHSession1 = OpenSSHSession -CredentialTargetName $SSHCredTarget1 -Server $SSHServerName1
If ( $SSHSession1 ) {
    # Activate certificate on server 1
    $SSHRemoteCommand1 = @"

<cli code>

"@
    $Result = ActivateCertificateSSH -Session $SSHSession1 -RemoteCommand $SSHRemoteCommand1
    $CertificateUpdateSuccess = $true
    CloseSSHSession -Session $SSHSession1
} Else {
    LogToFile $LogFile "Unable to open SSH session to $SSHServerName1."
}


# ----------------------------------------------------------------------------------
# Rename certificate (handle cert only once)
# ----------------------------------------------------------------------------------

If ( $CertificateUpdateSuccess ) {
    Rename-Item -Path $Certificate.FullPath -NewName $Certificate.Name.Replace(".pfx",".pfx.done")
    Rename-Item -Path $Certificate.FullPath.Replace(".pfx",".xml") -NewName $Certificate.Name.Replace(".pfx",".xml.done")
    LogToFile $LogFile "Appended '.done' to certificate and xml files."
    LogToFile $LogFile "Finished."
}

Nice work! Some of these will be possible to replace or extend with Deployment Tasks from v5 onwards but it’s great to see such a variety of scripts.

Deployment Tasks will (so far) provide UI to do:

  • Certificate exports (all formats)
  • Targeted exports to Apache, Nginx, Tomcat, Central Certificate Store etc
  • SSH/Sftp
  • Powershell Scripts
  • Webhooks
  • Exchange

Thx.

Yes, I read about the planned deployment functionality in the new version of Certify SSL Manager. But I needed a solution now, and v5 is not yet available so I had to do something myself :smiley:

I think it’s great, there are thousands of scripts out there but most people keep them to themselves. It could be good for each person to have start their own github repository then we could link to them.

There was the idea of having a shared repository of examples scripts but that never really took off and it would be something more for me to maintain!

I just purchased a Fortigate for my home network and using the latest CertifyTheWeb host on a Windows Server, is it okay to see if you or someone has updated these scripts to work with v5 and if so post them?

Scripts that work with older versions of Certify should work with v5.x, the main change in v5 was the introduction of user impersonation options so you can run as other users, and scripts were moved to the Tasks feature.

Maybe a newbie question, but Certify v5.x has an option to script export out a PEM file, yet the Task settings are for CER, KEY, and Chain so how to export out a PEM file?

So PEM is a generic text container format for writing out data , which is usually certificate or private key related. https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail

If something asks you to provide a PEM file, they’re really asking for a PEM encoded file of something, their instructions probably specify what (a certificate, a certificate chain or a private key).

Some servers loosely use the convention of .crt for the leaf/end-entity cert (i.e. your certificate), .chain for the list of certs that are used to sign your certificate by the certificate authority, and .key for your private key (the one used to sign your original certificate signing request with the certificate authority). The file extension means nothing though and is just a guide for the user, really it what’s in the file that matters.

Apologies for reviving this one however it’s so very close to doing exactly what I need. Unfortunately there is some issue with the ConvertCertificateToPEM function in the _include.ps1 script that is eluding me.

I’m getting this on script run when using the CLI for FortiGate section:

openssl.exe : writing RSA key
At C:\CTW\Scripts_scripts_files_include.ps1:390 char:134

  • … pPass1234 | & $PSScriptRoot\openssl.exe rsa -passin pass:TempPass1234 …
  •             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    • CategoryInfo : NotSpecified: (writing RSA key:String) , RemoteException
    • FullyQualifiedErrorId : NativeCommandError

I’ve tried everything I know how over a few hours including trying every possible version of OpenSSL I can find to no avail.

Did anyone ever get this working or [Miesepies] did you ever get this running? I love the design and do not want to let this error beat me!

Hi. It’s been a while since this first version and the scripts have changed quite a lot on my side. However the private key extraction code is still the same and is working fine.

It looks like it goes wrong when it tries to convert the password encrypted key to a RSA key. You could split the commands like below (not tested) to check which part of the code actually produces the error. You can also try to run the openssl commands manually in a powershell prompt to see what happens…

$PEMprivateKey = & $PSScriptRoot\openssl.exe pkcs12 -in $CertificateFullPath -nocerts -passin pass: -passout pass:TempPass1234
$PEMprivateKey = $PEMprivateKey | & $PSScriptRoot\openssl.exe rsa -passin pass:TempPass1234

Edit: I found an article which describes an issue where running openssl twice in succession could trigger an error in Powershell. Is your PS version up to date?

Michel

Note also that Certify The Web has a bunch of built in tasks such as Deploy to Generic Server (which exports PEM encoded files for many service types) or the more granular Export Certificate task. These tasks can generally export to the local server or copy to a network share, or copy via SSH/SFTP.

So depending on what you are trying to do you may not have to script absolutely everything yourself.

I agree. I think it might be easier to have Certify store the certificate in the required format and then use scripts to have the certificate imported to devices not supported by Certify.

Thanks guys. The part that I really loved about the script was just having the single PFX file and then the splitout to the PEM cert and key all within the script but it seems more work than it’s worth when CTW can already do the output to PEM.

I’m also doing this on a fresh 2022 Datacentre install so I’m running Windows PowerShell 5.1 that’s built in. Unsure if I need the newer (not Windows) PowerShell versions however.

I’ll give using a split out to the separate PEM files and then see how I go and report back for anyone else needing the same.