Powershell – Import certificates on remote servers

Standard

A while back I was tasked with importing some certificates on all our web servers. I didn’t like the idea of doing this manually on hundreds of servers so I decided I had to write a script which could help me with this job. The problem was that I couldn’t find any “easy” way of importing certificates on remote servers. It is possible with some commercial tools and also with windows 2012 R2 and powershell v4 but none of these options were available to me. Furthermore I wanted to have a script which was generic and could be used on all windows servers regardless of version. This led me to create this powershell script which I will go over for you now.

The script loops through the servers in the “servers.txt” file (you must create this file and put it in the script directory) and basically works as outlined in these steps:
1. The certificate is copied to the remote server
2. A custom mini-scipt for importing the certificate is created and copied to the remote server.
3. The mini-script is invoked by using psexec and imported into the desired certificate store.
4. The certificate and mini-script are deleted on the remote server after use.
5. The result of the certificate import is checked.

A log file with relevant information is created when the script is run, making it possible to confirm the successful import later on.
The script is divided into three sections – variables, functions and script main.

Variables

#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$RemoteCertDir = "c:\temp\certs\" #temporary file location on remote server
$logfile = "$dir\logs\" + "cert-bulkimport_" + $(get-date -Format "yyyy-MM-dd_HH-mm-ss") + ".log"
$servers = gc "$dir\servers.txt"
$psexec = "$dir\psexec.exe"
#parameters
$DebugPreference = "continue" #comment out to disable debug info
$certRootStore = "localmachine"
$certStore = "My"
$Cert = "$dir\certs\some_cert.pfx" #certificate to import
$CertPW = "Password" #password for certificate to import
$CertCN = "some.domain.com" #common name of certificate to import

The first two lines of code –
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath

are really practical as it gives me an object $dir which always points to where the script was run. This makes the script more robust as I can freely move the script around and run it from many locations without having to change any paths in the script.
$logfile is the path of the log file which will be created for each script run. The file name will contain a time stamp to avoid overwriting the file.
$servers is an array populated by running Get-Content on the “servers.txt” file.
$psexec is the path to psexec.exe (place it in the same directory as the script).
The parameters section is really important and you need to fill these variables with the specific information about your certificate and which store to import them to. The descriptions in the code comments should explain how to do this.

Functions

1. Function Log-Entry

function log-entry {
    param(
        $message
    )
    $time = Get-date
    Add-Content -Path $logfile -Value "$time`t$message"
}

This function takes a parameter “message” and appends it to the log file with a timestamp. I use it to easily write to the log file throughout the script.

2. Function RemoteCopy-Certificate

function RemoteCopy-Certificate {
    param(
        $server
    )
    $copyresult = $false
    $RemoteDir = "\\$server\" + $RemoteCertDir.replace(":","$")	
    Write-Debug "Creating directory $RemoteDir"
    log-entry -message "Creating directory $RemoteDir"	
    New-Item -ItemType "directory" $RemoteDir -Force
    $RemoteCertPath = $RemoteDir + (gci $Cert).Name	
    Write-Debug "Copying $Cert to $RemoteDir"
    log-entry -message "Copying $Cert to $RemoteDir"	
    Copy-Item -Path $Cert -Destination $RemoteCertPath -Force
    if (Test-Path -Path $RemoteCertPath) {
        $CopyResult = $true		
        Write-Debug "Copy of $Cert to $RemoteDir succeeded."
        log-entry -message "Copy of $Cert to $RemoteDir succeeded." 
    }
    else {
        $CopyResult = $false 		
        Write-Debug "Error - Copy of $Cert to $RemoteDir failed."
        log-entry -message "Error - Copy of $Cert to $RemoteDir failed."
    }
    return $CopyResult
}

This function creates a temp directory on the remote server and copies the certificate to there. It then checks if the certificate is there and returns $true if the copy was successful.

3. Function Create-ImportScript

