Thursday, September 12, 2013

Deploying Signed PowerShell Scripts in the Enterprise the Free and Easy Way

Some time back I created a PowerShell script that goes through all the computers you have in a list, searches those computer's certificate store for expired/expiring certificates and emails you reminders.  The script was designed to run nightly on one central computer. 

Side note: I have a quite a few certificates from StartSSL installed on a bunch of computers.  If you've never heard of StartSSL, I highly recommend checking them out!  You can get free SSL certificates and they're trusted by pretty much all computers these days.

I had a problem with the script I wrote though.  It required that the security context in which the PowerShell script ran had access to all the remote computer's certificate store.  I didn't like this for a couple reasons.  First, there was no central AD group that allows for this.  Which makes sense if you think about it.  Sure you could run it with domain administrator access but you shouldn't be doing that.  Following the Principal of Least Privilege, you want to limit access to the most essential.  So I thought add the user account that runs the PowerShell script to the "Certificate Service DCOM Access" on each individual machine that I wanted to get certificate reminders for.  This seems bad to me.  If that user account gets compromised the attacker has access to all the certificates on all the other computers.  Also the attacker would have access to modify all those certificate stores. 

After talking it over with some people, I decided the best route was to modify the PowerShell script slightly and have it execute on every computer instead of one central computer.  The script would just check its own certificate store and email about its own certificates.  This way the user account running the script doesn't need access to all the other computer's certificate store.  The problems I saw with this approach was that we'd need to change the PowerShell execution policy on all the computers from the default of not allowing scripts to either unrestricted or signed.  Unrestricted execution was out of the question, AllSigned it was.  So off I set to figure out how to sign PowerShell scripts and deploy everything via GPO to make like easier.

I've concluded that there are three approaches to signing scripts.  Using an internal certificate server, using a 3rd party certificate server and manually creating your own certificates. 

There are a ton of articles on how to do it with your own internal CA or a 3rd party CA but I couldn't find one about the manual method.  I didn't have an internal certificate server, so that right away was kind of out of the question.  I wasn't looking to implement one at this time.  Although I'd like to in the future. 

So I started looking at 3rd party CAs.  Specifically I started looking at using StartSSL code signing cert that you get with their class 2 validation level.  I got things working with this but I was worried about what would happen after two years when the code signing certificate expires.  I knew that I could use a timestamp server when signing my code to extend the validity of the script past the certificate's lifetime but when I tried testing it, it didn't seem to work?  Exploring this some more, it turns out that I would need to get StartSSL's extended validation to be able to time stamp with a code signing certificate from StartSSL.  Long story short, the certs created by StartSSL have the Lifetime Signing (1.3.6.1.4.1.311.10.3.13) OID added to them unless you get the extended validation and this OID prevents time stamping from working.  Check out this forum if you'd like a further explanation.

Side note:  They should have called it "No LifeTime Signer" or something a little more intuitive.

I didn't want to spend the money to upgrade to the extended validation level and I didn't want to spend much more time on this.  So I decided to explore the manual route quickly and it turned out to be the best option.  The certificate expires in 2039 and everything can get deployed via GPO to make life easy.  So here are the steps I took.  I'll break it down into 4 broad parts: 

  1. Modifying My Existing PowerShell Script and create Batch File
  2. Creating the Certificates
  3. Signing the PowerShell Scripts
  4. Deploying Everything

Modify the Existing PowerShell Script and Create a Batch File
The original PowerShell script that emails certificate reminders can be found here. I changed a couple lines to have it run on all the computers instead of one.

Change the line:
$Computers = "server1", "server2", "server3" #list of servers to check
To:
$Computers = $env:COMPUTERNAME

This basically keeps the script as is but runs it with only itself in the list of computers.  I also changed the email subjects and body slightly to include friendly names on the certificates.  Here is the modified script, copy it and save it as ExecuteCertReminder.ps1.  Be sure to set your own SMTP server and email addresses in the script.

#this script is intended to be run on one computer, it should then be pushed out with GPO and scheduled to run on each computer
$Computers = $env:COMPUTERNAME #list of computer names "server01", "server02"
$SmtpServer = "smtpserver.domain.com"
$MailFrom = "mailfrom@domain.com"
$MailTo = "mailto@domain.com"

# Change above settings. No need to change anything below.
$DaysToExpire = 30   
# increase $DaysToExpire if you want more of a warning, decrease it if you would like less
$StoreName = "My" 
# change $StoreName above to one of the valuse below if you wish to check a store other then the personal store, for example if you're worried about intermediate CA certs expiring
# "AddressBook", "AuthRoot", "CertificateAuthority", "Disallowed", "Root", TrustedPeople", "TrustedPublisher"

$deadline = (Get-Date).AddDays($DaysToExpire)