function Create-ImportScript {
    param(
        $server
    )
    $ScriptCreateResult = $false
    $RemoteScriptPath = "\\$server\" + $RemoteCertDir.Replace(":","$") + "import-pfx.ps1"    
    $CertPath = "'" + $RemoteCertDir + (gci $Cert).Name + "'"    
    $crtPass = "'" + $CertPW + "'"
    $SB = @"
`n
`$crt = new-object System.Security.Cryptography.X509Certificates.X509Certificate2
`$crt.import($certPath,$crtPass,"Exportable,PersistKeySet")
`$store = new-object System.Security.Cryptography.X509Certificates.X509Store(`'$certStore`',`'$certRootStore`') 
`$store.open("MaxAllowed") 
`$store.add(`$crt) 
`$store.close()
"@
    log-entry -message "Creating import script $RemoteScriptPath"
    Write-Debug "Creating import script $RemoteScriptPath`:$SB`n"	
    $SB | Out-File $RemoteScriptPath -force
    if (Test-Path -Path $RemoteScriptPath) {
        $ScriptCreateResult = $true		
        Write-Debug "Creation of $RemoteScriptPath succeeded."
        log-entry -message "Creation of $RemoteScriptPath succeeded." 
    }
    else {
        $ScriptCreateResult = $false		
        Write-Debug "Error - Creation of $RemoteScriptPath failed."
        log-entry -message "Error - Creation of $RemoteScriptPath failed."
    }
    return $ScriptCreateResult    
}

Here, a custom powershell mini import script is created. Basically the mini script comes from the multi-line string $SB which is redirected to create a .ps1 file. The function then checks if the file was created on the remote server and returns $true for a successful result.

4. Function Invoke-Importscript

function Invoke-Importscript {
    param(
        $server
    )
    $ImportScriptPath = $RemoteCertDir + "import-pfx.ps1"
    $arg1 = "/acceptEula"
    $arg2 = "\\$server"
    $arg3 = "cmd /c"
    $arg4 = "`"echo .|powershell -file $ImportScriptPath`""	
    Write-Debug "invoking remote import script on $server"   
    log-entry -message "invoking remote import script on $server"	
    Start-Process -Wait -FilePath $psexec -ArgumentList "$arg1 $arg2 $arg3 $arg4"
}

This function runs the mini script on the remote server and imports the certificate into the chosen store. Psexec.exe is used to invoke the script on the remote server.

5. Function Remove-TempFiles

function Remove-TempFiles {
    param(
        $server
    )
    $RemoteScriptDir = "\\$server\" + $RemoteCertDir.Replace(":","$")	
    Write-Debug "Removing temporary cert and script files from $RemoteScriptDir"
    log-entry -message "Removing temporary cert and script files from $RemoteScriptDir"	
    gci -Recurse -Path $RemoteScriptDir | Remove-Item
}

As we don’t want to leave any certs or scripts behind after we are done I wrote this function to remove these files after the cert has been imported.

6. Function List-RemoteCerts

function List-RemoteCerts {
    param(
        $server
    )
    $ro = [System.Security.Cryptography.X509Certificates.OpenFlags]"ReadOnly"
    $lm = [System.Security.Cryptography.X509Certificates.StoreLocation]"LocalMachine"
    $store = new-object System.Security.Cryptography.X509Certificates.X509Store("\\$server\$CertStore", $lm)
    $store.Open($ro)
    return $store.Certificates
}

This function lists the certificates on the remote server. I use this function in the next function Confirm-CertImport.

7. Function Confirm-CertImport

function Confirm-CertImport {
    param(
        $server
    )
    Write-Debug "Checking if certificate import on $server was successful."
    log-entry -message "Checking if certificate import on $server was successful."	
    $IsCertThere = $false    
    $MyCerts = List-RemoteCerts -server $server        
    $MyCerts | % {	
        Write-Debug "Found certificate: $($_.Subject)"		
        if ($_.Subject -like "*$CertCN*") {		
            Write-Debug "*************Certificate match for $CertCN found!)"			
            $IsCertThere = $true
        }
    }
    return $IsCertThere       
}

Here I loop through the certificates found on the remote server. If the certificate we just attempted to import is found a result of $true is returned.

Script Main