ForEach ($c in $Computers) { 
    Try { 

        $store=new-object System.Security.Cryptography.X509Certificates.X509Store("\\$c\$StoreName","LocalMachine")

        $store.open("ReadOnly")

        # all certs
        #$store.certificates | Select *
        
        # will expire certs
        $store.Certificates | ? {$_.NotAfter -le ($deadline-and $_.NotAfter -ge (Get-Date)} | Select *, @{Label="ExpiresIn"; Expression={($_.NotAfter - (Get-Date)).Days}} | % {

            $ExpiresIn = $_.ExpiresIn
            $NotAfter = $_.NotAfter
            $friendlyNAme = $_.FriendlyName

            $emailBody = "The following certificate on $c will expire in $ExpiresIn days on $NotAfter" + [System.Environment]::NewLine
            $emailBody += $_ | select * | Out-String

            Send-MailMessage -SmtpServer $SmtpServer -From $MailFrom -To $MailTo -Subject "Certificate about to Expire on $c - $friendlyNAme" -Body $emailBody

            Write-Host $emailBody
        }
           
        # expired certs
        $store.Certificates | ? {$_.NotAfter -lt (Get-Date)} | Select *, @{Label="ExpiredOn";Expression={$_.NotAfter}} | % {

            $ExpiredOn = $_.ExpiredOn
            $friendlyNAme = $_.FriendlyName

            $emailBody = "The following certificate on $c expired on $ExpiredOn" + [System.Environment]::NewLine
            $emailBody += $_ | select * | Out-String

            Send-MailMessage -SmtpServer $SmtpServer -From $MailFrom -To $MailTo -Subject "CERTIFICATE EXPIRED on $c - $friendlyNAme" -Body $emailBody

            Write-Host $emailBody
        }

    }
    Catch {            
        $emailBody = "Errors detected during certification reminder powershell script execution. " + [System.Environment]::NewLine + [System.Environment]::NewLine + "$($c): $($error[0])" 

        Send-MailMessage -SmtpServer $SmtpServer -From $MailFrom -To $MailTo -Subject "Certification Reminder Powershell Script Execution Error" -Body $emailBody

        Write-Host -foregroundcolor Yellow $emailBody #"$($c): $($error[0])" 
    } 

    Send-MailMessage -SmtpServer $SmtpServer -From $MailFrom -To $MailTo -Subject "ExecuteCertReminder ran on $c " -Body "ExecuteCertReminder ran on $c "
}

Next create a batch file called ExecuteCertReminder.bat containing the following:

powershell.exe -ExecutionPolicy Bypass -Command ". '\\network path will be determined later\ExecuteCertReminder.ps1'"

We'll use this batch file later when we deploy everything with GPO and create a scheduled task.  Note that we're going to deploy ExecuteCertReminder.ps1 and ExecuteCertReminder.bat with the GPO we create later.  They'll be stored in the domain's SysVol alongside the GPO we create later.  Thus we'll be coming back and editing this batch file once we know the path.

Creating the Certificates
I followed Scott Hanselman's blog about signing PowerShell scripts to create the certificates.  I modified his stuff slightly.

First go download the Windows SDK and install it on your workstation to gain access to the makecert executable.

On your workstation, open up the SDK Command Prompt with administrator privileges and run the slightly modified command from Scott's blog:

makecert -n "CN=Your Company Name PowerShell Certificate Root" -a sha256 -eku 1.3.6.1.5.5.7.3.3 -r -sv root.pvk root.cer -ss Root -sr localMachine

A dialog should pop up asking for a password.  Create a password and remember it.


Enter the password you created in the next dialog box.


This creates a public and private key root cert pair that can only be used to make code signing certs and puts the public key in your localmachine root store.  It creates two files.  One with the public key and one with the private key.  Keep these two files some place secure.  You will need them to sign other certificates in the future.  Later we will take this certificate and deploy it to the root certificate store on all your computers in your domain.  Do not lose the private key file if you intend to create other certs later on with this root cert.

Next run the slightly modified command from Scott's blog:

makecert -pe -n "CN=Your company Name PowerShell Signing Cert" -ss MY -a sha256 -eku 1.3.6.1.5.5.7.3.3 -iv root.pvk -ic root.cer

A dialog will pop up asking for the password you created in the previous step.


This creates the certificate pair that you will using for signing your PowerShell scripts and puts it in your personal certificate store.  It's marked as exportable.  You'll want to export it to a PFX file and keep it secure. 

Open the MMC by tying "mmc" and hitting enter at a command prompt.  Once loaded, select File, Add/Remove Snap-In. A dialog opens.


Select "Certificates" and click Add.


Make sure the "My user account" radio is selected and click Finish.  Click Ok on the "Add or Remove Snap-Ins" dialog.

Expand "Certificates - Current User", Expand "Personal" and Select "Certificates".


Find the certificate you created in the right hand pane, select it and right click.  Select "All Tasks", "Export".

An export wizard comes up, click Next.


Select "Yes, export the private key" radio and click next.


Check the "Export all extended properties" check-box and click next.


Check the "Password" check box, enter a password and click Next.



Enter a name for the exported certificate file with a .pfx extension and click Next.  A summary dialog will appear, click Finish and the export should be complete.  You should not have three files.  The root private key, the root public key and the .pfx file containing both the signing certificates key pair.  Key these three files some place safe where no one will have access to them.

Signing the PowerShell Scripts
You could sign the scripts manually but I found a link that showed how to add a menu item to the PowerShell Integrated Scripting Environment.  For me, this simplifies the process of signing scripts.  Once it's setup it's easy to use.  I won't repost the information, please follow the link and read the blog about how to do it.  

I will summarize the process though. 
  1. Download the script from the blog.

  1. The file has a txt extension.  Rename the file to Sign-ISEScript.ps1.

  1. If you have multiple code signing certs you may need to modify this file to select the correct certificate.
  1. To check if you have multiple code signing certificates, run the following from the interactive shell at the bottom of the ISE application:
    Get-ChildItem -Path cert:\CurrentUser\My -CodeSigningCert

  1. If only one certificate is listed, you can go on to step 4.
  2. If more than one certificate is listed, make note of the thumbprint of the certificate you want to use.
  3. You will need to change a line in Sign-ISEScript.ps1 to select the proper certificate.  Find the following line in Sign-ISEScript.ps1:
$cert=get-childitem -Path cert:\currentuser\my -CodeSigningCert

And replace it with

$cert=Get-ChildItem -Path cert:\CurrentUser\My -CodeSigningCert  | where {$_.Thumbprint -eq 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' }

Replace the F's with the thumbprint that you noted earlier.

  1. Edit your ISE profile
    1. In the interactive shell at the bottom, type "psedit $profile".  A tab will open called Microsoft.PowerShellISE_profile.ps1.
    2. Add the following to this tab

      . C:\<path to downloaded file>\Sign-ISEScript.ps1
      $psISE.CurrentPowerShellTab.AddOnsMenu.submenus.Add(“Sign Script”,{Sign-ISEScript},$null) | Out-Null

    3. Save the  file and reopen ISE.  There should now be a menu item called "Sign Script" under the "Add-ons" menu.
       
  1. Open the file you want to sign (ExecuteCertReminder.ps1 in this case), select Add-ons, sign-script.  It should sign your script by adding a signature to the end of the file, save it and reopen.


Deploying Everything
The final major hurdle is to push everything out to the computers you want the script to run on.  I'm not going to get into GPO organization strategies or anything.  I'm just going to setup one GPO with all the settings required.  You can then link this GPO however you want to any OU you want.  Also, you can break this out into multiple GPOs if you so desire.  There are three basic things this GPO needs to do.

  1. Push out the appropriate certificates
  2. Set the PowerShell execution policy to AllSigned
  3. Setup a scheduled task to execute the PowerShell script

Start by opening the Group Policy Management snap-in either on a domain controller or your workstation if you have the remote admin tools installed.  I assume you know how to make a new GPO.   You'll want to edit The GPO.  In the Group Policy Management Editor:

Right click Policy Object Name/Computer Configuration/Policies/Windows Settings/Security Settings/Public Key Policies/Trusted Root Certification Authorities and select import.  Use the root.cer file you created earlier.


Right click Policy Object Name/Computer Configuration/Policies/Windows Settings/Security Settings/Public Key Policies/Trusted Publishers and select import.  Use the "signing cert.pfx" file you exported earlier.

That's all you need to do to push out the certificates.

Continue editing the GPO.  Navigate to Policy Object Name/Computer Configuration/Policies/Administrative Templates/Windows Components/Windows PowerShell.  Double click "Turn on Script Execution" in the right hang pane.  Check the Enabled radio and change the drop down to "Allow only signed scripts".  Click Ok.



Now you'll need to setup the scheduled task to execute nightly.  First we'll copy ExecuteCertReminder.ps1 and ExecuteCertReminder.bat to the corresponding GPO in the SysVol on the domain.  In the Group Policy Management Snap-in find your new GPO and click on the details tab in the right hand pane. 


Note the GUID used for the Unique ID, yours will be different than the one above.  Open windows explorer and navigate to the SysVol share on one of your domain controllers.  There should be a subfolder with your domain name and under that a Policies folder.  Open this policies folder then find and open the folder with the GUID noted previously.  Then navigate farther to Machine\Scripts.  Copy ExecuteCertReminder.ps1 and ExecuteCertReminder.bat this this network location. 

Make note of the full path to the PowerShell script.  You'll need to edit the batch file and enter the full network path to the PowerShell script.

Make note of the full path to the batch file.  We'll need the full path when creating the scheduled task in GPO.

Back in the In the Group Policy Management Editor navigate to Policy Object Name/Computer Configuration/Preferences/Control Panel Settings/Scheduled Tasks.  Right click -> New -> Scheduled Task.  A dialog opens up. 
Set Action to Replace. 
Enter a Name of ExecuteCertReminder.
Enter the entire path to the batch file in the Run textbox.


Click on the schedule tab and setup whatever schedule you would like.  Hit Ok and you should be done.

No comments:

Post a Comment