#SCRIPT MAIN
new-item -itemtype "file" $logfile -force | out-null #create the log file for this script execution
Write-Debug "Script run started."
log-entry -message "Script run started."
$servers | % { #loop through the servers which we will import the certificate to
    Write-Debug "Starting cert import on $_".ToUpper()
    log-entry "Starting cert import on $_".ToUpper()
    $copyResult = RemoteCopy-Certificate -server $_ # Find out if the copy of the cert to the server was successful
    if ($copyResult) {   #If the copy of the certificate to the server succeeded then proceed.     
        $ScriptCreateResult = Create-ImportScript -server $_ #Find out if the script was created and copied to the server successfully
        if ($ScriptCreateResult) { #if the script was created/copied to the server successfully then proceed.
            Invoke-Importscript -server $_ #import the certificate into the specified store
            Remove-TempFiles -server $_ #Remove temp files such as the cert and import script
            $CertImportResult = Confirm-CertImport -server $_ #check that the cert was imported successfully and write the result to the log.
            if ($CertImportResult) {
                Write-Debug "Success! $CertCN was successfully imported on $_"
                log-entry -message "Success! $CertCN was successfully imported on $_"
            }
            else {
                Write-Debug "Error! Import of $CertCN on $_ failed."
                log-entry -message "Error! Import of $CertCN on $_ failed."
            }
        }
    }
    Write-Debug "-------------------------------------------------------------------------------------------------------------------------"
    log-entry "`n`n"
}

The script main starts with creating the log file used for the script run. I then loop through the $servers array and with some nested IF sentences run the functions described above.
When the script run has completed, the certificate will be imported on the servers specified in the “servers.txt” file and you will have a log file with the import results available.
I hope you find the script useful. Enjoy!!
I have copied in the full script below.

##################################################################################################################
##RemoteImport-Certificate.ps1
##Description: 		Script imports a certificate into all remote servers specified in the 
#+                  "servers.txt" inputlist.
#+					Fill out the "parameters" section according to your needs before running!
##Created by:		Noam Wajnman
##Creation Date:	March 17, 2014
###################################################################################################################
#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$RemoteCertDir = "c:\temp\certs\" #temporary file location on remote server
$logfile = "$dir\logs\" + "cert-bulkimport_" + $(get-date -Format "yyyy-MM-dd_HH-mm-ss") + ".log"
$servers = gc "$dir\servers.txt"
$psexec = "$dir\psexec.exe"
#parameters
$DebugPreference = "continue" #comment out to disable debug info
$certRootStore = "localmachine"
$certStore = "My"
$Cert = "$dir\certs\some_cert.pfx" #certificate to import
$CertPW = "Password" #password for certificate to import
$CertCN = "some.domain.com" #common name of certificate to import
#FUNCTIONS
function log-entry {
    param(
        $message
    )
    $time = Get-date
    Add-Content -Path $logfile -Value "$time`t$message"
}
function RemoteCopy-Certificate {
    param(
        $server
    )
    $copyresult = $false
    $RemoteDir = "\\$server\" + $RemoteCertDir.replace(":","$")	
    Write-Debug "Creating directory $RemoteDir"
    log-entry -message "Creating directory $RemoteDir"	
    New-Item -ItemType "directory" $RemoteDir -Force
    $RemoteCertPath = $RemoteDir + (gci $Cert).Name	
    Write-Debug "Copying $Cert to $RemoteDir"
    log-entry -message "Copying $Cert to $RemoteDir"	
    Copy-Item -Path $Cert -Destination $RemoteCertPath -Force
    if (Test-Path -Path $RemoteCertPath) {
        $CopyResult = $true		
        Write-Debug "Copy of $Cert to $RemoteDir succeeded."
        log-entry -message "Copy of $Cert to $RemoteDir succeeded." 
    }
    else {
        $CopyResult = $false 		
        Write-Debug "Error - Copy of $Cert to $RemoteDir failed."
        log-entry -message "Error - Copy of $Cert to $RemoteDir failed."
    }
    return $CopyResult
}
function Create-ImportScript {
    param(
        $server
    )
    $ScriptCreateResult = $false
    $RemoteScriptPath = "\\$server\" + $RemoteCertDir.Replace(":","$") + "import-pfx.ps1"    
    $CertPath = "'" + $RemoteCertDir + (gci $Cert).Name + "'"    
    $crtPass = "'" + $CertPW + "'"
    $SB = @"
`n
`$crt = new-object System.Security.Cryptography.X509Certificates.X509Certificate2
`$crt.import($certPath,$crtPass,"Exportable,PersistKeySet")
`$store = new-object System.Security.Cryptography.X509Certificates.X509Store(`'$certStore`',`'$certRootStore`') 
`$store.open("MaxAllowed") 
`$store.add(`$crt) 
`$store.close()
"@
    log-entry -message "Creating import script $RemoteScriptPath"
    Write-Debug "Creating import script $RemoteScriptPath`:$SB`n"	
    $SB | Out-File $RemoteScriptPath -force
    if (Test-Path -Path $RemoteScriptPath) {
        $ScriptCreateResult = $true		
        Write-Debug "Creation of $RemoteScriptPath succeeded."
        log-entry -message "Creation of $RemoteScriptPath succeeded." 
    }
    else {
        $ScriptCreateResult = $false		
        Write-Debug "Error - Creation of $RemoteScriptPath failed."
        log-entry -message "Error - Creation of $RemoteScriptPath failed."
    }
    return $ScriptCreateResult    
}
function Invoke-Importscript {
    param(
        $server
    )
    $ImportScriptPath = $RemoteCertDir + "import-pfx.ps1"
    $arg1 = "/acceptEula"
    $arg2 = "\\$server"
    $arg3 = "cmd /c"
    $arg4 = "`"echo .|powershell -file $ImportScriptPath`""	
    Write-Debug "invoking remote import script on $server"   
    log-entry -message "invoking remote import script on $server"	
    Start-Process -Wait -FilePath $psexec -ArgumentList "$arg1 $arg2 $arg3 $arg4"
}
function Remove-TempFiles {
    param(
        $server
    )
    $RemoteScriptDir = "\\$server\" + $RemoteCertDir.Replace(":","$")	
    Write-Debug "Removing temporary cert and script files from $RemoteScriptDir"
    log-entry -message "Removing temporary cert and script files from $RemoteScriptDir"	
    gci -Recurse -Path $RemoteScriptDir | Remove-Item
}
function List-RemoteCerts {
    param(
        $server
    )
    $ro = [System.Security.Cryptography.X509Certificates.OpenFlags]"ReadOnly"
    $lm = [System.Security.Cryptography.X509Certificates.StoreLocation]"LocalMachine"
    $store = new-object System.Security.Cryptography.X509Certificates.X509Store("\\$server\$CertStore", $lm)
    $store.Open($ro)
    return $store.Certificates
}
function Confirm-CertImport {
    param(
        $server
    )
    Write-Debug "Checking if certificate import on $server was successful."
    log-entry -message "Checking if certificate import on $server was successful."	
    $IsCertThere = $false    
    $MyCerts = List-RemoteCerts -server $server        
    $MyCerts | % {	
        Write-Debug "Found certificate: $($_.Subject)"		
        if ($_.Subject -like "*$CertCN*") {		
            Write-Debug "*************Certificate match for $CertCN found!)"			
            $IsCertThere = $true
        }
    }
    return $IsCertThere       
}
#SCRIPT MAIN
new-item -itemtype "file" $logfile -force | out-null #create the log file for this script execution
Write-Debug "Script run started."
log-entry -message "Script run started."
$servers | % { #loop through the servers which we will import the certificate to
    Write-Debug "Starting cert import on $_".ToUpper()
    log-entry "Starting cert import on $_".ToUpper()
    $copyResult = RemoteCopy-Certificate -server $_ # Find out if the copy of the cert to the server was successful
    if ($copyResult) {   #If the copy of the certificate to the server succeeded then proceed.     
        $ScriptCreateResult = Create-ImportScript -server $_ #Find out if the script was created and copied to the server successfully
        if ($ScriptCreateResult) { #if the script was created/copied to the server successfully then proceed.
            Invoke-Importscript -server $_ #import the certificate into the specified store
            Remove-TempFiles -server $_ #Remove temp files such as the cert and import script
            $CertImportResult = Confirm-CertImport -server $_ #check that the cert was imported successfully and write the result to the log.
            if ($CertImportResult) {
                Write-Debug "Success! $CertCN was successfully imported on $_"
                log-entry -message "Success! $CertCN was successfully imported on $_"
            }
            else {
                Write-Debug "Error! Import of $CertCN on $_ failed."
                log-entry -message "Error! Import of $CertCN on $_ failed."
            }
        }
    }
    Write-Debug "-------------------------------------------------------------------------------------------------------------------------"
    log-entry "`n`n"
}
Advertisements

2 thoughts on “Powershell – Import certificates on remote servers

  1. orly

    HI,

    I tried this but i’m getting an error says Import failed but when I put #remove-tempfiles so I can manually run the import-pfx.ps1 and it works. Not sure what is the issue

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s